ognibuild-0.2.6/.cargo_vcs_info.json0000644000000001360000000000100130150ustar { "git": { "sha1": "22bc5d2e6280401537d96e67fea3c6289c3d425a" }, "path_in_vcs": "" }ognibuild-0.2.6/.codespellrc000064400000000000000000000000461046102023000141050ustar 00000000000000[codespell] ignore-words-list = crate ognibuild-0.2.6/.flake8000064400000000000000000000003221046102023000127550ustar 00000000000000banned-modules = "silver-platter = Should not use silver-platter" exclude = "build,.eggs/,target/" extend-ignore = E203, E266, E501, W293, W291 max-line-length = 88 max-complexity = 18 select = B,C,E,F,W,T4,B9 ognibuild-0.2.6/.github/CODEOWNERS000064400000000000000000000000121046102023000145310ustar 00000000000000* @jelmer ognibuild-0.2.6/.github/FUNDING.yml000064400000000000000000000000171046102023000147600ustar 00000000000000github: jelmer ognibuild-0.2.6/.github/dependabot.yml000064400000000000000000000012371046102023000160000ustar 00000000000000# Keep GitHub Actions up to date with GitHub's Dependabot... # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem --- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" - package-ecosystem: "cargo" directory: "/" schedule: interval: "monthly" groups: cargo: patterns: - "*" # Group all Actions updates into a single larger pull request ognibuild-0.2.6/.github/workflows/auto-merge.yaml000064400000000000000000000011341046102023000201320ustar 00000000000000name: Dependabot auto-merge on: pull_request_target permissions: pull-requests: write contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ognibuild-0.2.6/.github/workflows/disperse.yml000064400000000000000000000002741046102023000175460ustar 00000000000000--- name: Disperse configuration "on": - push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: jelmer/action-disperse-validate@v2 ognibuild-0.2.6/.github/workflows/rust.yaml000064400000000000000000000050511046102023000170640ustar 00000000000000--- name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: rust: runs-on: ${{ matrix.os }} strategy: matrix: os: ['ubuntu-latest', macos-latest] fail-fast: false steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: "3.x" - name: Update apt cache if: matrix.os == 'ubuntu-latest' run: sudo apt-get update - name: Install Debian tools on Ubuntu if: matrix.os == 'ubuntu-latest' run: sudo apt-get install -y mmdebstrap - name: Install system breezy and libapt-pkg-dev if: matrix.os == 'ubuntu-latest' run: sudo apt-get install -y brz libapt-pkg-dev libpcre3-dev - name: Install breezy run: pip install breezy - name: Install breezy and brz-debian run: pip install \ git+https://github.com/breezy-team/breezy-debian \ python_apt@git+https://salsa.debian.org/apt-team/python-apt if: matrix.os == 'ubuntu-latest' - name: Build run: cargo build --verbose - name: Run tests # Exclude debian features: run: cargo test --verbose --no-default-features --features=breezy,dep-server,upstream if: matrix.os != 'ubuntu-latest' - name: Run tests run: cargo test --verbose if: matrix.os == 'ubuntu-latest' - name: Run tests (with Debian feature) run: cargo test --verbose --features=debian if: matrix.os == 'ubuntu-latest' minimal-versions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.x" - name: Update apt cache run: sudo apt-get update - name: Install Debian tools run: sudo apt-get install -y mmdebstrap - name: Install system dependencies run: sudo apt-get install -y brz libapt-pkg-dev libpcre3-dev - name: Install breezy and brz-debian run: pip install breezy \ git+https://github.com/breezy-team/breezy-debian \ python_apt@git+https://salsa.debian.org/apt-team/python-apt - name: Install cargo-minimal-versions run: cargo install cargo-minimal-versions cargo-hack - name: Check with minimal versions run: cargo minimal-versions --direct check --all-features - name: Test with minimal versions run: cargo minimal-versions --direct test --all-features ognibuild-0.2.6/.gitignore000064400000000000000000000002031046102023000135700ustar 00000000000000.coverage build *~ ognibuild.egg-info dist __pycache__ .eggs *.swp *.swo *.swn .mypy_cache target *.so .claude/settings.local.json ognibuild-0.2.6/AUTHORS000064400000000000000000000000431046102023000126520ustar 00000000000000Jelmer Vernooij ognibuild-0.2.6/CODE_OF_CONDUCT.md000064400000000000000000000064231046102023000144110ustar 00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project lead at jelmer@jelmer.uk. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ognibuild-0.2.6/Cargo.lock0000644000004334120000000000100107770ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "alloca" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" dependencies = [ "cc", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "apt-sources" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e618dd38bb7506d64302706d30261a2e8036c2ab7c85c4f192ae4480623be2" dependencies = [ "deb822-fast", "url", ] [[package]] name = "ar_archive_writer" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" dependencies = [ "object 0.32.2", ] [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "ascii-canvas" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ "term", ] [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing", ] [[package]] name = "axum-core" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] [[package]] name = "backtrace" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object 0.37.3", "rustc-demangle", "windows-link", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec 0.6.3", ] [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec 0.8.0", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ "serde_core", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "boxcar" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" [[package]] name = "breezyshim" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8efc483b715372b682e6094ddebdb0d6687e1c6f5ff922155495e7bfb2ae3d1d" dependencies = [ "chrono", "ctor", "deb822-lossless 0.5.0", "debian-changelog", "debian-control", "debversion", "difflib", "dirty-tracker", "lazy-regex", "lazy_static", "log", "patchkit", "percent-encoding", "pyo3", "pyo3-filelike", "regex", "serde", "tempfile", "url", "whoami", ] [[package]] name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "buildlog-consultant" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8431342508034596b8627273e67383eee840031dcf61b493376c3c33d682746" dependencies = [ "chrono", "clap", "debian-control", "debversion", "env_logger 0.11.8", "fancy-regex", "inventory", "lazy-regex", "lazy_static", "log", "maplit", "pep508_rs", "regex", "serde", "serde_json", "serde_yaml", "shlex", "text-size", "textwrap", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "charset" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" dependencies = [ "base64", "encoding_rs", ] [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "chumsky" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ "hashbrown 0.14.5", "stacker", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "clap_lex" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "configparser" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom 0.2.16", "once_cell", "tiny-keccak", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" dependencies = [ "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", "itertools 0.13.0", "num-traits", "oorandom", "page_size", "plotters", "rayon", "regex", "serde", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" dependencies = [ "cast", "itertools 0.13.0", ] [[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] name = "csv" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", "serde_core", ] [[package]] name = "csv-core" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] name = "ctor" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb230974aaf0aca4d71665bed0aca156cf43b764fcb9583b69c6c3e686f35e72" dependencies = [ "ctor-proc-macro", "dtor", ] [[package]] name = "ctor-proc-macro" version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deb822-derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bf2d0fa4ce2457e94bd7efb15aeadc115297f04b660bd0da706729e0d91442" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "deb822-fast" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f410ccb5cbd9b81d56b290131bad4350ecf8b46416fb901e759dc1e6916a8198" dependencies = [ "deb822-derive", ] [[package]] name = "deb822-lossless" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e69d6fccb673b9b20f317713e086a53da03e2c821f0e638fe1aa0749bf391c5a" dependencies = [ "regex", "rowan", "serde", ] [[package]] name = "deb822-lossless" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdcadf12851ddb37dc938e724beeb50e83bfe1a1fda3c15b997dc1105ec49e3d" dependencies = [ "pyo3", "regex", "rowan", "serde", ] [[package]] name = "debbugs" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b36dd7d7098ea92d5a789d4b83fade23d79014527ae71f079faea9bdaf1914e7" dependencies = [ "debversion", "lazy-regex", "log", "mailparse", "maplit", "reqwest", "tokio", "xmltree 0.11.0", ] [[package]] name = "debian-analyzer" version = "0.160.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d080d3784895db8fb69b543dbe17b60015bda7e0f767394dc309c4bbf26aa7da" dependencies = [ "breezyshim", "chrono", "configparser", "deb822-lossless 0.4.7", "debian-changelog", "debian-control", "debian-copyright", "debversion", "dep3", "difflib", "distro-info", "filetime", "hex", "lazy-regex", "lazy_static", "log", "makefile-lossless", "maplit", "merge3", "patchkit", "pyo3", "quote", "regex", "reqwest", "semver", "serde", "serde_json", "sha1", "tempfile", "toml_edit", "url", ] [[package]] name = "debian-changelog" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c77ec2b64e3c7baf5413982f17c78933713069eba0601064d78a75bbde8b5b4d" dependencies = [ "chrono", "debversion", "lazy-regex", "log", "rowan", "textwrap", "whoami", ] [[package]] name = "debian-control" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfc2596d8356567e2fdd8653210d94dd06ca8c4ab9679ec6edf443f9efaeb9c3" dependencies = [ "chrono", "deb822-fast", "deb822-lossless 0.5.0", "debversion", "pyo3", "regex", "rowan", "url", ] [[package]] name = "debian-copyright" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8820cd2195ca3f8159d6488419c02c1cca83535050ed60bbf00e5e54e89586c5" dependencies = [ "deb822-fast", "deb822-lossless 0.5.0", "debversion", "regex", ] [[package]] name = "debian-watch" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7af76aefd6ad01b821102369a5026948fe54c57fa79875a4c21f3a3ae767bb5" dependencies = [ "debversion", "m_lexer", "regex", "rowan", "url", ] [[package]] name = "debversion" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4f5cc9ce1d5067bee8060dd75208525dd0133ffea0b2960fef64ab85d58c4c5" dependencies = [ "chrono", "lazy-regex", "num-bigint", "pyo3", "serde", ] [[package]] name = "dep3" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "832c55a9cf6298eb1c6a82b827cabdedac7f9d0053f334739cfdab36637c5305" dependencies = [ "chrono", "deb822-fast", "deb822-lossless 0.5.0", "url", ] [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] [[package]] name = "derive_arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", "crypto-common", "subtle", ] [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users 0.5.2", "windows-sys 0.61.2", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users 0.4.6", "winapi", ] [[package]] name = "dirty-tracker" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57f673af5cabab0d10b822fae4b348c2f5fdc56d90474e26f5dcde0f94fce488" dependencies = [ "notify", "tempfile", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "distro-info" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef12237f2ced990e453ec0b69230752e73be0a357817448c50a62f8bbbe0ca71" dependencies = [ "chrono", "csv", "failure", ] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "document_tree" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6742722dd3e6cd908bc522283cb5502e25f696d1c9904fb251ec266b6b3f9cce" dependencies = [ "anyhow", "regex", "serde", "serde_derive", "url", ] [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dtor" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" dependencies = [ "dtor-proc-macro", ] [[package]] name = "dtor-proc-macro" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] [[package]] name = "ena" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", "humantime 1.3.0", "log", "regex", "termcolor", ] [[package]] name = "env_logger" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime 2.3.0", "log", "regex", "termcolor", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "etcetera" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", "windows-sys 0.48.0", ] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "failure" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" dependencies = [ "backtrace", "failure_derive", ] [[package]] name = "failure_derive" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", "synstructure 0.12.6", ] [[package]] name = "fancy-regex" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ "bit-set 0.8.0", "regex-automata", "regex-syntax 0.8.8", ] [[package]] name = "faster-hex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" dependencies = [ "heapless", "serde", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] [[package]] name = "flume" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", "spin", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" dependencies = [ "autocfg", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsevent-sys" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ "libc", ] [[package]] name = "futf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-intrusive" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", "parking_lot", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getopts" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gix-actor" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "694f6c16eb88b16b00b1d811e4e4bda6f79e9eb467a1b04fd5b848da677baa81" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa", "thiserror 2.0.17", "winnow", ] [[package]] name = "gix-config" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9419284839421488b5ab9b9b88386bdc1e159a986c08e17ffa3e9a5cd2b139f5" dependencies = [ "bstr", "gix-config-value", "gix-features", "gix-glob", "gix-path", "gix-ref", "gix-sec", "memchr", "smallvec", "thiserror 2.0.17", "unicode-bom", "winnow", ] [[package]] name = "gix-config-value" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c489abb061c74b0c3ad790e24a606ef968cebab48ec673d6a891ece7d5aef64" dependencies = [ "bitflags 2.10.0", "bstr", "gix-path", "libc", "thiserror 2.0.17", ] [[package]] name = "gix-date" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f94626a5bc591a57025361a3a890092469e47c7667e59fc143439cd6eaf47fe" dependencies = [ "bstr", "itoa", "jiff", "smallvec", "thiserror 2.0.17", ] [[package]] name = "gix-features" version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa64593d1586135102307fb57fb3a9d3868b6b1f45a4da1352cce5070f8916a" dependencies = [ "gix-path", "gix-trace", "gix-utils", "libc", "prodash", "walkdir", ] [[package]] name = "gix-fs" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f1ecd896258cdc5ccd94d18386d17906b8de265ad2ecf68e3bea6b007f6a28f" dependencies = [ "bstr", "fastrand", "gix-features", "gix-path", "gix-utils", "thiserror 2.0.17", ] [[package]] name = "gix-glob" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74254992150b0a88fdb3ad47635ab649512dff2cbbefca7916bb459894fc9d56" dependencies = [ "bitflags 2.10.0", "bstr", "gix-features", "gix-path", ] [[package]] name = "gix-hash" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826036a9bee95945b0be1e2394c64cd4289916c34a639818f8fd5153906985c1" dependencies = [ "faster-hex", "gix-features", "sha1-checked", "thiserror 2.0.17", ] [[package]] name = "gix-hashtable" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a27d4a3ea9640da504a2657fef3419c517fd71f1767ad8935298bcc805edd195" dependencies = [ "gix-hash", "hashbrown 0.16.1", "parking_lot", ] [[package]] name = "gix-lock" version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "729d7857429a66023bc0c29d60fa21d0d6ae8862f33c1937ba89e0f74dd5c67f" dependencies = [ "gix-tempfile", "gix-utils", "thiserror 2.0.17", ] [[package]] name = "gix-object" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84743d1091c501a56f00d7f4c595cb30f20fcef6503b32ac0a1ff3817efd7b5d" dependencies = [ "bstr", "gix-actor", "gix-date", "gix-features", "gix-hash", "gix-hashtable", "gix-path", "gix-utils", "gix-validate", "itoa", "smallvec", "thiserror 2.0.17", "winnow", ] [[package]] name = "gix-path" version = "0.10.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb06c3e4f8eed6e24fd915fa93145e28a511f4ea0e768bae16673e05ed3f366" dependencies = [ "bstr", "gix-trace", "gix-validate", "thiserror 2.0.17", ] [[package]] name = "gix-ref" version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51330a32f173c8e831731dfef8e93a748c23c057f4b028841f222564cad84cb" dependencies = [ "gix-actor", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-path", "gix-tempfile", "gix-utils", "gix-validate", "memmap2", "thiserror 2.0.17", "winnow", ] [[package]] name = "gix-sec" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9962ed6d9114f7f100efe038752f41283c225bb507a2888903ac593dffa6be" dependencies = [ "bitflags 2.10.0", "gix-path", "libc", "windows-sys 0.61.2", ] [[package]] name = "gix-tempfile" version = "19.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e265fc6b54e57693232a79d84038381ebfda7b1a3b1b8a9320d4d5fe6e820086" dependencies = [ "gix-fs", "libc", "parking_lot", "tempfile", ] [[package]] name = "gix-trace" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d3f59a8de2934f6391b6b3a1a7654eae18961fcb9f9c843533fed34ad0f3457" [[package]] name = "gix-utils" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "befcdbdfb1238d2854591f760a48711bed85e72d80a10e8f2f93f656746ef7c5" dependencies = [ "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" dependencies = [ "bstr", "thiserror 2.0.17", ] [[package]] name = "h2" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ "hashbrown 0.15.5", ] [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", "stable_deref_trait", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "html5ever" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", "markup5ever 0.11.0", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "html5ever" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" dependencies = [ "log", "markup5ever 0.36.1", ] [[package]] name = "http" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" dependencies = [ "quick-error", ] [[package]] name = "humantime" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2", "system-configuration", "tokio", "tower-service", "tracing", "windows-registry", ] [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "inotify" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ "bitflags 1.3.2", "inotify-sys", "libc", ] [[package]] name = "inotify-sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] [[package]] name = "inventory" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", ] [[package]] name = "is-terminal" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", "windows-sys 0.61.2", ] [[package]] name = "jiff-static" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "jiff-tzdb" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "js-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "kqueue" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", ] [[package]] name = "kqueue-sys" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] name = "lalrpop" version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" dependencies = [ "ascii-canvas", "bit-set 0.5.3", "diff", "ena", "is-terminal", "itertools 0.10.5", "lalrpop-util", "petgraph", "regex", "regex-syntax 0.6.29", "string_cache 0.8.9", "term", "tiny-keccak", "unicode-xid", ] [[package]] name = "lalrpop-util" version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" dependencies = [ "regex", ] [[package]] name = "lazy-regex" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.111", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", "redox_syscall", ] [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "pkg-config", "vcpkg", ] [[package]] name = "libz-rs-sys" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" dependencies = [ "twox-hash", ] [[package]] name = "lzma-rs" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" dependencies = [ "byteorder", "crc", ] [[package]] name = "m_lexer" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7e51ebf91162d585a5bae05e4779efc4a276171cb880d61dd6fab11c98467a7" dependencies = [ "regex", ] [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mailparse" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f" dependencies = [ "charset", "data-encoding", "quoted_printable", ] [[package]] name = "makefile-lossless" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a4db5908bb68ae2e0afc45301a4cb9aaceb364a06865a08efe28324a60f9217" dependencies = [ "log", "rowan", ] [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", "phf 0.10.1", "phf_codegen 0.10.0", "string_cache 0.8.9", "string_cache_codegen 0.5.4", "tendril", ] [[package]] name = "markup5ever" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" dependencies = [ "log", "tendril", "web_atoms", ] [[package]] name = "markup5ever_rcdom" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" dependencies = [ "html5ever 0.26.0", "markup5ever 0.11.0", "tendril", "xml5ever", ] [[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ "regex-automata", ] [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", "digest", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "merge3" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4b277bc3c7e2bc163abc6c0069f53715b52dc34442c0e807cc8758c7113524f" dependencies = [ "clap", "difflib", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "mio" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "notify" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ "bitflags 2.10.0", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-bigint-dig" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "rand 0.8.5", "smallvec", "zeroize", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "ognibuild" version = "0.2.6" dependencies = [ "apt-sources", "axum", "breezyshim", "buildlog-consultant", "chrono", "clap", "criterion", "deb822-lossless 0.5.0", "debian-analyzer", "debian-changelog", "debian-control", "debversion", "dirs", "env_logger 0.11.8", "flate2", "fs_extra", "inventory", "lazy-regex", "lazy_static", "libc", "log", "lz4_flex", "lzma-rs", "makefile-lossless", "maplit", "nix", "pep508_rs", "percent-encoding", "pyo3", "pyproject-toml", "r-description", "rand 0.9.2", "regex", "reqwest", "semver", "serde", "serde_json", "serde_yaml", "shlex", "sqlx", "stackdriver_logger", "tempfile", "test-log", "tokio", "toml 0.9.8", "toml_edit", "upstream-ontologist", "url", "whoami", "xmltree 0.11.0", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opam-file-rs" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dc9fde26706c9170630772dd86981d874e9a3107cc456c811e1ee234e0c4863" dependencies = [ "lalrpop", "lalrpop-util", "thiserror 1.0.69", ] [[package]] name = "openssl" version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", ] [[package]] name = "page_size" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "patchkit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21e87e02a475262c3166d32fea34710510448b37117cc448c1be03975816baf" dependencies = [ "chrono", "lazy-regex", "lazy_static", "once_cell", "proc-macro2", "regex", "rowan", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "pep440_rs" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" dependencies = [ "once_cell", "serde", "unicode-width", "unscanny", "version-ranges", ] [[package]] name = "pep508_rs" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" dependencies = [ "boxcar", "indexmap", "itertools 0.13.0", "once_cell", "pep440_rs", "regex", "rustc-hash 2.1.1", "serde", "smallvec", "thiserror 1.0.69", "unicode-width", "url", "urlencoding", "version-ranges", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "pest_meta" version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" dependencies = [ "pest", "sha2", ] [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "phf" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ "phf_shared 0.10.0", ] [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_shared 0.13.1", "serde", ] [[package]] name = "phf_codegen" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator 0.10.0", "phf_shared 0.10.0", ] [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ "phf_generator 0.13.1", "phf_shared 0.13.1", ] [[package]] name = "phf_generator" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", "rand 0.8.5", ] [[package]] name = "phf_generator" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", "rand 0.8.5", ] [[package]] name = "phf_generator" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", "phf_shared 0.13.1", ] [[package]] name = "phf_shared" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher 0.3.11", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher 1.0.1", ] [[package]] name = "phf_shared" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher 1.0.1", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", ] [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "pretty_env_logger" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" dependencies = [ "env_logger 0.7.1", "log", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a6efc566849d3d9d737c5cb06cc50e48950ebe3d3f9d70631490fff3a07b139" dependencies = [ "parking_lot", ] [[package]] name = "psm" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ "ar_archive_writer", "cc", ] [[package]] name = "pulldown-cmark" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", "unicase", ] [[package]] name = "pulldown-cmark-escape" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pyo3" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ "chrono", "indoc", "libc", "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", "serde", "unindent", ] [[package]] name = "pyo3-build-config" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", ] [[package]] name = "pyo3-filelike" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57429f455b9811f2a8af73d8bae91e028fbf6f62ad4011073c2248bb028a2288" dependencies = [ "pyo3", ] [[package]] name = "pyo3-macros" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", "syn 2.0.111", ] [[package]] name = "pyo3-macros-backend" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", "syn 2.0.111", ] [[package]] name = "pyproject-toml" version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d755483ad14b49e76713b52285235461a5b4f73f17612353e11a5de36a5fd2" dependencies = [ "indexmap", "pep440_rs", "pep508_rs", "serde", "thiserror 2.0.17", "toml 0.9.8", ] [[package]] name = "python-pkginfo" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464e5e2e0fb6c8c2c7aedc0cd6615258a3def4e34b417f6bf8835e76e7d441d4" dependencies = [ "flate2", "fs-err", "mailparse", "rfc2047-decoder", "tar", "thiserror 2.0.17", "zip", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r-description" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f97b1f65f8b0a2687d665c2d16e866f69c5c2f0486ccf905b776badacb3ebb53" dependencies = [ "deb822-derive", "deb822-fast", "deb822-lossless 0.5.0", "rowan", "serde", "url", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.3", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.4", ] [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.10.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 2.0.17", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax 0.8.8", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.8", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "rfc2047-decoder" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc36545d1021456a751b573517cb52e8c339b2f662e6b2778ef629282678de29" dependencies = [ "base64", "charset", "chumsky", "memchr", "quoted_printable", "thiserror 2.0.17", ] [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rowan" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" dependencies = [ "countme", "hashbrown 0.14.5", "rustc-hash 1.1.0", "text-size", ] [[package]] name = "rsa" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", "signature", "spki", "subtle", "zeroize", ] [[package]] name = "rst_parser" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f3029872a42c0be67d86e3e88bf8c1e73d1da3d714da00b9c29f60a4605bfb1" dependencies = [ "anyhow", "document_tree", "pest", "pest_derive", ] [[package]] name = "rst_renderer" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf982408766e5055367c60382b78dcee50c83b2b731e036a8b510e0aedf1efa1" dependencies = [ "anyhow", "document_tree", "serde-xml-rs", "serde_json", ] [[package]] name = "rust-ini" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "select" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5910c1d91bd7e6e178c0f8eb9e4ad01f814064b4a1c0ae3c906224a3cbf12879" dependencies = [ "bit-set 0.5.3", "html5ever 0.26.0", "markup5ever_rcdom", ] [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", "serde_core", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde-xml-rs" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" dependencies = [ "log", "serde", "thiserror 1.0.69", "xml-rs", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_path_to_error" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", "serde_core", ] [[package]] name = "serde_spanned" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ "indexmap", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha1-checked" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest", "sha1", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] [[package]] name = "sqlx" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", ] [[package]] name = "sqlx-core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", "crc", "crossbeam-queue", "either", "event-listener", "futures-core", "futures-intrusive", "futures-io", "futures-util", "hashbrown 0.15.5", "hashlink", "indexmap", "log", "memchr", "native-tls", "once_cell", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", "url", ] [[package]] name = "sqlx-macros" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", "syn 2.0.111", ] [[package]] name = "sqlx-macros-core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", "heck", "hex", "once_cell", "proc-macro2", "quote", "serde", "serde_json", "sha2", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "syn 2.0.111", "tokio", "url", ] [[package]] name = "sqlx-mysql" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", "bitflags 2.10.0", "byteorder", "bytes", "crc", "digest", "dotenvy", "either", "futures-channel", "futures-core", "futures-io", "futures-util", "generic-array", "hex", "hkdf", "hmac", "itoa", "log", "md-5", "memchr", "once_cell", "percent-encoding", "rand 0.8.5", "rsa", "serde", "sha1", "sha2", "smallvec", "sqlx-core", "stringprep", "thiserror 2.0.17", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", "bitflags 2.10.0", "byteorder", "crc", "dotenvy", "etcetera", "futures-channel", "futures-core", "futures-util", "hex", "hkdf", "hmac", "home", "itoa", "log", "md-5", "memchr", "once_cell", "rand 0.8.5", "serde", "serde_json", "sha2", "smallvec", "sqlx-core", "stringprep", "thiserror 2.0.17", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "flume", "futures-channel", "futures-core", "futures-executor", "futures-intrusive", "futures-util", "libsqlite3-sys", "log", "percent-encoding", "serde", "serde_urlencoded", "sqlx-core", "thiserror 2.0.17", "tracing", "url", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stackdriver_logger" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7935e3a17a0164bf9b908908b07514326119a677aab39370e957a4cd76d8dd7" dependencies = [ "chrono", "env_logger 0.9.3", "log", "pretty_env_logger", "serde_json", "toml 0.5.11", ] [[package]] name = "stacker" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", "libc", "psm", "windows-sys 0.59.0", ] [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared 0.11.3", "precomputed-hash", "serde", ] [[package]] name = "string_cache" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared 0.13.1", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", ] [[package]] name = "string_cache_codegen" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ "phf_generator 0.13.1", "phf_shared 0.13.1", "proc-macro2", "quote", ] [[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ "unicode-bidi", "unicode-normalization", "unicode-properties", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", "unicode-xid", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tar" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", "xattr", ] [[package]] name = "target-lexicon" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "tendril" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", "utf-8", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "test-log" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" dependencies = [ "env_logger 0.11.8", "test-log-macros", "tracing-subscriber", ] [[package]] name = "test-log-macros" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "textwrap" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio 1.1.0", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-util" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "toml" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_parser" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower-http" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags 2.10.0", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "tracing-core" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bom" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "unscanny" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "upstream-ontologist" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "197b703e56d251974337b5c6cc62ec86991ff4794240018f31f97314ae531a47" dependencies = [ "async-trait", "bit-set 0.8.0", "bit-vec 0.8.0", "breezyshim", "chrono", "configparser", "debbugs", "debian-changelog", "debian-control", "debian-copyright", "debian-watch", "debversion", "distro-info", "futures", "gix-config", "html5ever 0.36.1", "lazy-regex", "lazy_static", "log", "makefile-lossless", "maplit", "opam-file-rs", "openssl", "percent-encoding", "pulldown-cmark", "pyo3", "pyo3-filelike", "pyproject-toml", "python-pkginfo", "quote", "r-description", "regex", "reqwest", "rst_parser", "rst_renderer", "rust-ini", "select", "semver", "serde", "serde_json", "serde_yaml", "shlex", "tendril", "textwrap", "tokio", "toml 0.9.8", "url", "xmltree 0.12.0", ] [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-ranges" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3595ffe225639f1e0fd8d7269dcc05d2fbfea93cfac2fea367daf1adb60aae91" dependencies = [ "smallvec", ] [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "web_atoms" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd0c322f146d0f8aad130ce6c187953889359584497dac6561204c8e17bb43d" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", "string_cache 0.9.0", "string_cache_codegen 0.6.1", ] [[package]] name = "whoami" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ "libredox", "wasite", "web-sys", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.5", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", ] [[package]] name = "xml" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2df5825faced2427b2da74d9100f1e2e93c533fff063506a81ede1cf517b2e7e" [[package]] name = "xml-rs" version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xml5ever" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" dependencies = [ "log", "mac", "markup5ever 0.11.0", ] [[package]] name = "xmltree" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6" dependencies = [ "xml-rs", ] [[package]] name = "xmltree" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc04313cab124e498ab1724e739720807b6dc405b9ed0edc5860164d2e4ff70" dependencies = [ "xml", ] [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", "synstructure 0.13.2", ] [[package]] name = "zerocopy" version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", "synstructure 0.13.2", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn 2.0.111", ] [[package]] name = "zip" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" dependencies = [ "arbitrary", "crc32fast", "flate2", "indexmap", "memchr", "zopfli", ] [[package]] name = "zlib-rs" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] ognibuild-0.2.6/Cargo.toml0000644000000125600000000000100110170ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "ognibuild" version = "0.2.6" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false default-run = "ogni" description = "Detect and run any build system" homepage = "https://github.com/jelmer/ognibuild" readme = "README.md" license = "GPL-2.0+" repository = "https://github.com/jelmer/ognibuild.git" [features] breezy = ["dep:breezyshim"] cli = [ "dep:clap", "dep:env_logger", ] debian = [ "dep:debian-changelog", "dep:debversion", "dep:debian-control", "dep:apt-sources", "dep:flate2", "dep:lzma-rs", "dep:lz4_flex", "dep:reqwest", "breezyshim/debian", "dep:debian-analyzer", ] default = [ "cli", "upstream", "breezy", "dep-server", ] dep-server = [ "dep:axum", "dep:tokio", ] udd = [ "dep:sqlx", "dep:tokio", "debian", ] upstream = [ "dep:upstream-ontologist", "dep:tokio", ] [lib] name = "ognibuild" path = "src/lib.rs" [[bin]] name = "deb-fix-build" path = "src/bin/deb-fix-build.rs" required-features = [ "debian", "cli", "breezy", ] [[bin]] name = "deb-upstream-deps" path = "src/bin/deb-upstream-deps.rs" required-features = [ "cli", "debian", "breezy", ] [[bin]] name = "dep-server" path = "src/bin/dep-server.rs" required-features = [ "dep-server", "cli", "debian", ] [[bin]] name = "ogni" path = "src/bin/ogni.rs" required-features = ["cli"] [[bin]] name = "ognibuild-deb" path = "src/bin/ognibuild-deb.rs" required-features = [ "cli", "debian", "breezy", ] [[bin]] name = "ognibuild-dist" path = "src/bin/ognibuild-dist.rs" required-features = [ "cli", "breezy", ] [[bin]] name = "report-apt-deps-status" path = "src/bin/report-apt-deps-status.rs" required-features = [ "cli", "debian", ] [[example]] name = "apt-file-search" path = "examples/apt-file-search.rs" required-features = [ "cli", "debian", ] [[bench]] name = "apt_file_search" path = "benches/apt_file_search.rs" harness = false required-features = ["debian"] [dependencies.apt-sources] version = "0.1.1" optional = true [dependencies.axum] version = "0.8" features = [ "json", "http2", "tokio", ] optional = true [dependencies.breezyshim] version = "0.7.4" optional = true [dependencies.buildlog-consultant] version = "0.1.1" [dependencies.chrono] version = "0.4.42" [dependencies.clap] version = "4.5" features = [ "derive", "env", ] optional = true [dependencies.deb822-lossless] version = ">=0.4.7,<0.6" [dependencies.debian-analyzer] version = "0.160.1" optional = true [dependencies.debian-changelog] version = "0.2.3" optional = true [dependencies.debian-control] version = "0.2.7" optional = true [dependencies.debversion] version = "0.5" optional = true [dependencies.dirs] version = ">=5,<7" [dependencies.env_logger] version = ">=0.10" optional = true [dependencies.flate2] version = "1.0.33" optional = true [dependencies.fs_extra] version = "1.3.0" [dependencies.inventory] version = ">=0.3" [dependencies.lazy-regex] version = "3.4" [dependencies.lazy_static] version = "1.5" [dependencies.libc] version = "0.2.172" [dependencies.log] version = "0.4.21" [dependencies.lz4_flex] version = ">=0.11" optional = true [dependencies.lzma-rs] version = "0.3.0" optional = true [dependencies.makefile-lossless] version = ">=0.2.1,<0.4" [dependencies.maplit] version = "1.0.2" [dependencies.nix] version = ">=0.27.0" features = ["user"] [dependencies.pep508_rs] version = "0.9.1" [dependencies.percent-encoding] version = "2.3.1" [dependencies.pyo3] version = "0.27" [dependencies.pyproject-toml] version = "0.13.4" [dependencies.r-description] version = "0.3.5" features = ["serde"] [dependencies.rand] version = "0.9.2" [dependencies.regex] version = "1.11.2" [dependencies.reqwest] version = "0.12.15" features = [ "blocking", "json", ] optional = true [dependencies.semver] version = "1.0.26" [dependencies.serde] version = "1.0.219" features = ["derive"] [dependencies.serde_json] version = "1.0.120" [dependencies.serde_yaml] version = "0.9.34" [dependencies.shlex] version = "1.3.0" [dependencies.sqlx] version = "0.8.6" features = [ "postgres", "runtime-tokio-native-tls", ] optional = true [dependencies.stackdriver_logger] version = "0.8.2" optional = true [dependencies.tempfile] version = "3.23" [dependencies.tokio] version = "1.47.1" features = ["full"] optional = true [dependencies.toml] version = "0.9.8" [dependencies.toml_edit] version = "0.23.7" [dependencies.upstream-ontologist] version = "0.3.2" optional = true [dependencies.url] version = "2.5.4" [dependencies.whoami] version = "1.5" default-features = false [dependencies.xmltree] version = "0.11" [dev-dependencies.criterion] version = ">=0.5, <0.9" features = ["html_reports"] [dev-dependencies.lazy_static] version = "1.5" [dev-dependencies.test-log] version = "0.2" ognibuild-0.2.6/Cargo.toml.orig000064400000000000000000000071771046102023000145100ustar 00000000000000[package] name = "ognibuild" version = "0.2.6" authors = [ "Jelmer Vernooij "] edition = "2021" license = "GPL-2.0+" description = "Detect and run any build system" repository = "https://github.com/jelmer/ognibuild.git" homepage = "https://github.com/jelmer/ognibuild" default-run = "ogni" [dependencies] pyo3 = "0.27" breezyshim = { version = "0.7.4", optional = true } buildlog-consultant = { version = "0.1.1" } upstream-ontologist = { version = "0.3.2", optional = true } axum = { version = "0.8", optional = true, features = ["json", "http2", "tokio"] } chrono = "0.4.42" clap = { version = "4.5", features = ["derive", "env"], optional = true } deb822-lossless = ">=0.4.7,<0.6" debian-analyzer = { version = "0.160.1", optional = true } debian-changelog = { version = "0.2.3", optional = true } debian-control = { version = "0.2.7", optional = true } apt-sources = { version = "0.1.1", optional = true } debversion = { version = "0.5", optional = true } env_logger = { version = ">=0.10", optional = true } flate2 = { version = "1.0.33", optional = true } fs_extra = "1.3.0" inventory = ">=0.3" lazy-regex = "3.4" lazy_static = "1.5" libc = "0.2.172" log = "0.4.21" lz4_flex = { version = ">=0.11", optional = true } lzma-rs = { version = "0.3.0", optional = true } makefile-lossless = ">=0.2.1,<0.4" maplit = "1.0.2" nix = { version = ">=0.27.0", features = ["user"] } pep508_rs = "0.9.1" percent-encoding = "2.3.1" pyproject-toml = "0.13.4" r-description = { version = "0.3.5", features = ["serde"] } rand = "0.9.2" regex = "1.11.2" reqwest = { version = "0.12.15", optional = true, features = ["blocking", "json"] } semver = "1.0.26" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.120" serde_yaml = "0.9.34" shlex = "1.3.0" sqlx = { version = "0.8.6", optional = true, features = ["postgres", "runtime-tokio-native-tls"] } stackdriver_logger = { version = "0.8.2", optional = true } tempfile = "3.23" tokio = { version = "1.47.1", features = ["full"], optional = true } toml = "0.9.8" toml_edit = "0.23.7" url = "2.5.4" whoami = { version = "1.5", default-features = false } xmltree = "0.11" dirs = ">=5,<7" [features] default = ["cli", "upstream", "breezy", "dep-server"] debian = ["dep:debian-changelog", "dep:debversion", "dep:debian-control", "dep:apt-sources", "dep:flate2", "dep:lzma-rs", "dep:lz4_flex", "dep:reqwest", "breezyshim/debian", "dep:debian-analyzer"] cli = ["dep:clap", "dep:env_logger"] udd = ["dep:sqlx", "dep:tokio", "debian"] dep-server = ["dep:axum", "dep:tokio"] upstream = ["dep:upstream-ontologist", "dep:tokio"] breezy = ["dep:breezyshim"] [dev-dependencies] lazy_static = "1.5" test-log = "0.2" criterion = { version = ">=0.5, <0.9", features = ["html_reports"] } [[bin]] name = "ognibuild-deb" path = "src/bin/ognibuild-deb.rs" required-features = ["cli", "debian", "breezy"] [[example]] name = "apt-file-search" path = "examples/apt-file-search.rs" required-features = ["cli", "debian"] [[bin]] name = "dep-server" path = "src/bin/dep-server.rs" required-features = ["dep-server", "cli", "debian"] [[bin]] name = "ognibuild-dist" path = "src/bin/ognibuild-dist.rs" required-features = ["cli", "breezy"] [[bin]] name = "ogni" path = "src/bin/ogni.rs" required-features = ["cli"] [[bin]] name = "deb-fix-build" path = "src/bin/deb-fix-build.rs" required-features = ["debian", "cli", "breezy"] [[bin]] name = "deb-upstream-deps" path = "src/bin/deb-upstream-deps.rs" required-features = ["cli", "debian", "breezy"] [[bin]] name = "report-apt-deps-status" path = "src/bin/report-apt-deps-status.rs" required-features = ["cli", "debian"] [[bench]] name = "apt_file_search" harness = false required-features = ["debian"] ognibuild-0.2.6/LICENSE000064400000000000000000000432541046102023000126220ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ognibuild-0.2.6/Makefile000064400000000000000000000004651046102023000132520ustar 00000000000000check:: style style: ruff check py check:: testsuite build-inplace: python3 setup.py build_rust --inplace testsuite: cargo test check:: typing typing: mypy py tests coverage: PYTHONPATH=$(shell pwd)/py python3 -m coverage run -m unittest tests.test_suite coverage-html: python3 -m coverage html ognibuild-0.2.6/README.md000064400000000000000000000107161046102023000130710ustar 00000000000000# ognibuild Ognibuild is a simple wrapper with a common interface for invoking any kind of build tool. The ideas is that it can be run to build and install any source code directory by detecting the build system that is in use and invoking that with the correct parameters. It can also detect and install missing dependencies. ## Goals The goal of ognibuild is to provide a consistent CLI that can be used for any software package. It is mostly useful for automated building of large sets of diverse packages (e.g. different programming languages). It is not meant to expose all functionality that is present in the underlying build systems. To use that, invoke those build systems directly. ## Usage Ognibuild has a number of subcommands: * ``ogni clean`` - remove any built artifacts * ``ogni dist`` - create a source tarball * ``ogni build`` - build the package in-tree * ``ogni install`` - install the package * ``ogni test`` - run the testsuite in the source directory * ``ogni cache-env`` - cache a Debian cloud image for testing (Linux only) It also includes a subcommand that can fix up the build dependencies for Debian packages, called deb-fix-build. ### Examples ``` ogni -d https://gitlab.gnome.org/GNOME/fractal install ``` ### Caching Test Environments On Linux systems with the `debian` feature enabled, ognibuild can use cached Debian cloud images to speed up tests that require a Debian environment. This is particularly useful for running tests with `UnshareSession`. To cache a Debian image: ```bash ogni cache-env sid ``` Once cached, tests will automatically use the cached image instead of bootstrapping a new environment from the network. This significantly reduces test execution time. You can also specify a different Debian suite: ```bash ogni cache-env bookworm ogni cache-env stable ``` The cached images are stored in `~/.cache/ognibuild/images/`. To run tests without network access: 1. First, cache an image and build everything (requires network): ```bash ogni cache-env sid cargo build --all-targets ``` 2. Run tests in a network-isolated environment (requires sudo): ```bash sudo CARGO_HOME=$HOME/.cargo OGNIBUILD_DEBIAN_TEST_TARBALL=$HOME/.cache/ognibuild/images/debian-sid-amd64.tar.gz unshare -n cargo test --frozen ``` The `CARGO_HOME` environment variable ensures cargo finds the downloaded dependencies. The `OGNIBUILD_DEBIAN_TEST_TARBALL` environment variable points to the cached Debian image. The `--frozen` flag prevents cargo from accessing the network. ### Environment Variables Ognibuild respects the following environment variables: - `OGNIBUILD_DISABLE_NET` - When set to `1`, `true`, `yes`, or `on` (case-insensitive), prevents the `ogni cache-env` CLI command from downloading Debian images. Note: This only affects the CLI tool, not library code. - `OGNIBUILD_DEPS` - URL of the ognibuild dependency server to use for resolving dependencies. - `OGNIBUILD_DEBIAN_TEST_TARBALL` - Path to a custom Debian tarball to use for testing instead of downloading one. ### Running Tests Without Network Access To run tests in a network-isolated environment: 1. First, cache a Debian image (requires network): ```bash ogni cache-env sid ``` 2. Then run tests in a network namespace (requires root or CAP_NET_ADMIN): ```bash sudo unshare -n -- sudo -u $USER bash -c 'cd $(pwd) && cargo test' ``` If no cached image exists and network is unavailable, tests will fail with a clear error message indicating that either a cached image or network access is required. ### Debugging If you run into any issues, please see [Debugging](notes/debugging.md). ## Status Ognibuild is functional, but sometimes rough around the edges. If you run into issues (or lack of support for a particular ecosystem), please file a bug. ### Supported Build Systems - Bazel - Cabal - Cargo - Golang - Gradle - Make, including various makefile generators: - autoconf/automake - CMake - Makefile.PL - qmake - Maven - ninja, including ninja file generators: - meson - Node - Octave - Perl - Module::Build::Tiny - Dist::Zilla - Minilla - PHP Pear - Python - setup.py/setup.cfg/pyproject.toml - R - Ruby gems - Waf ### Supported package repositories Package repositories are used to install missing dependencies. The following "native" repositories are supported: - pypi - cpan - hackage - npm - cargo - cran - golang\* As well one distribution repository: - apt ## License Ognibuild is licensed under the GNU GPL, v2 or later. ognibuild-0.2.6/SECURITY.md000064400000000000000000000004441046102023000134000ustar 00000000000000# Security Policy ## Supported Versions ognibuild is still under heavy development. Only the latest version is security supported. ## Reporting a Vulnerability Please report security issues by e-mail to jelmer@jelmer.uk, ideally PGP encrypted to the key at https://jelmer.uk/D729A457.asc ognibuild-0.2.6/TODO000064400000000000000000000003171046102023000122760ustar 00000000000000- Need to be able to check up front whether a requirement is satisfied, before attempting to install it (which is more expensive) - Cache parsed Contents files during test suite runs and/or speed up reading ognibuild-0.2.6/benches/apt_file_search.rs000064400000000000000000000406331046102023000167000ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use ognibuild::debian::file_search::{ get_apt_contents_file_searcher, setup_apt_file, FileSearcher, RemoteContentsFileSearcher, }; use ognibuild::session::unshare::UnshareSession; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::time::Duration; /// Benchmark data structure to hold different searcher implementations struct BenchmarkData { remote_searcher: Option, local_searcher: Option, common_paths: Vec<&'static str>, common_regexes: Vec<&'static str>, } impl BenchmarkData { fn new() -> Self { println!("Loading real Debian Contents files for benchmarking..."); // Create RemoteContents searcher - downloads from internet println!("Creating RemoteContents searcher (downloads from internet)..."); let remote_searcher = match UnshareSession::bootstrap() { Ok(session) => { // Set up apt-file in the session match setup_apt_file(&session) { Ok(_) => { println!("APT setup completed successfully"); // Now get the file searcher match get_apt_contents_file_searcher(&session) { Ok(searcher) => { println!("RemoteContents searcher created successfully"); // Convert Box to RemoteContentsFileSearcher if possible // For now, create a new one via from_session match RemoteContentsFileSearcher::from_session(&session) { Ok(contents_searcher) => Some(contents_searcher), Err(e) => { println!("Warning: Failed to create RemoteContentsFileSearcher: {}", e); None } } } Err(e) => { println!("Warning: Failed to get file searcher: {}", e); None } } } Err(e) => { println!("Warning: Failed to set up APT: {}", e); None } } } Err(e) => { println!( "Warning: Failed to bootstrap UnshareSession for remote: {}", e ); None } }; // Create LocalApt searcher - uses local APT cache println!("Creating LocalApt searcher (uses local /var/lib/apt/lists)..."); let local_searcher = match UnshareSession::bootstrap() { Ok(session) => { // Create a temporary searcher to test local loading match RemoteContentsFileSearcher::from_session(&session) { Ok(mut searcher) => { // Try to reload from local cache instead match searcher.load_local() { Ok(_) => { println!("LocalApt searcher created successfully"); Some(searcher) } Err(e) => { println!("Warning: Failed to load from local APT cache: {}", e); // Return the remote searcher as fallback Some(searcher) } } } Err(e) => { println!("Warning: Failed to create searcher for local APT: {}", e); None } } } Err(e) => { println!("Warning: Failed to bootstrap session for local APT: {}", e); None } }; // Realistic file paths from actual Debian sid (these will match real entries) let common_paths = vec![ // High-frequency exact matches (single results) "/usr/bin/python3.11", "/usr/bin/gcc-12", "/usr/bin/git", "/usr/bin/vim.basic", "/usr/bin/curl", "/usr/bin/make", "/usr/bin/node", // Common libraries (single results) "/lib/x86_64-linux-gnu/libc.so.6", "/usr/lib/x86_64-linux-gnu/libssl.so.3", "/usr/lib/x86_64-linux-gnu/libcurl.so.4", // Header files (single results) "/usr/include/stdio.h", "/usr/include/openssl/ssl.h", // Config files (single results) "/etc/hostname", "/etc/passwd", // Non-existent files (zero results) "/usr/bin/nonexistent-binary", "/etc/nonexistent.conf", ]; // Realistic regex patterns with different result volumes based on actual Debian contents let common_regexes = vec![ // High volume matches (thousands of results) r"^/usr/share/doc/", // ~50k+ matches r"^/usr/lib/python3\.\d+/", // ~20k+ matches r"^/usr/share/locale/.*/LC_MESSAGES/", // ~10k+ matches // Medium volume matches (hundreds of results) r"^/usr/bin/[^/]*$", // ~5k matches r"^/usr/lib/x86_64-linux-gnu/lib.*\.so", // ~2k matches r"^/usr/include/.*\.h$", // ~15k matches // Low volume matches (tens of results) r"^/usr/bin/.*gcc.*", // ~50 matches r"^/usr/bin/python.*", // ~20 matches r"^/etc/.*systemd.*", // ~30 matches // Very specific matches (few results) r"firefox", // ~100 matches across all files r"vim.*", // ~200 matches // Zero matches r"^/nonexistent/path/", r"totallyfakepattern123", ]; println!("Benchmark setup complete"); BenchmarkData { remote_searcher, local_searcher, common_paths, common_regexes, } } fn create_realistic_memory_db() -> HashMap { // Create a realistic subset of data for memory searcher let mut db = HashMap::new(); // Add common binaries db.insert( PathBuf::from("/usr/bin/python3.11"), "python3.11".to_string(), ); db.insert(PathBuf::from("/usr/bin/gcc-12"), "gcc-12".to_string()); db.insert(PathBuf::from("/usr/bin/git"), "git".to_string()); db.insert(PathBuf::from("/usr/bin/vim.basic"), "vim".to_string()); db.insert(PathBuf::from("/usr/bin/curl"), "curl".to_string()); db.insert(PathBuf::from("/usr/bin/make"), "make".to_string()); // Add some libraries db.insert( PathBuf::from("/lib/x86_64-linux-gnu/libc.so.6"), "libc6".to_string(), ); db.insert( PathBuf::from("/usr/lib/x86_64-linux-gnu/libssl.so.3"), "libssl3".to_string(), ); // Add some headers db.insert( PathBuf::from("/usr/include/stdio.h"), "libc6-dev".to_string(), ); db.insert( PathBuf::from("/usr/include/openssl/ssl.h"), "libssl-dev".to_string(), ); // Add config files db.insert(PathBuf::from("/etc/hostname"), "base-files".to_string()); db.insert(PathBuf::from("/etc/passwd"), "base-passwd".to_string()); // Add many more entries to simulate realistic volume for i in 0..1000 { db.insert( PathBuf::from(format!("/usr/share/doc/package{}/README", i)), format!("package{}", i), ); db.insert( PathBuf::from(format!("/usr/lib/python3.11/site-packages/module{}.py", i)), "python3.11".to_string(), ); } db } } fn bench_exact_path_searches(c: &mut Criterion) { let data = BenchmarkData::new(); let mut group = c.benchmark_group("exact_path_search"); group.measurement_time(Duration::from_secs(10)); // Benchmark remote searcher if available if let Some(ref searcher) = data.remote_searcher { group.bench_with_input( BenchmarkId::new("single_path_search", "remote"), searcher, |b, searcher| { b.iter(|| { for path in &data.common_paths { let results: Vec<_> = searcher .search_files(black_box(Path::new(path)), false) .collect(); black_box(results); } }); }, ); group.bench_with_input( BenchmarkId::new("case_insensitive_search", "remote"), searcher, |b, searcher| { b.iter(|| { for path in &data.common_paths { let results: Vec<_> = searcher .search_files(black_box(Path::new(path)), true) .collect(); black_box(results); } }); }, ); } // Benchmark local searcher if available if let Some(ref searcher) = data.local_searcher { group.bench_with_input( BenchmarkId::new("single_path_search", "local"), searcher, |b, searcher| { b.iter(|| { for path in &data.common_paths { let results: Vec<_> = searcher .search_files(black_box(Path::new(path)), false) .collect(); black_box(results); } }); }, ); group.bench_with_input( BenchmarkId::new("case_insensitive_search", "local"), searcher, |b, searcher| { b.iter(|| { for path in &data.common_paths { let results: Vec<_> = searcher .search_files(black_box(Path::new(path)), true) .collect(); black_box(results); } }); }, ); } group.finish(); } fn bench_regex_searches(c: &mut Criterion) { let data = BenchmarkData::new(); let mut group = c.benchmark_group("regex_search"); group.measurement_time(Duration::from_secs(20)); let high_volume_patterns = &data.common_regexes[0..3]; // First 3 are high volume let medium_volume_patterns = &data.common_regexes[3..6]; // Next 3 are medium volume let low_volume_patterns = &data.common_regexes[6..9]; // Next 3 are low volume let _zero_match_patterns = &data.common_regexes[9..]; // Last ones have zero matches // Benchmark remote searcher if available if let Some(ref searcher) = data.remote_searcher { // High volume regex searches (thousands of results) group.bench_with_input( BenchmarkId::new("regex_high_volume", "remote"), searcher, |b, searcher| { b.iter(|| { for pattern in high_volume_patterns { let results: Vec<_> = searcher .search_files_regex(black_box(pattern), false) .collect(); black_box(results); } }); }, ); // Medium volume regex searches group.bench_with_input( BenchmarkId::new("regex_medium_volume", "remote"), searcher, |b, searcher| { b.iter(|| { for pattern in medium_volume_patterns { let results: Vec<_> = searcher .search_files_regex(black_box(pattern), false) .collect(); black_box(results); } }); }, ); // Low volume regex searches group.bench_with_input( BenchmarkId::new("regex_low_volume", "remote"), searcher, |b, searcher| { b.iter(|| { for pattern in low_volume_patterns { let results: Vec<_> = searcher .search_files_regex(black_box(pattern), false) .collect(); black_box(results); } }); }, ); } // Benchmark local searcher if available if let Some(ref searcher) = data.local_searcher { group.bench_with_input( BenchmarkId::new("regex_high_volume", "local"), searcher, |b, searcher| { b.iter(|| { for pattern in high_volume_patterns { let results: Vec<_> = searcher .search_files_regex(black_box(pattern), false) .collect(); black_box(results); } }); }, ); } group.finish(); } fn bench_repeated_queries(c: &mut Criterion) { let data = BenchmarkData::new(); let mut group = c.benchmark_group("repeated_queries"); group.measurement_time(Duration::from_secs(15)); // Benchmark remote searcher if available if let Some(ref searcher) = data.remote_searcher { // Benchmark repeated exact queries (should show caching benefits) group.bench_with_input( BenchmarkId::new("repeated_exact_queries", "remote"), searcher, |b, searcher| { let test_path = "/usr/bin/python3.11"; b.iter(|| { for _ in 0..100 { let results: Vec<_> = searcher .search_files(black_box(Path::new(test_path)), false) .collect(); black_box(results); } }); }, ); // Benchmark repeated regex queries (tests regex caching) group.bench_with_input( BenchmarkId::new("repeated_regex_queries", "remote"), searcher, |b, searcher| { let test_pattern = r"^/usr/bin/.*gcc.*"; b.iter(|| { for _ in 0..50 { let results: Vec<_> = searcher .search_files_regex(black_box(test_pattern), false) .collect(); black_box(results); } }); }, ); } // Benchmark local searcher if available if let Some(ref searcher) = data.local_searcher { group.bench_with_input( BenchmarkId::new("repeated_exact_queries", "local"), searcher, |b, searcher| { let test_path = "/usr/bin/python3.11"; b.iter(|| { for _ in 0..100 { let results: Vec<_> = searcher .search_files(black_box(Path::new(test_path)), false) .collect(); black_box(results); } }); }, ); group.bench_with_input( BenchmarkId::new("repeated_regex_queries", "local"), searcher, |b, searcher| { let test_pattern = r"^/usr/bin/.*gcc.*"; b.iter(|| { for _ in 0..50 { let results: Vec<_> = searcher .search_files_regex(black_box(test_pattern), false) .collect(); black_box(results); } }); }, ); } group.finish(); } criterion_group!( benches, bench_exact_path_searches, bench_regex_searches, bench_repeated_queries, ); criterion_main!(benches); ognibuild-0.2.6/disperse.toml000064400000000000000000000001011046102023000143100ustar 00000000000000tag-name = "v$VERSION" tarball-location = [] release-timeout = 5 ognibuild-0.2.6/examples/apt-file-search.rs000064400000000000000000000025211046102023000167350ustar 00000000000000use clap::Parser; use ognibuild::debian::file_search::{ get_apt_contents_file_searcher, get_packages_for_paths, FileSearcher, GENERATED_FILE_SEARCHER, }; use std::path::PathBuf; #[derive(Parser)] struct Args { #[clap(short, long)] /// Search for regex. regex: bool, /// Path to search for. path: Vec, #[clap(short, long)] /// Enable debug output. debug: bool, #[clap(short, long)] /// Case insensitive search. case_insensitive: bool, } pub fn main() -> Result<(), i8> { let args: Args = Args::parse(); env_logger::builder() .filter_level(if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }) .init(); let session = ognibuild::session::plain::PlainSession::new(); let main_searcher = get_apt_contents_file_searcher(&session).unwrap(); let searchers: Vec<&dyn FileSearcher> = vec![ main_searcher.as_ref(), &*GENERATED_FILE_SEARCHER as &dyn FileSearcher, ]; let packages = get_packages_for_paths( args.path .iter() .map(|x| x.as_path().to_str().unwrap()) .collect(), searchers.as_slice(), args.regex, args.case_insensitive, ); for package in packages { println!("{}", package); } Ok(()) } ognibuild-0.2.6/notes/debugging.md000064400000000000000000000023721046102023000152160ustar 00000000000000# Debugging Hopefully ognibuild just Does The Right Thing™, but sometimes it doesn't. Here are some tips for debugging. ## Detecting dependencies If you're trying to build a project and it's failing, it might be because ognibuild is missing a dependency. You can use ``ogni info`` to see what dependencies ognibuild thinks are missing. ## Log file parsing If a build fails, ognibuild will attempt to parse the log file with [buildlog-consultant](https://github.com/jelmer/buildlog-consultant) to try to find out how to fix the build. If you think it's not doing a good job, you can run buildlog-consultant manually on the log file, and then possibly file a bug against buildlog-consultant. ## Failure to build If onibuild fails to determine how to build a project, it will print out an error message. If you think it should be able to build the project, please file a bug. ## Reporting bugs If you think you've found a bug in ognibuild, please report it! You can do so on GitHub at https://github.com/jelmer/ognibuild/issues/new If ognibuild crashed, please include the backtrace with ``RUST_BACKTRACE=full`` set. If it is possible to reproduce the bug on a particular open source project, please include the URL of that project, and the exact command you ran. ognibuild-0.2.6/notes/design.md000064400000000000000000000041761046102023000145400ustar 00000000000000Ognibuild aims to build and extract build metadata from any software project. It does this through a variety of mechanisms: * Detecting know build systems * Ensuring necessary dependencies are present * Fixing other issues in the project A build system is anything that can create artefacts from source and/or test and install those artefacts. Some projects use multiple build systems which may or may not be tightly integrated. A build action is one of “clean”, “build”, “install” or “test”. DependencyCategory: Dependencies can be for different purposes: “build” (just necessary for building), “runtime” (to run after it has been built and possibly installed), “test” (for running tests - e.g. test frameworks, test runners), “dev” (necessary for development - e.g. listens, ide plugins, etc). When a build action is requested, ognibuild detects the build system(s) and then invokes the action. The action is run and if it failed the output is scanned for problems by buildlog-consultant. If a problem is found then the appropriate Fixer is invoked. This may take any of a number of steps, including changing the project source tree, configuring a tool locally for the user or installing more packages. If no appropriate Fixer can be found then no further action is taken. If the Fixer is successful then the original action is retried. When it comes to dependencies, there is usually only one relevant fixer loaded. Depending on the situation, this can either update the local project to reference the extra dependencies or install them on the system, invoking the appropriate Installer, Dependency fixers start by trying to derive the missing dependency from the problem that was found. Some second level dependency fixers may then try to coerce the dependency into a specific kind of dependency (e.g. a Debian dependency from a Python dependency). InstallationScope: Where dependencies are installed can vary from “user” (installed in the user’s home directory), “system” (installed globally on the system, usually in /usr), “vendor” (bundled with the project source code). Not all installers support all scopes. ognibuild-0.2.6/src/actions/build.rs000064400000000000000000000033701046102023000155040ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, Installer}; use crate::logs::{wrap, LogManager}; use crate::session::Session; /// Run the build process using the first applicable build system. /// /// This function attempts to build a package using the first build system in the provided list /// that is applicable for the current project. If the build fails, it will attempt to fix /// issues using the provided fixers. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to try /// * `installer` - Installer to use for installing dependencies /// * `fixers` - List of fixers to try if build fails /// * `log_manager` - Manager for logging build output /// /// # Returns /// * `Ok(())` if the build succeeds /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used /// * Other errors if the build fails and can't be fixed pub fn run_build( session: &dyn Session, buildsystems: &[&dyn BuildSystem], installer: &dyn Installer, fixers: &[&dyn BuildFixer], log_manager: &mut dyn LogManager, ) -> Result<(), Error> { // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; if let Some(buildsystem) = buildsystems.iter().next() { return Ok(iterate_with_build_fixers( fixers, || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.build(session, installer) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } ognibuild-0.2.6/src/actions/clean.rs000064400000000000000000000034021046102023000154630ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, Installer}; use crate::logs::{wrap, LogManager}; use crate::session::Session; /// Run the clean process using the first applicable build system. /// /// This function attempts to clean a project using the first build system in the provided list /// that is applicable for the current project. If the clean operation fails, it will attempt to fix /// issues using the provided fixers. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to try /// * `installer` - Installer to use for installing dependencies /// * `fixers` - List of fixers to try if clean fails /// * `log_manager` - Manager for logging clean output /// /// # Returns /// * `Ok(())` if the clean succeeds /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used /// * Other errors if the clean fails and can't be fixed pub fn run_clean( session: &dyn Session, buildsystems: &[&dyn BuildSystem], installer: &dyn Installer, fixers: &[&dyn BuildFixer], log_manager: &mut dyn LogManager, ) -> Result<(), Error> { // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; if let Some(buildsystem) = buildsystems.iter().next() { return Ok(iterate_with_build_fixers( fixers, || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.clean(session, installer) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } ognibuild-0.2.6/src/actions/dist.rs000064400000000000000000000041111046102023000153420ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, Installer}; use crate::logs::{wrap, LogManager}; use crate::session::Session; use std::ffi::OsString; use std::path::Path; /// Run the distribution package creation process using the first applicable build system. /// /// This function attempts to create a distribution package using the first build system in the /// provided list that is applicable for the current project. If the operation fails, it will /// attempt to fix issues using the provided fixers. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to try /// * `installer` - Installer to use for installing dependencies /// * `fixers` - List of fixers to try if dist creation fails /// * `target_directory` - Directory where distribution packages should be created /// * `quiet` - Whether to suppress output /// * `log_manager` - Manager for logging output /// /// # Returns /// * `Ok(OsString)` with the filename of the created package if successful /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used /// * Other errors if the dist creation fails and can't be fixed pub fn run_dist( session: &dyn Session, buildsystems: &[&dyn BuildSystem], installer: &dyn Installer, fixers: &[&dyn BuildFixer], target_directory: &Path, quiet: bool, log_manager: &mut dyn LogManager, ) -> Result { // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; if let Some(buildsystem) = buildsystems.iter().next() { return Ok(iterate_with_build_fixers( fixers, || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.dist(session, installer, target_directory, quiet) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } ognibuild-0.2.6/src/actions/info.rs000064400000000000000000000045251046102023000153430ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::fix_build::BuildFixer; use crate::installer::Error as InstallerError; use crate::session::Session; use std::collections::HashMap; /// Display information about detected build systems and their dependencies/outputs. /// /// This function logs information about each detected build system, including its /// declared dependencies and outputs. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to get information from /// * `fixers` - Optional list of fixers to use if getting information fails /// /// # Returns /// * `Ok(())` if information was successfully retrieved and displayed /// * Errors are logged but not returned, so this function will generally succeed pub fn run_info( session: &dyn Session, buildsystems: &[&dyn BuildSystem], fixers: Option<&[&dyn BuildFixer]>, ) -> Result<(), Error> { for buildsystem in buildsystems { log::info!("{:?}", buildsystem); let mut deps = HashMap::new(); match buildsystem.get_declared_dependencies(session, fixers) { Ok(declared_deps) => { for (category, dep) in declared_deps { deps.entry(category).or_insert_with(Vec::new).push(dep); } } Err(e) => { log::error!( "Failed to get declared dependencies from {:?}: {}", buildsystem, e ); } } if !deps.is_empty() { log::info!(" Declared dependencies:"); for (category, deps) in deps { for dep in deps { log::info!(" {}: {:?}", category, dep); } } } let outputs = match buildsystem.get_declared_outputs(session, fixers) { Ok(outputs) => outputs, Err(e) => { log::error!( "Failed to get declared outputs from {:?}: {}", buildsystem, e ); continue; } }; if !outputs.is_empty() { log::info!(" Outputs:"); for output in outputs { log::info!(" {:?}", output); } } } Ok(()) } ognibuild-0.2.6/src/actions/install.rs000064400000000000000000000041611046102023000160520ustar 00000000000000use crate::buildsystem::{BuildSystem, Error, InstallTarget}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, InstallationScope, Installer}; use crate::logs::{wrap, LogManager}; use crate::session::Session; use std::path::Path; /// Run the installation process using the first applicable build system. /// /// This function attempts to install a package using the first build system in the provided list /// that is applicable for the current project. If the installation fails, it will attempt to fix /// issues using the provided fixers. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to try /// * `installer` - Installer to use for installing dependencies /// * `fixers` - List of fixers to try if installation fails /// * `log_manager` - Manager for logging installation output /// * `scope` - Installation scope (user or system) /// * `prefix` - Optional installation prefix path /// /// # Returns /// * `Ok(())` if the installation succeeds /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used /// * Other errors if the installation fails and can't be fixed pub fn run_install( session: &dyn Session, buildsystems: &[&dyn BuildSystem], installer: &dyn Installer, fixers: &[&dyn BuildFixer], log_manager: &mut dyn LogManager, scope: InstallationScope, prefix: Option<&Path>, ) -> Result<(), Error> { // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; let target = InstallTarget { scope, prefix: prefix.map(|p| p.to_path_buf()), }; if let Some(buildsystem) = buildsystems.iter().next() { return Ok(iterate_with_build_fixers( fixers, || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.install(session, installer, &target) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } ognibuild-0.2.6/src/actions/mod.rs000064400000000000000000000005031046102023000151570ustar 00000000000000/// Build action implementation. pub mod build; /// Clean action implementation. pub mod clean; /// Distribution creation action implementation. pub mod dist; /// Information display action implementation. pub mod info; /// Installation action implementation. pub mod install; /// Test action implementation. pub mod test; ognibuild-0.2.6/src/actions/test.rs000064400000000000000000000033371046102023000153670ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, Installer}; use crate::logs::{wrap, LogManager}; use crate::session::Session; /// Run tests using the first applicable build system. /// /// This function attempts to run tests using the first build system in the provided list /// that is applicable for the current project. If the tests fail, it will attempt to fix /// issues using the provided fixers. /// /// # Arguments /// * `session` - The session to run commands in /// * `buildsystems` - List of build systems to try /// * `installer` - Installer to use for installing dependencies /// * `fixers` - List of fixers to try if tests fail /// * `log_manager` - Manager for logging test output /// /// # Returns /// * `Ok(())` if the tests succeed /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used /// * Other errors if the tests fail and can't be fixed pub fn run_test( session: &dyn Session, buildsystems: &[&dyn BuildSystem], installer: &dyn Installer, fixers: &[&dyn BuildFixer], log_manager: &mut dyn LogManager, ) -> Result<(), Error> { // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; if let Some(buildsystem) = buildsystems.iter().next() { return Ok(iterate_with_build_fixers( fixers, || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.test(session, installer) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } ognibuild-0.2.6/src/analyze.rs000064400000000000000000000276151046102023000144200ustar 00000000000000use crate::session::{run_with_tee, Error as SessionError, Session}; use buildlog_consultant::problems::common::MissingCommand; fn default_check_success(status: std::process::ExitStatus, _lines: Vec<&str>) -> bool { status.success() } #[derive(Debug)] /// Error type for analyzed command execution errors. /// /// This enum represents different kinds of errors that can occur when running /// and analyzing commands, with details about the specific error. pub enum AnalyzedError { /// Error indicating a command was not found. MissingCommandError { /// The name of the command that was not found. command: String, }, /// Error from an IO operation. IoError(std::io::Error), /// Detailed error with information from the buildlog consultant. Detailed { /// The return code of the failed command. retcode: i32, /// The specific build problem identified. error: Box, }, /// Error that could not be specifically identified. Unidentified { /// The return code of the failed command. retcode: i32, /// The output lines from the command. lines: Vec, /// Optional secondary information about the error. secondary: Option>, }, } impl From for AnalyzedError { fn from(e: std::io::Error) -> Self { #[cfg(unix)] match e.raw_os_error() { Some(libc::ENOSPC) => { return AnalyzedError::Detailed { retcode: 1, error: Box::new(buildlog_consultant::problems::common::NoSpaceOnDevice), }; } Some(libc::EMFILE) => { return AnalyzedError::Detailed { retcode: 1, error: Box::new(buildlog_consultant::problems::common::TooManyOpenFiles), } } _ => {} } AnalyzedError::IoError(e) } } impl std::fmt::Display for AnalyzedError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AnalyzedError::MissingCommandError { command } => { write!(f, "Command not found: {}", command) } AnalyzedError::IoError(e) => write!(f, "IO error: {}", e), AnalyzedError::Detailed { retcode, error } => { write!(f, "Command failed with code {}", retcode)?; write!(f, "\n{}", error) } AnalyzedError::Unidentified { retcode, lines, secondary, } => { write!(f, "Command failed with code {}", retcode)?; if let Some(secondary) = secondary { write!(f, "\n{}", secondary) } else { write!(f, "\n{}", lines.join("\n")) } } } } } impl std::error::Error for AnalyzedError {} #[cfg(test)] mod tests { use super::*; use crate::session::plain::PlainSession; use std::process::ExitStatus; use tempfile::TempDir; #[test] fn test_analyzed_error_display_missing_command() { let error = AnalyzedError::MissingCommandError { command: "nonexistent".to_string(), }; assert_eq!(error.to_string(), "Command not found: nonexistent"); } #[test] fn test_analyzed_error_display_io_error() { let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied"); let error = AnalyzedError::IoError(io_error); assert_eq!(error.to_string(), "IO error: Access denied"); } #[test] fn test_analyzed_error_display_detailed() { let problem = Box::new(buildlog_consultant::problems::common::MissingCommand( "test".to_string(), )); let error = AnalyzedError::Detailed { retcode: 127, error: problem, }; let display = error.to_string(); assert!(display.starts_with("Command failed with code 127")); assert!(display.contains("test")); } #[test] fn test_analyzed_error_display_unidentified_with_secondary() { let error = AnalyzedError::Unidentified { retcode: 1, lines: vec!["line1".to_string(), "line2".to_string()], secondary: None, // Skip the secondary match for now due to API complexity }; let display = error.to_string(); assert!(display.starts_with("Command failed with code 1")); assert!(display.contains("line1\nline2")); } #[test] fn test_analyzed_error_display_unidentified_without_secondary() { let error = AnalyzedError::Unidentified { retcode: 1, lines: vec!["line1".to_string(), "line2".to_string()], secondary: None, }; let display = error.to_string(); assert!(display.starts_with("Command failed with code 1")); assert!(display.contains("line1\nline2")); } #[test] fn test_analyzed_error_from_io_error() { let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); let analyzed_error: AnalyzedError = io_error.into(); match analyzed_error { AnalyzedError::IoError(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound), _ => panic!("Expected IoError variant"), } } #[cfg(unix)] #[test] fn test_analyzed_error_from_no_space_error() { let io_error = std::io::Error::new(std::io::ErrorKind::Other, "No space left"); // Unfortunately we can't easily create an io::Error with a specific raw_os_error // in a portable way, so this test is limited let analyzed_error: AnalyzedError = io_error.into(); match analyzed_error { AnalyzedError::IoError(_) => {} _ => panic!("Expected IoError variant for non-specific error"), } } #[test] fn test_run_detecting_problems_success() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let result = run_detecting_problems( &session, vec!["echo", "hello"], None, true, None, None, None, None, ); assert!(result.is_ok()); let lines = result.unwrap(); assert_eq!(lines, vec!["hello"]); } #[test] fn test_run_detecting_problems_with_custom_check_success() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let custom_check = |_status: ExitStatus, lines: Vec<&str>| -> bool { lines.iter().any(|line| line.contains("hello")) }; let result = run_detecting_problems( &session, vec!["echo", "hello"], Some(&custom_check), true, None, None, None, None, ); assert!(result.is_ok()); } #[test] fn test_run_detecting_problems_nonexistent_command() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let result = run_detecting_problems( &session, vec!["nonexistent_command_12345"], None, true, None, None, None, None, ); assert!(result.is_err()); match result.unwrap_err() { AnalyzedError::Detailed { retcode, error } => { assert_eq!(retcode, 127); assert_eq!( error.to_string(), "Missing command: nonexistent_command_12345" ); } _ => panic!("Expected Detailed error for nonexistent command"), } } #[test] fn test_run_detecting_problems_failing_command() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let result = run_detecting_problems( &session, vec!["false"], // Command that always fails None, true, None, None, None, None, ); assert!(result.is_err()); match result.unwrap_err() { AnalyzedError::Unidentified { retcode, .. } => { assert_eq!(retcode, 1); } _ => panic!("Expected Unidentified error for failing command"), } } #[test] fn test_default_check_success_with_success() { // Create a successful exit status (0) let output = std::process::Command::new("true").output().unwrap(); let result = default_check_success(output.status, vec![]); assert!(result); } #[test] fn test_default_check_success_with_failure() { // Create a failed exit status (non-zero) let output = std::process::Command::new("false").output().unwrap(); let result = default_check_success(output.status, vec![]); assert!(!result); } } /// Run a command and analyze the output for common build errors. /// /// # Arguments /// * `session`: Session to run the command in /// * `args`: Arguments to the command /// * `check_success`: Function to determine if the command was successful /// * `quiet`: Whether to log the command being run /// * `cwd`: Current working directory for the command /// * `user`: User to run the command as /// * `env`: Environment variables to set for the command /// * `stdin`: Stdin for the command pub fn run_detecting_problems( session: &dyn Session, args: Vec<&str>, check_success: Option<&dyn Fn(std::process::ExitStatus, Vec<&str>) -> bool>, quiet: bool, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option<&std::collections::HashMap>, stdin: Option, ) -> Result, AnalyzedError> { let check_success = check_success.unwrap_or(&default_check_success); let (retcode, contents) = match run_with_tee(session, args.clone(), cwd, user, env, stdin, quiet) { Ok((retcode, contents)) => (retcode, contents), Err(SessionError::SetupFailure(..)) => unreachable!(), Err(SessionError::ImageError(..)) => unreachable!(), Err(SessionError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => { let command = args[0].to_string(); return Err(AnalyzedError::Detailed { retcode: 127, error: Box::new(MissingCommand(command)) as Box, }); } Err(SessionError::IoError(e)) => { return Err(AnalyzedError::IoError(e)); } Err(SessionError::CalledProcessError(retcode)) => (retcode, vec![]), }; log::debug!( "Command returned code {}, with {} lines of output.", retcode.code().unwrap_or(1), contents.len() ); if check_success(retcode, contents.iter().map(|s| s.as_str()).collect()) { return Ok(contents); } let (r#match, error) = buildlog_consultant::common::find_build_failure_description( contents.iter().map(|x| x.as_str()).collect(), ); if let Some(error) = error { log::debug!("Identified error: {}", error); Err(AnalyzedError::Detailed { retcode: retcode.code().unwrap_or(1), error, }) } else { if let Some(r#match) = r#match.as_ref() { log::warn!("Build failed with unidentified error:"); log::warn!("{}", r#match.line().trim_end_matches('\n')); } else { log::warn!("Build failed without error being identified."); } Err(AnalyzedError::Unidentified { retcode: retcode.code().unwrap_or(1), lines: contents, secondary: r#match, }) } } ognibuild-0.2.6/src/bin/deb-fix-build.rs000064400000000000000000000144071046102023000161330ustar 00000000000000use clap::Parser; use ognibuild::debian::fix_build::{rescue_build_log, IterateBuildError}; use ognibuild::session::plain::PlainSession; #[cfg(target_os = "linux")] use ognibuild::session::schroot::SchrootSession; use ognibuild::session::Session; use std::fmt::Write as _; use std::io::Write as _; use std::path::PathBuf; #[derive(Parser)] struct Args { /// Suffix to use for test builds #[clap(short, long, default_value = "fixbuild1")] suffix: String, /// Suite to target #[clap(short, long, default_value = "unstable")] suite: String, /// Committer string (name and email) #[clap(short, long)] committer: Option, /// Document changes in the changelog [default: auto-detect] #[arg(long, default_value_t = false, conflicts_with = "no_update_changelog")] update_changelog: bool, /// Do not document changes in the changelog (useful when using e.g. "gbp dch") [default: auto-detect] #[arg(long, default_value_t = false, conflicts_with = "update_changelog")] no_update_changelog: bool, /// Output directory. #[clap(short, long)] output_directory: Option, /// Build command #[clap(short, long, default_value = "sbuild -A -s -v")] build_command: String, /// Maximum number of issues to attempt to fix before giving up. #[clap(short, long, default_value = "10")] max_iterations: usize, #[cfg(target_os = "linux")] /// chroot to use. #[clap(short, long)] schroot: Option, /// ognibuild dep server to use #[clap(short, long, env = "OGNIBUILD_DEPS")] dep_server_url: Option, /// Be verbose #[clap(short, long)] verbose: bool, /// Directory to use #[clap(short, long, default_value = ".")] directory: PathBuf, } fn main() -> Result<(), i32> { let args = Args::parse(); let update_changelog: Option = if args.update_changelog { Some(true) } else if args.no_update_changelog { Some(false) } else { None }; env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.verbose { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); let temp_output_dir; let output_directory = if let Some(output_directory) = &args.output_directory { if !output_directory.is_dir() { log::error!("output directory {:?} is not a directory", output_directory); std::process::exit(1); } output_directory.clone() } else { temp_output_dir = Some(tempfile::tempdir().unwrap()); log::info!("Using output directory {:?}", temp_output_dir); temp_output_dir.as_ref().unwrap().path().to_path_buf() }; let (tree, subpath) = breezyshim::workingtree::open_containing(&args.directory).unwrap(); #[cfg(target_os = "linux")] let session = if let Some(schroot) = &args.schroot { Box::new(SchrootSession::new(schroot, Some("deb-fix-build")).unwrap()) as Box } else { Box::new(PlainSession::new()) }; #[cfg(not(target_os = "linux"))] let session = Box::new(PlainSession::new()); let apt = ognibuild::debian::apt::AptManager::new(session.as_ref(), None); let committer = args .committer .as_ref() .map(|committer| breezyshim::config::parse_username(committer)); let packaging_context = ognibuild::debian::context::DebianPackagingContext::new( tree.clone(), &subpath, committer, args.update_changelog, Some(Box::new(breezyshim::commit::NullCommitReporter::new())), ); let fixers = ognibuild::debian::fixers::default_fixers(&packaging_context, &apt); match ognibuild::debian::fix_build::build_incrementally( &tree, Some(&args.suffix), Some(&args.suite), &output_directory, &args.build_command, fixers .iter() .map(|f| f.as_ref()) .collect::>() .as_slice(), None, Some(args.max_iterations), &subpath, None, None, None, None, update_changelog == Some(false), ) { Ok(build_result) => { log::info!( "Built {} - changes file at {:?}.", build_result.version, build_result.changes_names, ); Ok(()) } Err(IterateBuildError::Persistent(phase, error)) => { log::error!("Error during {}: {}", phase, error); if let Some(output_directory) = args.output_directory { rescue_build_log(&output_directory, Some(&tree)).unwrap(); } Err(1) } Err(IterateBuildError::Unidentified { phase, lines, secondary, .. }) => { let mut header = if let Some(phase) = phase { format!("Error during {}:", phase) } else { "Error:".to_string() }; if let Some(m) = secondary { let linenos = m.linenos(); write!( header, " on lines {}-{}", linenos[0], linenos[linenos.len() - 1] ) .unwrap(); } header.write_str(":").unwrap(); log::error!("{}", header); for line in lines { log::error!(" {}", line); } if let Some(output_directory) = args.output_directory { rescue_build_log(&output_directory, Some(&tree)).unwrap(); } Err(1) } Err(IterateBuildError::FixerLimitReached(n)) => { log::error!("Fixer limit reached - {} attempts.", n); Err(1) } Err(IterateBuildError::Other(o)) => { log::error!("Error: {}", o); Err(1) } Err(IterateBuildError::MissingPhase) => { log::error!("Missing phase"); Err(1) } Err(IterateBuildError::ResetTree(e)) => { log::error!("Error resetting tree: {}", e); Err(1) } } } ognibuild-0.2.6/src/bin/deb-upstream-deps.rs000064400000000000000000000063731046102023000170440ustar 00000000000000use breezyshim::error::Error as BrzError; use breezyshim::workingtree::{self, WorkingTree}; use clap::Parser; use debian_analyzer::editor::{Editor, MutableTreeEdit}; use debian_control::lossless::relations::Relations; use ognibuild::session::Session; use std::io::Write; use std::path::PathBuf; #[derive(Parser)] struct Args { #[clap(short, long)] /// Be verbose debug: bool, #[clap(short, long)] /// Update current package update: bool, #[clap(short, long, default_value = ".")] /// Directory to run in directory: PathBuf, } fn main() -> Result<(), i8> { let args = Args::parse(); env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); let (wt, subpath) = match workingtree::open_containing(&args.directory) { Ok((wt, subpath)) => (wt, subpath), Err(e @ BrzError::NotBranchError { .. }) => { log::error!("please run deps in an existing branch: {}", e); return Err(1); } Err(e) => { log::error!("error opening working tree: {}", e); return Err(1); } }; let mut build_deps = vec![]; let mut test_deps = vec![]; let mut session: Box = Box::new(ognibuild::session::plain::PlainSession::new()); let project = session.project_from_vcs(&wt, None, None).unwrap(); for (bs_subpath, bs) in ognibuild::buildsystem::scan_buildsystems(&wt.abspath(&subpath).unwrap()) { session .chdir(&project.internal_path().join(&bs_subpath)) .unwrap(); let (bs_build_deps, bs_test_deps) = ognibuild::debian::upstream_deps::get_project_wide_deps(session.as_ref(), bs.as_ref()); build_deps.extend(bs_build_deps); test_deps.extend(bs_test_deps); } if !build_deps.is_empty() { println!( "Build-Depends: {}", build_deps .iter() .map(|x| x.relation_string()) .collect::>() .join(", ") ); } if !test_deps.is_empty() { println!( "Test-Depends: {}", test_deps .iter() .map(|x| x.relation_string()) .collect::>() .join(", ") ); } if args.update { let edit = wt .edit_file::(&subpath.join("debian/control"), true, true) .unwrap(); let mut source = edit.source().unwrap(); for build_dep in build_deps { for entry in build_dep.iter() { let mut relations = source.build_depends().unwrap_or_else(Relations::new); let old_str = relations.to_string(); debian_analyzer::relations::ensure_relation(&mut relations, entry); if old_str != relations.to_string() { log::info!("Bumped to {}", relations); source.set_build_depends(&relations); } } } edit.commit().unwrap(); } Ok(()) } ognibuild-0.2.6/src/bin/dep-server.rs000064400000000000000000000033101046102023000155630ustar 00000000000000use axum::{routing::get, Router}; use clap::Parser; use ognibuild::debian::apt::AptManager; #[cfg(target_os = "linux")] use ognibuild::session::schroot::SchrootSession; use ognibuild::session::{plain::PlainSession, Session}; use std::io::Write; #[derive(Parser)] struct Args { #[clap(short, long)] listen_address: String, #[clap(short, long)] port: u16, #[cfg(target_os = "linux")] #[clap(short, long)] schroot: Option, #[clap(short, long)] debug: bool, } #[tokio::main] async fn main() -> Result<(), i8> { let args = Args::parse(); env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); #[cfg(target_os = "linux")] let session: Box = if let Some(schroot) = args.schroot { Box::new(SchrootSession::new(&schroot, None).unwrap()) } else { Box::new(PlainSession::new()) }; #[cfg(not(target_os = "linux"))] let session: Box = Box::new(PlainSession::new()); let _apt_mgr = AptManager::from_session(session.as_ref()); let app = Router::new() .route("/health", get(|| async { "ok" })) .route("/version", get(|| async { env!("CARGO_PKG_VERSION") })) .route("/ready", get(|| async { "ok" })); let listener = tokio::net::TcpListener::bind((args.listen_address.as_str(), args.port)) .await .unwrap(); log::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); Ok(()) } ognibuild-0.2.6/src/bin/ogni.rs000064400000000000000000000553541046102023000144620ustar 00000000000000use clap::{Parser, Subcommand}; use ognibuild::analyze::AnalyzedError; use ognibuild::buildsystem::{ detect_buildsystems, supported_buildsystem_names, BuildSystem, DependencyCategory, Error, }; use ognibuild::dependency::Dependency; use ognibuild::fix_build::BuildFixer; use ognibuild::installer::{ auto_installer, select_installers, Error as InstallerError, Explanation, InstallationScope, Installer, }; use ognibuild::session::Session; use std::io::Write; use std::path::Path; use std::path::PathBuf; use url::Url; /// Check if network access is disabled via the OGNIBUILD_DISABLE_NET environment variable. /// /// Network access is disabled if OGNIBUILD_DISABLE_NET is set to "1", "true", "yes", or "on" (case-insensitive). /// /// # Arguments /// * `env_getter` - Function to get environment variable value /// /// # Returns /// `true` if network access should be disabled, `false` otherwise fn is_network_disabled_with(env_getter: F) -> bool where F: Fn(&str) -> Option, { match env_getter("OGNIBUILD_DISABLE_NET") { Some(val) => { let val = val.to_lowercase(); val == "1" || val == "true" || val == "yes" || val == "on" } None => false, } } /// Check if network access is disabled via the OGNIBUILD_DISABLE_NET environment variable. /// /// Network access is disabled if OGNIBUILD_DISABLE_NET is set to "1", "true", "yes", or "on" (case-insensitive). /// /// # Returns /// `true` if network access should be disabled, `false` otherwise fn is_network_disabled() -> bool { is_network_disabled_with(|key| std::env::var(key).ok()) } #[derive(Parser)] struct ExecArgs { #[clap(name = "subargv", trailing_var_arg = true)] subargv: Vec, } #[derive(Parser)] struct InstallArgs { #[clap(long)] prefix: Option, } #[derive(Parser)] struct CacheEnvArgs { /// Debian suite to cache (e.g., "sid", "bookworm", "stable") #[clap(default_value = "sid")] suite: String, /// Force re-download even if cached #[clap(long)] force: bool, } #[derive(Subcommand)] enum Command { #[clap(name = "dist")] /// Create a distribution package/tarball Dist, #[clap(name = "build")] /// Build the project Build, #[clap(name = "clean")] /// Clean build artifacts Clean, #[clap(name = "test")] /// Run tests Test, #[clap(name = "info")] /// Display build system information and dependencies Info, #[clap(name = "verify")] /// Build and run tests Verify, #[clap(name = "exec")] /// Execute a command with automatic dependency resolution Exec(ExecArgs), #[clap(name = "install")] /// Install the project Install(InstallArgs), #[clap(name = "cache-env")] /// Cache a Debian cloud image for use with UnshareSession CacheEnv(CacheEnvArgs), } #[derive(Parser)] struct Args { #[clap(subcommand)] command: Option, #[clap(long, short, default_value = ".")] directory: String, #[cfg(target_os = "linux")] #[clap(long)] schroot: Option, #[clap(long, short, default_value = "auto", use_value_delimiter = true)] installer: Vec, #[clap(long, hide = true)] apt: bool, #[clap(long, hide = true)] native: bool, #[clap(long)] /// Explain what needs to be done rather than making changes explain: bool, #[clap(long)] /// Ignore declared dependencies, follow build errors only ignore_declared_dependencies: bool, #[clap(long)] /// Scope to install in installation_scope: Option, #[clap(long, env = "OGNIBUILD_DEPS")] /// ognibuild dep server to use dep_server_url: Option, #[clap(long)] /// Print more verbose output debug: bool, #[clap(long)] /// List all supported build systems supported_buildsystems: bool, } fn explain_missing_deps( session: &dyn Session, installer: &dyn Installer, scope: InstallationScope, deps: &[&dyn Dependency], ) -> Result, Error> { if deps.is_empty() { return Ok(vec![]); } let missing = deps .iter() .filter(|dep| !dep.present(session)) .collect::>(); if missing.is_empty() { return Ok(vec![]); } Ok(missing .into_iter() .map(|dep| installer.explain(*dep, scope)) .collect::>()?) } fn explain_necessary_declared_dependencies( session: &dyn Session, installer: &dyn Installer, fixers: &[&dyn BuildFixer], buildsystems: &[&dyn BuildSystem], categories: &[DependencyCategory], scope: InstallationScope, ) -> Result, Error> { let mut relevant: Vec> = vec![]; for buildsystem in buildsystems { let declared_deps = buildsystem.get_declared_dependencies(session, Some(fixers))?; for (category, dep) in declared_deps { if categories.contains(&category) { relevant.push(dep); } } } explain_missing_deps( session, installer, scope, relevant .iter() .map(|d| d.as_ref()) .collect::>() .as_slice(), ) } fn install_necessary_declared_dependencies( session: &dyn Session, installer: &dyn Installer, scopes: &[InstallationScope], fixers: &[&dyn BuildFixer], buildsystems: &[&dyn BuildSystem], categories: &[DependencyCategory], ) -> Result<(), Error> { for buildsystem in buildsystems { buildsystem.install_declared_dependencies( categories, scopes, session, installer, Some(fixers), )?; } Ok(()) } fn run_action( session: &dyn Session, scope: InstallationScope, external_dir: &Path, installer: &dyn Installer, fixers: &[&dyn BuildFixer], args: &Args, ) -> Result<(), Error> { if let Some(Command::Exec(ExecArgs { subargv })) = &args.command { ognibuild::fix_build::run_fixing_problems::<_, Error>( fixers, None, session, subargv .iter() .map(|s| s.as_str()) .collect::>() .as_slice(), false, None, None, None, )?; return Ok(()); } let mut log_manager = ognibuild::logs::NoLogManager; let bss = detect_buildsystems(external_dir); if !args.ignore_declared_dependencies { let categories = match args.command.as_ref().unwrap() { Command::Dist => vec![], Command::Build => vec![DependencyCategory::Universal, DependencyCategory::Build], Command::Clean => vec![], Command::Install(_) => vec![DependencyCategory::Universal, DependencyCategory::Build], Command::Test => vec![ DependencyCategory::Universal, DependencyCategory::Build, DependencyCategory::Test, ], Command::Info => vec![], Command::Verify => vec![ DependencyCategory::Universal, DependencyCategory::Build, DependencyCategory::Test, ], Command::Exec(_) => vec![], Command::CacheEnv(_) => return Ok(()), // No dependencies needed }; if !categories.is_empty() { log::info!("Checking that declared dependencies are present"); if !args.explain { match install_necessary_declared_dependencies( session, installer, &[scope, InstallationScope::Vendor], fixers, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), &categories, ) { Ok(_) => {} Err(e) => { log::info!("Unable to install declared dependencies: {}", e); return Err(e); } } } else { match explain_necessary_declared_dependencies( session, installer, fixers, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), &categories, scope, ) { Ok(explanations) => { for explanation in explanations { log::info!("{}", explanation); } } Err(e) => { log::info!("Unable to explain declared dependencies",); return Err(e); } } } } } match args.command.as_ref().unwrap() { Command::Exec(..) => unreachable!(), Command::CacheEnv(..) => unreachable!(), Command::Dist => { ognibuild::actions::dist::run_dist( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, Path::new("."), false, &mut log_manager, )?; } Command::Build => { ognibuild::actions::build::run_build( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, )?; } Command::Clean => { ognibuild::actions::clean::run_clean( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, )?; } Command::Install(install_args) => { ognibuild::actions::install::run_install( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, scope, install_args.prefix.as_deref(), )?; } Command::Test => { ognibuild::actions::test::run_test( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, )?; } Command::Info => { ognibuild::actions::info::run_info( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), Some(fixers), )?; } Command::Verify => { ognibuild::actions::build::run_build( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, )?; ognibuild::actions::test::run_test( session, bss.iter() .map(|bs| bs.as_ref()) .collect::>() .as_slice(), installer, fixers, &mut log_manager, )?; } } Ok(()) } fn main() -> Result<(), i32> { let mut args = Args::parse(); if args.supported_buildsystems { for bs in supported_buildsystem_names() { println!("{}", bs); } return Ok(()); } // Check if command is provided let command = match args.command { Some(cmd) => cmd, None => { eprintln!("Error: No command provided"); return Err(1); } }; args.command = Some(command); env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); // Handle cache-env command separately as it doesn't need a session if let Some(Command::CacheEnv(ref cache_args)) = args.command { return cache_debian_image(&cache_args.suite, cache_args.force); } #[cfg(target_os = "linux")] let mut session: Box = if let Some(schroot) = args.schroot.as_ref() { Box::new(ognibuild::session::schroot::SchrootSession::new(schroot, None).unwrap()) } else { Box::new(ognibuild::session::plain::PlainSession::new()) }; #[cfg(not(target_os = "linux"))] let mut session: Box = Box::new(ognibuild::session::plain::PlainSession::new()); let url = if let Ok(url) = args.directory.parse::() { url } else { let p = Path::new(&args.directory); url::Url::from_directory_path(p.canonicalize().unwrap()).unwrap() }; let mut td: Option = None; // TODO(jelmer): Get a list of supported schemes from breezy? #[cfg(feature = "breezy")] let project = if ["git", "http", "https", "ssh"].contains(&url.scheme()) { let b = breezyshim::branch::open(&url).unwrap(); log::info!("Cloning {}", args.directory); td = Some(tempfile::tempdir().unwrap()); let to_dir = b .controldir() .sprout( Url::from_directory_path(td.as_ref().unwrap().path()).unwrap(), None, Some(true), None, None, ) .unwrap(); let wt = to_dir.open_workingtree().unwrap(); session.project_from_vcs(&wt, None, None).unwrap() } else { let directory = if url.scheme() == "file" { Path::new(url.path()).to_path_buf() } else { PathBuf::from(args.directory.clone()) }; log::info!("Preparing directory {}", directory.display()); session.project_from_directory(&directory, None).unwrap() }; #[cfg(not(feature = "breezy"))] let project = { let directory = PathBuf::from(args.directory.clone()); log::info!("Preparing directory {}", directory.display()); session.project_from_directory(&directory, None).unwrap() }; session.chdir(project.internal_path()).unwrap(); std::env::set_current_dir(project.external_path()).unwrap(); if !session.is_temporary() && matches!(args.command, Some(Command::Info)) { args.explain = true; } if args.apt { args.installer = vec!["apt".to_string()]; } if args.native { args.installer = vec!["native".to_string()]; } let scope = if let Some(scope) = args.installation_scope { scope } else if args.explain { InstallationScope::Global } else if args.installer.contains(&"apt".to_string()) { InstallationScope::Global } else { ognibuild::installer::auto_installation_scope(session.as_ref()) }; let installer: Box = if args.installer == ["auto"] { auto_installer(session.as_ref(), scope, args.dep_server_url.as_ref()) } else { select_installers( session.as_ref(), args.installer .iter() .map(|x| x.as_str()) .collect::>() .as_slice(), args.dep_server_url.as_ref(), ) .unwrap() }; let fixers: Vec>> = if !args.explain { vec![Box::new(ognibuild::fixers::InstallFixer::new( installer.as_ref(), scope, ))] } else { vec![] }; match run_action( session.as_ref(), scope, project.external_path(), installer.as_ref(), fixers .iter() .map(|f| f.as_ref()) .collect::>() .as_slice(), &args, ) { Ok(_) => {} Err(Error::NoBuildSystemDetected) => { log::info!("No build tools found."); return Err(1); } Err(Error::DependencyInstallError(e)) => { log::info!("Dependency installation failed: {}", e); return Err(1); } Err(Error::Unimplemented) => { log::info!("This command is not yet implemented."); return Err(1); } Err(Error::Error(AnalyzedError::Unidentified { .. })) => { log::info!( "If there is a clear indication of a problem in the build log, please consider filing a request to update the patterns in buildlog-consultant at https://github.com/jelmer/buildlog-consultant/issues/new"); return Err(1); } Err(Error::Error(AnalyzedError::Detailed { error, .. })) => { log::info!("Detailed error: {}", error); log::info!( "Please consider filing a bug report at https://github.com/jelmer/ognibuild/issues/new" ); } Err(e) => { log::info!("Error: {}", e); return Err(1); } } std::mem::drop(td); Ok(()) } #[cfg(target_os = "linux")] fn cache_debian_image(suite: &str, force: bool) -> Result<(), i32> { if is_network_disabled() { eprintln!("Error: Network access is disabled (OGNIBUILD_DISABLE_NET is set)"); eprintln!("Cannot download Debian image without network access."); return Err(1); } let arch = std::env::consts::ARCH; let arch_name = match arch { "x86_64" => "amd64", "aarch64" => "arm64", _ => { eprintln!("Unsupported architecture: {}", arch); return Err(1); } }; let cache_dir = match dirs::cache_dir() { Some(dir) => dir.join("ognibuild").join("images"), None => { eprintln!("Cannot determine cache directory"); return Err(1); } }; if let Err(e) = std::fs::create_dir_all(&cache_dir) { eprintln!("Failed to create cache directory: {}", e); return Err(1); } let tarball_name = format!("debian-{}-{}.tar.gz", suite, arch_name); let tarball_path = cache_dir.join(&tarball_name); if tarball_path.exists() && !force { log::info!( "Debian {} image already cached at {}", suite, tarball_path.display() ); log::info!("Use --force to re-download."); return Ok(()); } // Bootstrap a Debian session using mmdebstrap and save it log::info!("Bootstrapping Debian {} image using mmdebstrap...", suite); match ognibuild::session::unshare::bootstrap_debian_tarball(suite, true) { Ok(session) => { // Save the bootstrapped session to the cache log::info!("Saving to cache: {}", tarball_path.display()); match session.save_to_tarball(&tarball_path) { Ok(_) => { log::info!( "Successfully cached Debian {} image at {}", suite, tarball_path.display() ); log::info!(""); log::info!("This cached image will now be used automatically by tests."); log::info!("You can also explicitly use it by setting:"); log::info!(" OGNIBUILD_DEBIAN_TEST_TARBALL={}", tarball_path.display()); Ok(()) } Err(e) => { eprintln!("Failed to save tarball: {}", e); Err(1) } } } Err(e) => { eprintln!("Failed to bootstrap image: {}", e); Err(1) } } } #[cfg(not(target_os = "linux"))] fn cache_debian_image(_suite: &str, _force: bool) -> Result<(), i32> { eprintln!("Error: cache-env command is only available on Linux"); Err(1) } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_network_disabled_not_set() { use std::collections::HashMap; let env: HashMap = HashMap::new(); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); } #[test] fn test_is_network_disabled_true() { use std::collections::HashMap; let mut env = HashMap::new(); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "1".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "true".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "TRUE".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "yes".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "YES".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "on".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "ON".to_string()); assert!(is_network_disabled_with(|key| env.get(key).cloned())); } #[test] fn test_is_network_disabled_false() { use std::collections::HashMap; let mut env = HashMap::new(); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "0".to_string()); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "false".to_string()); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "no".to_string()); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "off".to_string()); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); env.insert("OGNIBUILD_DISABLE_NET".to_string(), "random".to_string()); assert!(!is_network_disabled_with(|key| env.get(key).cloned())); } } ognibuild-0.2.6/src/bin/ognibuild-deb.rs000064400000000000000000000056541046102023000162300ustar 00000000000000use clap::Parser; use ognibuild::debian::build::{BuildOnceError, DEFAULT_BUILDER}; use std::io::Write; use std::path::PathBuf; #[derive(Parser)] struct Args { #[clap(short, long)] suffix: Option, #[clap(long, default_value = DEFAULT_BUILDER)] build_command: String, #[clap(short, long, default_value = "..")] output_directory: PathBuf, #[clap(short, long)] build_suite: Option, #[clap(long)] debug: bool, #[clap(long)] build_changelog_entry: Option, #[clap(short, long, default_value = ".")] /// The directory to build in directory: PathBuf, /// Use gbp dch to generate the changelog entry #[clap(long, default_value = "false")] gbp_dch: bool, } pub fn main() -> Result<(), i32> { let args = Args::parse(); let dir = args.directory; let (wt, subpath) = breezyshim::workingtree::open_containing(&dir).unwrap(); breezyshim::init(); breezyshim::plugin::load_plugins(); env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); log::info!("Using output directory {}", args.output_directory.display()); if args.suffix.is_some() && args.build_changelog_entry.is_none() { log::warn!("--suffix is ignored without --build-changelog-entry"); } if args.build_changelog_entry.is_some() && args.build_suite.is_none() { log::error!("--build-changelog-entry requires --build-suite"); return Err(1); } let source_date_epoch = std::env::var("SOURCE_DATE_EPOCH") .ok() .map(|s| s.parse().unwrap()); match ognibuild::debian::build::attempt_build( &wt, args.suffix.as_deref(), args.build_suite.as_deref(), &args.output_directory, &args.build_command, args.build_changelog_entry.as_deref(), &subpath, source_date_epoch, args.gbp_dch, None, None, None, ) { Ok(_) => {} Err(BuildOnceError::Unidentified { phase, description, .. }) => { if let Some(phase) = phase { log::error!("build failed during {}: {}", phase, description); } else { log::error!("build failed: {}", description); } return Err(1); } Err(BuildOnceError::Detailed { phase, description, error, .. }) => { if let Some(phase) = phase { log::error!("build failed during {}: {}", phase, description); } else { log::error!("build failed: {}", description); } log::info!("error: {:?}", error); return Err(1); } } Ok(()) } ognibuild-0.2.6/src/bin/ognibuild-dist.rs000064400000000000000000000141721046102023000164340ustar 00000000000000use breezyshim::export::export; use breezyshim::workingtree::{self, WorkingTree}; use clap::Parser; #[cfg(feature = "debian")] use debian_control::Control; use std::path::{Path, PathBuf}; // These imports are only used in Linux-specific code #[cfg(target_os = "linux")] use breezyshim::tree::Tree; #[cfg(target_os = "linux")] use ognibuild::analyze::AnalyzedError; #[cfg(target_os = "linux")] use ognibuild::buildsystem::Error; #[derive(Clone, Default, PartialEq, Eq)] pub enum Mode { #[default] Auto, Vcs, Buildsystem, } impl std::str::FromStr for Mode { type Err = String; fn from_str(s: &str) -> Result { match s { "auto" => Ok(Mode::Auto), "vcs" => Ok(Mode::Vcs), "buildsystem" => Ok(Mode::Buildsystem), _ => Err(format!("Unknown mode: {}", s)), } } } impl std::fmt::Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Mode::Auto => write!(f, "auto"), Mode::Vcs => write!(f, "vcs"), Mode::Buildsystem => write!(f, "buildsystem"), } } } #[derive(Parser)] struct Args { #[clap(short, long, default_value = "unstable-amd64-sbuild")] /// Name of chroot to use chroot: String, #[clap(default_value = ".")] /// Directory with upstream source. directory: PathBuf, #[clap(long)] /// Path to packaging directory. packaging_directory: Option, #[clap(long, default_value = "..")] /// Target directory target_directory: PathBuf, #[clap(long)] /// Enable debug output. debug: bool, #[clap(long, default_value = "auto")] /// Mechanism to use to create buildsystem mode: Mode, #[clap(long)] /// Include control directory in tarball. include_controldir: bool, } pub fn main() -> Result<(), i32> { let args = Args::parse(); env_logger::builder() .filter_level(if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }) .init(); let (tree, subpath) = workingtree::open_containing(&args.directory).unwrap(); // These variables are only used in the Linux-specific code path #[cfg(all(target_os = "linux", feature = "debian"))] let (packaging_tree, packaging_subdir, package_name): ( Option>, Option, Option, ) = if let Some(packaging_directory) = &args.packaging_directory { let (packaging_tree, packaging_subpath) = workingtree::open_containing(packaging_directory).unwrap(); let text = packaging_tree .get_file(Path::new("debian/control")) .unwrap(); let control: Control = Control::read(text).unwrap(); let package_name = control.source().unwrap().name().unwrap(); ( Some(Box::new(packaging_tree)), Some(packaging_subpath), Some(package_name), ) } else { (None, None, None) }; #[cfg(all(target_os = "linux", not(feature = "debian")))] let (packaging_tree, packaging_subdir, package_name): ( Option>, Option, Option, ) = (None, None, None); match args.mode { Mode::Vcs => { export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); Ok(()) } Mode::Auto | Mode::Buildsystem => { #[cfg(not(target_os = "linux"))] { log::error!("Unsupported mode: {}", args.mode); Err(1) } #[cfg(target_os = "linux")] match ognibuild::dist::create_dist_schroot( &tree, &args.target_directory.canonicalize().unwrap(), &args.chroot, packaging_tree.as_ref().map(|t| &**t as &dyn Tree), packaging_subdir.as_deref(), Some(args.include_controldir), &subpath, &mut ognibuild::logs::NoLogManager, None, package_name.as_deref(), ) { Ok(ret) => { log::info!("Created {}", ret.to_str().unwrap()); Ok(()) } Err(Error::IoError(e)) => { log::error!("IO error: {}", e); Err(1) } Err(Error::DependencyInstallError(e)) => { log::error!("Dependency install error: {}", e); Err(1) } Err(Error::NoBuildSystemDetected) => { if args.mode == Mode::Buildsystem { log::error!("No build system detected, unable to create tarball"); Err(1) } else { log::info!("No build system detected, falling back to simple export."); export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); Ok(()) } } Err(Error::Unimplemented) => { if args.mode == Mode::Buildsystem { log::error!("Unable to ask buildsystem for tarball"); Err(1) } else { log::info!("Build system does not support dist tarball creation, falling back to simple export."); export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); Ok(()) } } Err(Error::Error(AnalyzedError::Unidentified { lines, .. })) => { log::error!("Unidentified error: {:?}", lines); Err(1) } Err(Error::Error(AnalyzedError::Detailed { error, .. })) => { log::error!("Identified error during dist creation: {}", error); Err(1) } Err(e) => { log::error!("Error: {}", e); Err(1) } } } } } ognibuild-0.2.6/src/bin/report-apt-deps-status.rs000064400000000000000000000153711046102023000200700ustar 00000000000000use clap::Parser; use ognibuild::buildsystem::{detect_buildsystems, DependencyCategory}; use ognibuild::debian::apt::{dependency_to_deb_dependency, AptManager}; use ognibuild::dependencies::debian::{ default_tie_breakers, DebianDependency, DebianDependencyCategory, }; use ognibuild::dependency::Dependency; use ognibuild::session::plain::PlainSession; use ognibuild::session::Session; use std::collections::HashMap; use std::io::Write; use std::path::PathBuf; #[derive(Parser)] struct Args { #[clap(long)] detailed: bool, directory: PathBuf, #[clap(long)] debug: bool, } fn main() -> Result<(), i32> { let args = Args::parse(); let mut session = PlainSession::new(); env_logger::builder() .format(|buf, record| writeln!(buf, "{}", record.args())) .filter( None, if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info }, ) .init(); let directory = args.directory.canonicalize().unwrap(); session.chdir(&directory).unwrap(); let bss = detect_buildsystems(&directory); if bss.is_empty() { eprintln!("No build tools found"); std::process::exit(1); } log::debug!("Detected buildsystems: {:?}", bss); let mut deps: HashMap>> = HashMap::new(); for buildsystem in bss { match buildsystem.get_declared_dependencies(&session, Some(&[])) { Ok(declared_reqs) => { for (stage, req) in declared_reqs { deps.entry(stage).or_default().push(req); } } Err(_e) => { log::warn!( "Unable to get dependencies from buildsystem {:?}, skipping", buildsystem ); continue; } } } let tie_breakers = default_tie_breakers(&session); let apt = AptManager::new(&mut session, None); if args.detailed { let mut unresolved = false; for (stage, deps) in deps.iter() { log::info!("Stage: {}", stage); for dep in deps { if let Some(deb_dep) = dependency_to_deb_dependency(&apt, dep.as_ref(), &tie_breakers).unwrap() { log::info!("Dependency: {:?} → {}", dep, deb_dep.relation_string()); } else { log::warn!("Dependency: {:?} → ??", dep); unresolved = true; } } log::info!(""); } if unresolved { Err(1) } else { Ok(()) } } else { let mut dep_depends: HashMap> = HashMap::new(); let mut unresolved = vec![]; for (stage, reqs) in deps.iter() { for dep in reqs { if let Some(deb_dep) = dependency_to_deb_dependency(&apt, dep.as_ref(), &tie_breakers).unwrap() { match stage { DependencyCategory::Universal => { dep_depends .entry(DebianDependencyCategory::Build) .or_default() .push(deb_dep.clone()); dep_depends .entry(DebianDependencyCategory::Runtime) .or_default() .push(deb_dep); } DependencyCategory::Build => { dep_depends .entry(DebianDependencyCategory::Build) .or_default() .push(deb_dep); } DependencyCategory::Runtime => { dep_depends .entry(DebianDependencyCategory::Runtime) .or_default() .push(deb_dep); } DependencyCategory::BuildExtra(_name) => { // TODO: handle build extra: build profile? } DependencyCategory::Test => { dep_depends .entry(DebianDependencyCategory::Test("test".to_string())) .or_default() .push(deb_dep); } DependencyCategory::Dev => {} DependencyCategory::RuntimeExtra(_name) => { // TODO: handle runtime extra } } } else { unresolved.push(dep); } } } for (category, deps) in dep_depends.iter() { match category { DebianDependencyCategory::Build => { log::info!( "Build-Depends: {}", deps.iter() .map(|d| d.relation_string()) .collect::>() .join(", ") ); } DebianDependencyCategory::Runtime => { log::info!( "Depends: {}", deps.iter() .map(|d| d.relation_string()) .collect::>() .join(", ") ); } DebianDependencyCategory::Test(test) => { log::info!( "Test-Depends ({}): {}", test, deps.iter() .map(|d| d.relation_string()) .collect::>() .join(", ") ); } DebianDependencyCategory::Install => { log::info!( "Pre-Depends: {}", deps.iter() .map(|d| d.relation_string()) .collect::>() .join(", ") ); } } } if !unresolved.is_empty() { log::warn!("Unable to find apt packages for the following dependencies:"); for req in unresolved { log::warn!("* {:?}", req); } Err(1) } else { Ok(()) } } } ognibuild-0.2.6/src/buildlog.rs000064400000000000000000000117071046102023000145510ustar 00000000000000use crate::dependency::Dependency; use buildlog_consultant::problems::common::*; #[cfg(feature = "debian")] use buildlog_consultant::problems::debian::UnsatisfiedAptDependencies; use buildlog_consultant::Problem; /// Trait for converting build problems to dependencies. /// /// This trait allows build problems to report what dependencies would be needed to fix them. pub trait ToDependency: Problem { /// Convert this problem to a dependency that might fix it. /// /// # Returns /// * `Some(Box)` if the problem can be fixed by installing a dependency /// * `None` if the problem cannot be fixed by installing a dependency fn to_dependency(&self) -> Option>; } macro_rules! try_problem_to_dependency { ($expr:expr, $type:ty) => { if let Some(p) = $expr .as_any() .downcast_ref::<$type>() .and_then(|p| p.to_dependency()) { return Some(p); } }; } /// Convert a build problem to a dependency that might fix it. /// /// This function tries to convert various known problem types to dependencies /// that might fix them. /// /// # Arguments /// * `problem` - The build problem to convert /// /// # Returns /// * `Some(Box)` if the problem can be fixed by installing a dependency /// * `None` if the problem cannot be fixed by installing a dependency or isn't recognized pub fn problem_to_dependency(problem: &dyn Problem) -> Option> { // TODO(jelmer): Find a more idiomatic way to do this. try_problem_to_dependency!(problem, MissingAutoconfMacro); #[cfg(feature = "debian")] try_problem_to_dependency!(problem, UnsatisfiedAptDependencies); try_problem_to_dependency!(problem, MissingGoPackage); try_problem_to_dependency!(problem, MissingHaskellDependencies); try_problem_to_dependency!(problem, MissingJavaClass); try_problem_to_dependency!(problem, MissingJDK); try_problem_to_dependency!(problem, MissingJRE); try_problem_to_dependency!(problem, MissingJDKFile); try_problem_to_dependency!(problem, MissingLatexFile); try_problem_to_dependency!(problem, MissingCommand); try_problem_to_dependency!(problem, MissingCommandOrBuildFile); try_problem_to_dependency!(problem, VcsControlDirectoryNeeded); try_problem_to_dependency!(problem, MissingLuaModule); try_problem_to_dependency!(problem, MissingCargoCrate); try_problem_to_dependency!(problem, MissingRustCompiler); try_problem_to_dependency!(problem, MissingPkgConfig); try_problem_to_dependency!(problem, MissingFile); try_problem_to_dependency!(problem, MissingCHeader); try_problem_to_dependency!(problem, MissingJavaScriptRuntime); try_problem_to_dependency!(problem, MissingValaPackage); try_problem_to_dependency!(problem, MissingRubyGem); try_problem_to_dependency!(problem, DhAddonLoadFailure); try_problem_to_dependency!(problem, MissingLibrary); try_problem_to_dependency!(problem, MissingStaticLibrary); try_problem_to_dependency!(problem, MissingRubyFile); try_problem_to_dependency!(problem, MissingSprocketsFile); try_problem_to_dependency!(problem, CMakeFilesMissing); try_problem_to_dependency!(problem, MissingMavenArtifacts); try_problem_to_dependency!(problem, MissingGnomeCommonDependency); try_problem_to_dependency!(problem, MissingQtModules); try_problem_to_dependency!(problem, MissingQt); try_problem_to_dependency!(problem, MissingX11); try_problem_to_dependency!(problem, UnknownCertificateAuthority); try_problem_to_dependency!(problem, MissingLibtool); try_problem_to_dependency!(problem, MissingCMakeComponents); try_problem_to_dependency!(problem, MissingGnulibDirectory); try_problem_to_dependency!(problem, MissingIntrospectionTypelib); try_problem_to_dependency!(problem, MissingCSharpCompiler); try_problem_to_dependency!(problem, MissingXfceDependency); try_problem_to_dependency!(problem, MissingNodePackage); try_problem_to_dependency!(problem, MissingNodeModule); try_problem_to_dependency!(problem, MissingPerlPredeclared); try_problem_to_dependency!(problem, MissingPerlFile); try_problem_to_dependency!(problem, MissingPerlModule); try_problem_to_dependency!(problem, MissingPhpClass); try_problem_to_dependency!(problem, MissingPHPExtension); try_problem_to_dependency!(problem, MissingPytestFixture); try_problem_to_dependency!(problem, UnsupportedPytestArguments); try_problem_to_dependency!(problem, UnsupportedPytestConfigOption); try_problem_to_dependency!(problem, MissingPythonDistribution); try_problem_to_dependency!(problem, MissingPythonModule); try_problem_to_dependency!(problem, MissingSetupPyCommand); try_problem_to_dependency!(problem, MissingRPackage); try_problem_to_dependency!(problem, MissingVagueDependency); try_problem_to_dependency!(problem, MissingXmlEntity); try_problem_to_dependency!(problem, MissingMakeTarget); None } ognibuild-0.2.6/src/buildsystem.rs000064400000000000000000000754711046102023000153240ustar 00000000000000use crate::dependencies::BinaryDependency; use crate::dependency::Dependency; use crate::installer::{ install_missing_deps, Error as InstallerError, InstallationScope, Installer, }; use crate::output::Output; use crate::session::{which, Session}; use std::path::{Path, PathBuf}; /// The category of a dependency #[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash)] pub enum DependencyCategory { /// A dependency that is required for the package to build and run Universal, /// Building of artefacts Build, /// For running artefacts after build or install Runtime, /// Test infrastructure, e.g. test frameworks or test runners Test, /// Needed for development, e.g. linters or IDE plugins Dev, /// Extra build dependencies, e.g. for optional features BuildExtra(String), /// Extra dependencies, e.g. for optional features RuntimeExtra(String), } impl std::fmt::Display for DependencyCategory { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { DependencyCategory::Universal => write!(f, "universal"), DependencyCategory::Build => write!(f, "build"), DependencyCategory::Runtime => write!(f, "runtime"), DependencyCategory::Test => write!(f, "test"), DependencyCategory::Dev => write!(f, "dev"), DependencyCategory::BuildExtra(s) => write!(f, "build-extra:{}", s), DependencyCategory::RuntimeExtra(s) => write!(f, "runtime-extra:{}", s), } } } #[derive(Debug)] /// Error types for build system operations. /// /// These represent different kinds of errors that can occur when working with build systems. pub enum Error { /// The build system could not be detected. NoBuildSystemDetected, /// Error occurred while installing dependencies. DependencyInstallError(InstallerError), /// Error detected and analyzed from build output. Error(crate::analyze::AnalyzedError), /// Error from an IO operation. IoError(std::io::Error), /// The requested operation is not implemented by this build system. Unimplemented, /// Generic error with a message. Other(String), } impl From for Error { fn from(e: InstallerError) -> Self { Error::DependencyInstallError(e) } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::IoError(e) } } impl From for Error { fn from(e: crate::analyze::AnalyzedError) -> Self { Error::Error(e) } } impl From for Error { fn from(e: crate::session::Error) -> Self { match e { crate::session::Error::CalledProcessError(e) => { crate::analyze::AnalyzedError::Unidentified { retcode: e.code().unwrap(), lines: Vec::new(), secondary: None, } .into() } crate::session::Error::IoError(e) => e.into(), crate::session::Error::SetupFailure(_, _) => unreachable!(), crate::session::Error::ImageError(_) => unreachable!(), } } } impl From> for Error { fn from(e: crate::fix_build::IterateBuildError) -> Self { match e { crate::fix_build::IterateBuildError::FixerLimitReached(n) => { Error::Other(format!("Fixer limit reached: {}", n)) } crate::fix_build::IterateBuildError::Persistent(e) => { crate::analyze::AnalyzedError::Detailed { error: e, retcode: 1, } .into() } crate::fix_build::IterateBuildError::Unidentified { retcode, lines, secondary, } => crate::analyze::AnalyzedError::Unidentified { retcode, lines, secondary, } .into(), crate::fix_build::IterateBuildError::Other(o) => o.into(), } } } impl From> for Error { fn from(e: crate::fix_build::IterateBuildError) -> Self { match e { crate::fix_build::IterateBuildError::FixerLimitReached(n) => { Error::Other(format!("Fixer limit reached: {}", n)) } crate::fix_build::IterateBuildError::Persistent(e) => { crate::analyze::AnalyzedError::Detailed { error: e, retcode: 1, } .into() } crate::fix_build::IterateBuildError::Unidentified { retcode, lines, secondary, } => crate::analyze::AnalyzedError::Unidentified { retcode, lines, secondary, } .into(), crate::fix_build::IterateBuildError::Other(o) => o, } } } impl From for crate::fix_build::InterimError { fn from(e: Error) -> Self { match e { Error::Error(crate::analyze::AnalyzedError::Detailed { error, retcode: _ }) => { crate::fix_build::InterimError::Recognized(error) } e => crate::fix_build::InterimError::Other(e), } } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::NoBuildSystemDetected => write!(f, "No build system detected"), Error::DependencyInstallError(e) => write!(f, "Error installing dependency: {}", e), Error::Error(e) => write!(f, "Error: {}", e), Error::IoError(e) => write!(f, "IO Error: {}", e), Error::Other(e) => write!(f, "Error: {}", e), Error::Unimplemented => write!(f, "Unimplemented"), } } } impl std::error::Error for Error {} #[derive(Debug, Clone)] /// Target configuration for installation. /// /// Defines where and how packages should be installed. pub struct InstallTarget { /// The scope of installation (e.g., global or user). pub scope: InstallationScope, /// Optional installation prefix directory. pub prefix: Option, } impl DependencyCategory { /// Get all standard dependency categories. /// /// Returns an array containing all standard dependency categories. pub fn all() -> [DependencyCategory; 5] { [ DependencyCategory::Universal, DependencyCategory::Build, DependencyCategory::Runtime, DependencyCategory::Test, DependencyCategory::Dev, ] } } #[derive(Debug, Clone, PartialEq, Eq)] /// Standard build system actions. /// /// These represent the common actions that can be performed by build systems. pub enum Action { /// Clean the build environment. Clean, /// Build the project. Build, /// Run the project's tests. Test, /// Install the project. Install, } /// Determine the path to a binary, installing it if necessary pub fn guaranteed_which( session: &dyn Session, installer: &dyn Installer, name: &str, ) -> Result { match which(session, name) { Some(path) => Ok(PathBuf::from(path)), None => { installer.install(&BinaryDependency::new(name), InstallationScope::Global)?; Ok(PathBuf::from(which(session, name).unwrap())) } } } /// A particular buildsystem. pub trait BuildSystem: std::fmt::Debug { /// The name of the buildsystem. fn name(&self) -> &str; /// Create a distribution package for the project. /// /// # Arguments /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// * `target_directory` - Directory where the distribution package should be created /// * `quiet` - Whether to suppress output /// /// # Returns /// * The filename of the created distribution package on success /// * Error if the distribution creation fails fn dist( &self, session: &dyn Session, installer: &dyn Installer, target_directory: &Path, quiet: bool, ) -> Result; /// Install the dependencies declared by the build system. /// /// # Arguments /// * `categories` - The categories of dependencies to install /// * `scopes` - The scopes in which to install the dependencies /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// * `fixers` - Optional list of fixers to use if getting dependency information fails /// /// # Returns /// * `Ok(())` if the dependencies were installed successfully /// * Error if installation fails fn install_declared_dependencies( &self, categories: &[DependencyCategory], scopes: &[InstallationScope], session: &dyn Session, installer: &dyn Installer, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result<(), Error> { let declared_deps = self.get_declared_dependencies(session, fixers)?; let relevant: Vec<_> = declared_deps .into_iter() .filter(|(c, _d)| categories.contains(c)) .map(|(_, d)| d) .collect(); log::debug!("Declared dependencies: {:?}", relevant); let dep_refs: Vec<&dyn Dependency> = relevant.iter().map(|d| d.as_ref()).collect(); install_missing_deps(session, installer, scopes, &dep_refs)?; Ok(()) } /// Run tests for the project. /// /// # Arguments /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// * `Ok(())` if the tests pass /// * Error if the tests fail fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>; /// Build the project. /// /// # Arguments /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// * `Ok(())` if the build succeeds /// * Error if the build fails fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>; /// Clean the project's build artifacts. /// /// # Arguments /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// * `Ok(())` if the clean succeeds /// * Error if the clean fails fn clean(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>; /// Install the project. /// /// # Arguments /// * `session` - The session to run commands in /// * `installer` - Installer to use for installing dependencies /// * `install_target` - Target configuration for the installation /// /// # Returns /// * `Ok(())` if the installation succeeds /// * Error if the installation fails fn install( &self, session: &dyn Session, installer: &dyn Installer, install_target: &InstallTarget, ) -> Result<(), Error>; /// Get the dependencies declared by the build system. /// /// # Arguments /// * `session` - The session to run commands in /// * `fixers` - Optional list of fixers to use if getting dependency information fails /// /// # Returns /// * List of dependencies with their categories /// * Error if getting dependency information fails fn get_declared_dependencies( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error>; /// Get the outputs declared by the build system. /// /// # Arguments /// * `session` - The session to run commands in /// * `fixers` - Optional list of fixers to use if getting output information fails /// /// # Returns /// * List of declared outputs /// * Error if getting output information fails fn get_declared_outputs( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error>; /// Convert this build system to Any for dynamic casting. /// /// This method allows for conversion of the build system to concrete types at runtime. /// /// # Returns /// A reference to this build system as Any fn as_any(&self) -> &dyn std::any::Any; } /// A single buildsystem registry entry. struct BuildSystemEntry { /// The name of the buildsystem name: &'static str, /// Function to probe for this buildsystem probe: fn(&Path) -> Option>, } /// Registry of all supported buildsystems in detection order. const BUILDSYSTEM_REGISTRY: &[BuildSystemEntry] = &[ BuildSystemEntry { name: "pear", probe: Pear::probe, }, BuildSystemEntry { name: "setup.py", probe: crate::buildsystems::python::SetupPy::probe, }, BuildSystemEntry { name: "node", probe: crate::buildsystems::node::Node::probe, }, BuildSystemEntry { name: "waf", probe: crate::buildsystems::waf::Waf::probe, }, BuildSystemEntry { name: "gem", probe: crate::buildsystems::ruby::Gem::probe, }, BuildSystemEntry { name: "meson", probe: crate::buildsystems::meson::Meson::probe, }, BuildSystemEntry { name: "cargo", probe: crate::buildsystems::rust::Cargo::probe, }, BuildSystemEntry { name: "cabal", probe: crate::buildsystems::haskell::Cabal::probe, }, BuildSystemEntry { name: "gradle", probe: crate::buildsystems::java::Gradle::probe, }, BuildSystemEntry { name: "maven", probe: crate::buildsystems::java::Maven::probe, }, BuildSystemEntry { name: "distzilla", probe: crate::buildsystems::perl::DistZilla::probe, }, BuildSystemEntry { name: "perl-build-tiny", probe: crate::buildsystems::perl::PerlBuildTiny::probe, }, BuildSystemEntry { name: "go", probe: crate::buildsystems::go::Golang::probe, }, BuildSystemEntry { name: "bazel", probe: crate::buildsystems::bazel::Bazel::probe, }, BuildSystemEntry { name: "r", probe: crate::buildsystems::r::R::probe, }, BuildSystemEntry { name: "octave", probe: crate::buildsystems::octave::Octave::probe, }, BuildSystemEntry { name: "cmake", probe: crate::buildsystems::make::CMake::probe, }, BuildSystemEntry { name: "gnome-shell-extension", probe: crate::buildsystems::gnome::GnomeShellExtension::probe, }, // Make is intentionally at the end of the list. BuildSystemEntry { name: "make", probe: crate::buildsystems::make::Make::probe, }, BuildSystemEntry { name: "composer", probe: Composer::probe, }, BuildSystemEntry { name: "runtests", probe: RunTests::probe, }, ]; /// XML namespaces used by PEAR package definitions. pub const PEAR_NAMESPACES: &[&str] = &[ "http://pear.php.net/dtd/package-2.0", "http://pear.php.net/dtd/package-2.1", ]; #[derive(Debug)] /// PEAR (PHP Extension and Application Repository) build system. pub struct Pear(pub PathBuf); impl Pear { /// Create a new PEAR build system. /// /// # Arguments /// * `path` - Path to the PEAR package.xml file /// /// # Returns /// A new PEAR build system instance pub fn new(path: PathBuf) -> Self { Self(path) } /// Detect if a directory contains a PEAR project. /// /// # Arguments /// * `path` - Directory to probe /// /// # Returns /// * `Some(Box)` if a PEAR project is detected /// * `None` if no PEAR project is detected pub fn probe(path: &Path) -> Option> { let package_xml_path = path.join("package.xml"); if !package_xml_path.exists() { return None; } use xmltree::Element; let root = Element::parse(std::fs::File::open(package_xml_path).unwrap()).unwrap(); // Check that the root tag is and that the namespace is one of the known PEAR // namespaces. if root .namespace .as_deref() .and_then(|ns| PEAR_NAMESPACES.iter().find(|&n| *n == ns)) .is_none() { log::warn!( "Namespace of package.xml is not recognized as a PEAR package: {:?}", root.namespace ); return None; } if root.name != "package" { log::warn!("Root tag of package.xml is not : {:?}", root.name); return None; } log::debug!( "Found package.xml with namespace {}, assuming pear package.", root.namespace.as_ref().unwrap() ); Some(Box::new(Self(PathBuf::from(path)))) } } impl BuildSystem for Pear { fn name(&self) -> &str { "pear" } fn dist( &self, session: &dyn Session, installer: &dyn Installer, target_directory: &Path, quiet: bool, ) -> Result { let dc = crate::dist_catcher::DistCatcher::new(vec![session.external_path(Path::new("."))]); let pear = guaranteed_which(session, installer, "pear")?; session .command(vec![pear.to_str().unwrap(), "package"]) .quiet(quiet) .run_detecting_problems()?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { let pear = guaranteed_which(session, installer, "pear")?; session .command(vec![pear.to_str().unwrap(), "run-tests"]) .run_detecting_problems()?; Ok(()) } fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { let pear = guaranteed_which(session, installer, "pear")?; session .command(vec![ pear.to_str().unwrap(), "build", self.0.to_str().unwrap(), ]) .run_detecting_problems()?; Ok(()) } fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn install( &self, session: &dyn Session, installer: &dyn Installer, _install_target: &InstallTarget, ) -> Result<(), Error> { let pear = guaranteed_which(session, installer, "pear")?; session .command(vec![ pear.to_str().unwrap(), "install", self.0.to_str().unwrap(), ]) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { let path = self.0.join("package.xml"); use xmltree::Element; let root = Element::parse(std::fs::File::open(path).unwrap()).unwrap(); // Check that the root tag is and that the namespace is one of the known PEAR // namespaces. if root .namespace .as_deref() .and_then(|ns| PEAR_NAMESPACES.iter().find(|&n| *n == ns)) .is_none() { log::warn!( "Namespace of package.xml is not recognized as a PEAR package: {:?}", root.namespace ); return Ok(vec![]); } if root.name != "package" { log::warn!("Root tag of package.xml is not : {:?}", root.name); return Ok(vec![]); } let dependencies_tag = root .get_child("dependencies") .ok_or_else(|| Error::Other("No tag found in ".to_owned()))?; let required_tag = dependencies_tag .get_child("required") .ok_or_else(|| Error::Other("No tag found in ".to_owned()))?; Ok(required_tag .children .iter() .filter_map(|x| x.as_element()) .filter(|c| c.name.as_str() == "package") .filter_map( |package_tag| -> Option<(DependencyCategory, Box)> { let name = package_tag .get_child("name") .and_then(|n| n.get_text()) .unwrap() .into_owned(); let min_version = package_tag .get_child("min") .and_then(|m| m.get_text()) .map(|s| s.into_owned()); let max_version = package_tag .get_child("max") .and_then(|m| m.get_text()) .map(|s| s.into_owned()); let channel = package_tag .get_child("channel") .and_then(|c| c.get_text()) .map(|s| s.into_owned()); Some(( DependencyCategory::Universal, Box::new(crate::dependencies::php::PhpPackageDependency { package: name, channel, min_version, max_version, }) as Box, )) }, ) .collect()) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } /// Detect build systems. pub fn scan_buildsystems(path: &Path) -> Vec<(PathBuf, Box)> { let mut ret = vec![]; ret.extend( detect_buildsystems(path) .into_iter() .map(|bs| (PathBuf::from(path), bs)), ); if ret.is_empty() { // Nothing found. Try the next level? for entry in std::fs::read_dir(path).unwrap() { let entry = entry.unwrap(); if entry.file_type().unwrap().is_dir() { ret.extend( detect_buildsystems(&entry.path()) .into_iter() .map(|bs| (entry.path(), bs)), ); } } } ret } #[derive(Debug)] /// PHP Composer build system. pub struct Composer(pub PathBuf); impl Composer { /// Create a new Composer build system instance. /// /// # Arguments /// * `path` - Path to the project directory /// /// # Returns /// A new Composer build system instance pub fn new(path: PathBuf) -> Self { Self(path) } /// Detect if a directory contains a Composer project. /// /// # Arguments /// * `path` - Directory to probe /// /// # Returns /// * `Some(Box)` if a Composer project is detected /// * `None` if no Composer project is detected pub fn probe(path: &Path) -> Option> { if path.join("composer.json").exists() { Some(Box::new(Self(path.into()))) } else { None } } } impl BuildSystem for Composer { fn name(&self) -> &str { "composer" } fn dist( &self, _session: &dyn Session, _installer: &dyn Installer, _target_directory: &Path, _quiet: bool, ) -> Result { todo!() } fn test(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn build(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn install( &self, _session: &dyn Session, _installer: &dyn Installer, _install_target: &InstallTarget, ) -> Result<(), Error> { todo!() } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { todo!() } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug)] /// Generic build system that just runs tests. pub struct RunTests(pub PathBuf); impl RunTests { /// Create a new RunTests build system instance. /// /// # Arguments /// * `path` - Path to the project directory /// /// # Returns /// A new RunTests build system instance pub fn new(path: PathBuf) -> Self { Self(path) } /// Detect if a directory contains a project with tests that can be run. /// /// # Arguments /// * `path` - Directory to probe /// /// # Returns /// * `Some(Box)` if runnable tests are detected /// * `None` if no runnable tests are detected pub fn probe(path: &Path) -> Option> { if path.join("runtests.sh").exists() { Some(Box::new(Self(path.into()))) } else { None } } } impl BuildSystem for RunTests { fn name(&self) -> &str { "runtests" } fn dist( &self, _session: &dyn Session, _installer: &dyn Installer, _target_directory: &Path, _quiet: bool, ) -> Result { todo!() } fn test(&self, session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { let interpreter = crate::shebang::shebang_binary(&self.0.join("runtests.sh")).unwrap(); let argv = if interpreter.is_some() { vec!["./runtests.sh"] } else { vec!["/bin/bash", "./runtests.sh"] }; session.command(argv).run_detecting_problems()?; Ok(()) } fn build(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { todo!() } fn install( &self, _session: &dyn Session, _installer: &dyn Installer, _install_target: &InstallTarget, ) -> Result<(), Error> { todo!() } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { todo!() } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } /// Detect all applicable build systems for a given path. /// /// This function attempts to detect any build systems that can be used with the /// provided project directory. Multiple build systems may be detected for a single project. /// /// # Arguments /// * `path` - Path to the project directory /// /// # Returns /// A vector of detected build systems, sorted in order of preference pub fn detect_buildsystems(path: &std::path::Path) -> Vec> { if !path.exists() { log::error!("Path does not exist: {:?}", path); return vec![]; } let path = path.canonicalize().unwrap(); let mut ret = vec![]; for entry in BUILDSYSTEM_REGISTRY { let bs = (entry.probe)(&path); if let Some(bs) = bs { ret.push(bs); } } ret } /// Get the most appropriate build system for a given path. /// /// This function returns the first (most preferred) build system that can be used /// with the provided project directory, along with its path. /// /// # Arguments /// * `path` - Path to the project directory /// /// # Returns /// An optional tuple containing the path to the build system file and the build system instance pub fn get_buildsystem(path: &Path) -> Option<(PathBuf, Box)> { scan_buildsystems(path).into_iter().next() } /// Get all supported build system names. /// /// # Returns /// A vector of all supported build system names in detection order pub fn supported_buildsystem_names() -> Vec<&'static str> { BUILDSYSTEM_REGISTRY .iter() .map(|entry| entry.name) .collect() } /// Get a build system by name for a given path. /// /// This function tries to create a specific build system by name for the provided /// project directory. /// /// # Arguments /// * `name` - Name of the build system to use /// * `path` - Path to the project directory /// /// # Returns /// An optional build system instance if the specified build system is applicable pub fn buildsystem_by_name(name: &str, path: &Path) -> Option> { BUILDSYSTEM_REGISTRY .iter() .find(|entry| entry.name == name) .and_then(|entry| (entry.probe)(path)) } #[cfg(test)] mod tests { use super::*; use crate::installer::NullInstaller; use crate::session::plain::PlainSession; #[test] fn test_guaranteed_which() { let session = PlainSession::new(); let installer = NullInstaller::new(); let _path = guaranteed_which(&session, &installer, "ls").unwrap(); } #[test] fn test_guaranteed_which_not_found() { let session = PlainSession::new(); let installer = NullInstaller::new(); assert!(matches!( guaranteed_which(&session, &installer, "this-does-not-exist").unwrap_err(), InstallerError::UnknownDependencyFamily, )); } #[test] fn test_supported_buildsystem_names() { let names = supported_buildsystem_names(); assert!(!names.is_empty()); assert!(names.contains(&"cargo")); assert!(names.contains(&"make")); assert!(names.contains(&"meson")); assert_eq!(names.len(), 21); } } ognibuild-0.2.6/src/buildsystems/bazel.rs000064400000000000000000000071221046102023000165700ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Bazel build system representation. pub struct Bazel { #[allow(dead_code)] path: PathBuf, } impl Bazel { /// Create a new Bazel build system instance. /// /// # Arguments /// * `path` - Path to the Bazel project directory /// /// # Returns /// A new Bazel instance pub fn new(path: &Path) -> Self { Self { path: path.to_path_buf(), } } /// Probe a directory to check if it contains a Bazel build system. /// /// # Arguments /// * `path` - Path to check for Bazel build files /// /// # Returns /// Some(BuildSystem) if a Bazel build is found, None otherwise pub fn probe(path: &Path) -> Option> { if path.join("BUILD").exists() { Some(Box::new(Self::new(path))) } else { None } } /// Check if a Bazel build system exists at the specified path. /// /// # Arguments /// * `path` - Path to check for Bazel build files /// /// # Returns /// true if a BUILD file exists, false otherwise pub fn exists(path: &Path) -> bool { path.join("BUILD").exists() } } impl BuildSystem for Bazel { fn name(&self) -> &str { "bazel" } fn dist( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _target_directory: &Path, _quiet: bool, ) -> Result { Err(Error::Unimplemented) } fn test( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["bazel", "test", "//..."]) .run_detecting_problems()?; Ok(()) } fn build( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["bazel", "build", "//..."]) .run_detecting_problems()?; Ok(()) } fn clean( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn install( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["bazel", "build", "//..."]) .run_detecting_problems()?; Err(Error::Unimplemented) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { Err(Error::Unimplemented) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/gnome.rs000064400000000000000000000104551046102023000166030ustar 00000000000000use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; use crate::dependencies::vague::VagueDependency; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Representation of a GNOME Shell extension. pub struct GnomeShellExtension { path: PathBuf, } #[derive(Debug, serde::Deserialize)] #[allow(dead_code)] struct Metadata { name: String, description: String, uuid: String, shell_version: String, version: String, url: String, license: String, authors: Vec, settings_schema: Option, gettext_domain: Option, extension: Option, _generated: Option, } impl GnomeShellExtension { /// Create a new GNOME Shell extension instance. /// /// # Arguments /// * `path` - Path to the GNOME Shell extension directory /// /// # Returns /// A new GnomeShellExtension instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Check if a GNOME Shell extension exists at the specified path. /// /// # Arguments /// * `path` - Path to check for GNOME Shell extension /// /// # Returns /// true if metadata.json exists, false otherwise pub fn exists(path: &PathBuf) -> bool { path.join("metadata.json").exists() } /// Probe a directory to check if it contains a GNOME Shell extension. /// /// # Arguments /// * `path` - Path to check for GNOME Shell extension files /// /// # Returns /// Some(BuildSystem) if a GNOME Shell extension is found, None otherwise pub fn probe(path: &Path) -> Option> { if Self::exists(&path.to_path_buf()) { log::debug!("Found metadata.json , assuming gnome-shell extension."); Some(Box::new(Self::new(path.to_path_buf()))) } else { None } } } impl BuildSystem for GnomeShellExtension { fn name(&self) -> &str { "gnome-shell-extension" } fn dist( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _target_directory: &std::path::Path, _quiet: bool, ) -> Result { Err(Error::Unimplemented) } fn test( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Ok(()) } fn build( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Ok(()) } fn clean( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn install( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { let f = std::fs::File::open(self.path.join("metadata.json")).unwrap(); let metadata: Metadata = serde_json::from_reader(f).unwrap(); let deps: Vec<(DependencyCategory, Box)> = vec![( DependencyCategory::Universal, Box::new(VagueDependency::new( "gnome-shell", Some(&metadata.shell_version), )), )]; Ok(deps) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/go.rs000064400000000000000000000177211046102023000161060ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use crate::dependencies::go::{GoDependency, GoPackageDependency}; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Golang (Go) build system representation. pub struct Golang { path: PathBuf, } impl Golang { /// Create a new Golang build system instance. /// /// # Arguments /// * `path` - Path to the Go project directory /// /// # Returns /// A new Golang instance pub fn new(path: &Path) -> Self { Self { path: path.to_path_buf(), } } /// Probe a directory to check if it contains a Go project. /// /// Checks for go.mod, go.sum, or .go files in subdirectories. /// /// # Arguments /// * `path` - Path to check for Go project files /// /// # Returns /// Some(BuildSystem) if a Go project is found, None otherwise pub fn probe(path: &Path) -> Option> { if path.join("go.mod").exists() { return Some(Box::new(Self::new(path))); } if path.join("go.sum").exists() { return Some(Box::new(Self::new(path))); } for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); if !entry.file_type().unwrap().is_dir() { continue; } match entry.path().read_dir() { Ok(d) => { for subentry in d { let subentry = subentry.unwrap(); if subentry.file_type().unwrap().is_file() && subentry.path().extension() == Some(std::ffi::OsStr::new("go")) { return Some(Box::new(Self::new(path))); } } } Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { // Ignore permission denied errors. log::debug!("Permission denied reading {:?}", entry.path()); } Err(e) => { panic!("Error reading {:?}: {:?}", entry.path(), e); } } } None } } impl BuildSystem for Golang { fn name(&self) -> &str { "golang" } fn dist( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _target_directory: &Path, _quiet: bool, ) -> Result { Err(Error::Unimplemented) } fn test( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["go", "test", "./..."]) .run_detecting_problems()?; Ok(()) } fn build( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["go", "build"]) .run_detecting_problems()?; Ok(()) } fn clean( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { session.command(vec!["go", "clean"]).check_call()?; Ok(()) } fn install( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { session .command(vec!["go", "install"]) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { let mut ret = vec![]; let go_mod_path = self.path.join("go.mod"); if go_mod_path.exists() { let f = std::fs::File::open(go_mod_path).unwrap(); ret.extend( go_mod_dependencies(f) .into_iter() .map(|dep| (crate::buildsystem::DependencyCategory::Build, dep)), ); } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } enum GoModEntry { Go(String), Require(String, String), Exclude(String, String), Replace(String, String, String, String), Retract(String, String), Toolchain(String), Module(String), } impl GoModEntry { fn parse(name: &str, args: &[&str]) -> Self { match name { "go" => GoModEntry::Go(args[0].to_string()), "require" => GoModEntry::Require(args[0].to_string(), args[1].to_string()), "exclude" => GoModEntry::Exclude(args[0].to_string(), args[1].to_string()), "replace" => { assert_eq!(args[2], "=>"); GoModEntry::Replace( args[0].to_string(), args[1].to_string(), args[3].to_string(), args[4].to_string(), ) } "retract" => GoModEntry::Retract(args[0].to_string(), args[1].to_string()), "toolchain" => GoModEntry::Toolchain(args[0].to_string()), "module" => GoModEntry::Module(args[0].to_string()), _ => panic!("unknown go.mod directive: {}", name), } } } fn parse_go_mod(f: R) -> Vec { let f = std::io::BufReader::new(f); let mut ret = vec![]; use std::io::BufRead; let lines = f .lines() .map(|l| l.unwrap()) .map(|l| l.split("//").next().unwrap().to_string()) .collect::>(); let mut line_iter = lines.iter(); while let Some(mut line) = line_iter.next() { let parts = line.trim().split(" ").collect::>(); if parts.is_empty() || parts == [""] { continue; } if parts.len() == 2 && parts[1] == "(" { line = line_iter.next().unwrap(); while line.trim() != ")" { ret.push(GoModEntry::parse( parts[0], line.trim().split(' ').collect::>().as_slice(), )); line = line_iter.next().expect("unexpected EOF"); } } else { ret.push(GoModEntry::parse(parts[0], &parts[1..])); } } ret } fn go_mod_dependencies(r: R) -> Vec> { let mut ret: Vec> = vec![]; for entry in parse_go_mod(r) { match entry { GoModEntry::Go(version) => { ret.push(Box::new(GoDependency::new(Some(&version)))); } GoModEntry::Require(name, version) => { ret.push(Box::new(GoPackageDependency::new( &name, Some(version.strip_prefix('v').unwrap()), ))); } GoModEntry::Exclude(_name, _version) => { // TODO(jelmer): Create conflicts? } GoModEntry::Module(_name) => {} GoModEntry::Retract(_name, _version) => {} GoModEntry::Toolchain(_name) => {} GoModEntry::Replace(_n1, _v1, _n2, _v2) => { // TODO(jelmer): do.. something? } } } ret } ognibuild-0.2.6/src/buildsystems/haskell.rs000064400000000000000000000107001046102023000171120ustar 00000000000000use crate::buildsystem::{BuildSystem, Error}; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Haskell Cabal build system representation. pub struct Cabal { #[allow(dead_code)] path: PathBuf, } impl Cabal { /// Create a new Cabal build system instance. /// /// # Arguments /// * `path` - Path to the Cabal project directory /// /// # Returns /// A new Cabal instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Run a Cabal command with the given arguments. /// /// Handles common Cabal errors, such as needing to run configure first. /// /// # Arguments /// * `session` - The session to run the command in /// * `extra_args` - Additional arguments to pass to the Cabal command /// /// # Returns /// Ok(()) if the command succeeded, otherwise an error fn run( &self, session: &dyn crate::session::Session, extra_args: Vec<&str>, ) -> Result<(), crate::analyze::AnalyzedError> { let mut args = vec!["runhaskell", "Setup.hs"]; args.extend(extra_args); match session.command(args.clone()).run_detecting_problems() { Ok(ls) => Ok(ls), Err(crate::analyze::AnalyzedError::Unidentified { lines, .. }) if lines.contains(&"Run the 'configure' command first.".to_string()) => { session .command(vec!["runhaskell", "Setup.hs", "configure"]) .run_detecting_problems()?; session.command(args).run_detecting_problems() } Err(e) => Err(e), } .map(|_| ()) } /// Probe a directory to check if it contains a Cabal project. /// /// # Arguments /// * `path` - Path to check for Cabal project files /// /// # Returns /// Some(BuildSystem) if a Cabal project is found, None otherwise pub fn probe(path: &Path) -> Option> { if path.join("Setup.hs").exists() { Some(Box::new(Self::new(path.to_owned()))) } else { None } } } impl BuildSystem for Cabal { fn name(&self) -> &str { "cabal" } fn dist( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, target_directory: &std::path::Path, _quiet: bool, ) -> Result { let dc = crate::dist_catcher::DistCatcher::new(vec![ session.external_path(Path::new("dist-newstyle/sdist")), session.external_path(Path::new("dist")), ]); self.run(session, vec!["sdist"])?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.run(session, vec!["test"])?; Ok(()) } fn build( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn clean( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn install( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { Err(Error::Unimplemented) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/java.rs000064400000000000000000000231571046102023000164220ustar 00000000000000use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; use crate::dependency::Dependency; use crate::installer::{InstallationScope, Installer}; use crate::session::Session; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Gradle build system for Java projects. pub struct Gradle { path: PathBuf, executable: String, } impl Gradle { /// Create a new Gradle build system with a specified path and executable. pub fn new(path: PathBuf, executable: String) -> Self { Self { path, executable } } /// Create a new Gradle build system with the default executable name. pub fn simple(path: PathBuf) -> Self { Self { path, executable: "gradle".to_string(), } } /// Check if a Gradle build system exists at the given path. pub fn exists(path: &Path) -> bool { path.join("build.gradle").exists() || path.join("build.gradle.kts").exists() } /// Create a Gradle build system from a path, using wrapper if available. pub fn from_path(path: &Path) -> Self { if path.join("gradlew").exists() { Self::new(path.to_path_buf(), "./gradlew".to_string()) } else { Self::simple(path.to_path_buf()) } } /// Probe a directory for a Gradle build system. pub fn probe(path: &Path) -> Option> { if Self::exists(path) { log::debug!("Found build.gradle, assuming gradle package."); Some(Box::new(Self::from_path(path))) } else { None } } fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { if !self.executable.starts_with("./") { let binary_req = crate::dependencies::BinaryDependency::new(&self.executable); if !binary_req.present(session) { installer.install(&binary_req, InstallationScope::Global)?; } } Ok(()) } fn run( &self, session: &dyn Session, installer: &dyn Installer, task: &str, args: Vec<&str>, ) -> Result<(), Error> { self.setup(session, installer)?; let mut argv = vec![]; if self.executable.starts_with("./") && (!std::fs::metadata(self.path.join(&self.executable)) .unwrap() .permissions() .mode() & 0o111 != 0) { argv.push("sh".to_string()); } argv.extend(vec![self.executable.clone(), task.to_owned()]); argv.extend(args.iter().map(|x| x.to_string())); match session .command(argv.iter().map(|x| x.as_str()).collect()) .run_detecting_problems() { Err(crate::analyze::AnalyzedError::Unidentified { lines, .. }) if lines.iter().any(|l| { lazy_regex::regex_is_match!(r"Task '(.*)' not found in root project '.*'\.", l) }) => { unimplemented!("Task not found"); } other => other, } .map(|_| ()) .map_err(|e| e.into()) } } impl BuildSystem for Gradle { fn name(&self) -> &str { "gradle" } fn dist( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, target_directory: &std::path::Path, _quiet: bool, ) -> Result { let dc = crate::dist_catcher::DistCatcher::new(vec![session.external_path(Path::new("."))]); self.run(session, installer, "distTar", [].to_vec())?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.run(session, installer, "test", [].to_vec())?; Ok(()) } fn build( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.run(session, installer, "build", [].to_vec())?; Ok(()) } fn clean( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.run(session, installer, "clean", [].to_vec())?; Ok(()) } fn install( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(crate::buildsystem::Error::Unimplemented) // TODO(jelmer): installDist just creates files under build/install/... // self.run(session, installer, "installDist", [].to_vec())?; // Ok(()) } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { Err(crate::buildsystem::Error::Unimplemented) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(crate::buildsystem::Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug)] /// Maven build system for Java projects. pub struct Maven { path: PathBuf, } impl Maven { /// Probe a directory for a Maven build system. pub fn probe(path: &Path) -> Option> { if path.join("pom.xml").exists() { log::debug!("Found pom.xml, assuming maven package."); Some(Box::new(Self::new(path.join("pom.xml")))) } else { None } } /// Create a new Maven build system. pub fn new(path: PathBuf) -> Self { Self { path } } } impl BuildSystem for Maven { fn name(&self) -> &str { "maven" } fn dist( &self, _session: &dyn Session, _installer: &dyn Installer, _target_directory: &Path, _quiet: bool, ) -> Result { // TODO(jelmer): 'mvn generate-sources' creates a jar in target/. is that what we need? Err(Error::Unimplemented) } fn test(&self, session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { session .command(vec!["mvn", "test"]) .run_detecting_problems()?; Ok(()) } fn build(&self, session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { session .command(vec!["mvn", "compile"]) .run_detecting_problems()?; Ok(()) } fn clean(&self, session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { session .command(vec!["mvn", "clean"]) .run_detecting_problems()?; Ok(()) } fn install( &self, session: &dyn Session, _installer: &dyn Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), Error> { session .command(vec!["mvn", "install"]) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { let mut ret = vec![]; use xmltree::Element; let f = std::fs::File::open(&self.path).unwrap(); let root = Element::parse(f).unwrap(); if root.namespace != Some("http://maven.apache.org/POM/4.0.0".to_string()) { log::warn!("Unknown namespace in pom.xml: {:?}", root.namespace); return Ok(vec![]); } assert_eq!(root.name, "project"); if let Some(deps_tag) = root.get_child("dependencies") { for dep in deps_tag.children.iter().filter_map(|x| x.as_element()) { let version_tag = dep.get_child("version"); let group_id = dep .get_child("groupId") .unwrap() .get_text() .unwrap() .into_owned(); let artifact_id = dep .get_child("artifactId") .unwrap() .get_text() .unwrap() .into_owned(); let version = version_tag.map(|x| x.get_text().unwrap().into_owned()); ret.push(( DependencyCategory::Universal, Box::new(crate::dependencies::MavenArtifactDependency { group_id, artifact_id, version, kind: None, }) as Box, )); } } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/make.rs000064400000000000000000000511471046102023000164160ustar 00000000000000use crate::analyze::AnalyzedError; use crate::buildsystem::{BuildSystem, DependencyCategory, Error, InstallTarget}; use crate::dependency::Dependency; use crate::installer::Installer; use crate::session::Session; use crate::shebang::shebang_binary; use std::path::{Path, PathBuf}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum Kind { MakefilePL, Automake, Autoconf, Qmake, Make, } #[derive(Debug)] /// Make build system. /// /// Supports different kinds of Make-based build systems, including regular Make, /// Automake, Autoconf, Makefile.PL, and Qmake. pub struct Make { path: PathBuf, kind: Kind, } /// Check if a Makefile exists in the current directory. fn makefile_exists(session: &dyn Session) -> bool { session.exists(Path::new("Makefile")) || session.exists(Path::new("GNUmakefile")) || session.exists(Path::new("makefile")) } impl Make { /// Create a new Make build system with the specified path. /// /// Automatically detects the specific type of Make build system. pub fn new(path: &Path) -> Self { let kind = if path.join("Makefile.PL").exists() { Kind::MakefilePL } else if path.join("Makefile.am").exists() { Kind::Automake } else if path.join("configure.ac").exists() || path.join("configure.in").exists() || path.join("autogen.sh").exists() { Kind::Autoconf } else if path .read_dir() .unwrap() .any(|n| n.unwrap().file_name().to_string_lossy().ends_with(".pro")) { Kind::Qmake } else { Kind::Make }; Self { path: path.to_path_buf(), kind, } } fn setup( &self, session: &dyn Session, _installer: &dyn Installer, prefix: Option<&Path>, ) -> Result<(), Error> { if self.kind == Kind::MakefilePL && !makefile_exists(session) { session .command(vec!["perl", "Makefile.PL"]) .cwd(&self.path) .run_detecting_problems()?; } if !makefile_exists(session) && !session.exists(&self.path.join("configure")) { if session.exists(&self.path.join("autogen.sh")) { if shebang_binary(&self.path.join("autogen.sh")) .unwrap() .is_none() { session .command(vec!["/bin/sh", "./autogen.sh"]) .cwd(&self.path) .run_detecting_problems()?; } match session .command(vec!["./autogen.sh"]) .cwd(&self.path) .run_detecting_problems() { Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains( &"Gnulib not yet bootstrapped; run ./bootstrap instead.".to_string(), ) => { session .command(vec!["./bootstrap"]) .cwd(&self.path) .run_detecting_problems()?; session .command(vec!["./autogen.sh"]) .cwd(&self.path) .run_detecting_problems() } other => other, }?; } else if session.exists(&self.path.join("configure.ac")) || session.exists(&self.path.join("configure.in")) { session .command(vec!["autoreconf", "-i"]) .cwd(&self.path) .run_detecting_problems()?; } } if !makefile_exists(session) && session.exists(&self.path.join("configure")) { let args = [ vec!["./configure".to_string()], if let Some(p) = prefix { vec![format!("--prefix={}", p.to_str().unwrap())] } else { vec![] }, ] .concat(); session .command(args.iter().map(|s| s.as_str()).collect()) .cwd(&self.path) .run_detecting_problems()?; } if !makefile_exists(session) && session .read_dir(&self.path) .unwrap() .iter() .any(|n| n.file_name().to_str().unwrap().ends_with(".pro")) { session .command(vec!["qmake"]) .cwd(&self.path) .run_detecting_problems()?; } Ok(()) } fn run_make( &self, session: &dyn Session, args: Vec<&str>, prefix: Option<&Path>, ) -> Result<(), AnalyzedError> { fn wants_configure(line: &str) -> bool { if line.starts_with("Run ./configure") { return true; } if line == "Please run ./configure first" { return true; } if line.starts_with("Project not configured") { return true; } if line.starts_with("The project was not configured") { return true; } lazy_regex::regex_is_match!( r"Makefile:[0-9]+: \*\*\* You need to run \.\/configure .*", line ) } let build_path = self.path.join("build"); let cwd = if session.exists(&build_path.join("Makefile")) { &build_path } else { &self.path }; let args = [vec!["make"], args].concat(); match session .command(args.clone()) .cwd(cwd) .run_detecting_problems() { Err(AnalyzedError::Unidentified { lines, .. }) if lines.len() < 5 && lines.iter().any(|l| wants_configure(l)) => { session .command( [ vec!["./configure".to_string()], if let Some(p) = prefix.as_ref() { vec![format!("--prefix={}", p.to_str().unwrap())] } else { vec![] }, ] .concat() .iter() .map(|x| x.as_str()) .collect(), ) .cwd(&self.path) .run_detecting_problems()?; session.command(args).cwd(cwd).run_detecting_problems() } Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains( &"Reconfigure the source tree (via './config' or 'perl Configure'), please." .to_string(), ) => { session .command(vec!["./config"]) .cwd(&self.path) .run_detecting_problems()?; session.command(args).cwd(cwd).run_detecting_problems() } other => other, } .map(|_| ()) } /// Probe a directory for a Make build system. /// /// Returns a Make build system if a Makefile or related build files are found. pub fn probe(path: &Path) -> Option> { if [ "Makefile", "GNUmakefile", "makefile", "Makefile.PL", "autogen.sh", "configure.ac", "configure.in", ] .iter() .any(|p| path.join(p).exists()) { return Some(Box::new(Self::new(path))); } for n in path.read_dir().unwrap() { let n = n.unwrap(); if n.file_name().to_string_lossy().ends_with(".pro") { return Some(Box::new(Self::new(path))); } } None } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingMakeTarget { fn to_dependency(&self) -> Option> { if let Some(_local_path) = self.0.strip_prefix("/<>/") { // Local file or target None } else if self.0.starts_with('/') { Some(Box::new(crate::dependencies::PathDependency::from( PathBuf::from(&self.0), ))) } else { None } } } impl BuildSystem for Make { fn name(&self) -> &str { "make" } fn dist( &self, session: &dyn crate::session::Session, installer: &dyn Installer, target_directory: &std::path::Path, _quiet: bool, ) -> Result { self.setup(session, installer, None)?; let dc = crate::dist_catcher::DistCatcher::default(&session.external_path(Path::new("."))); match self.run_make(session, vec!["dist"], None) { Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&"make: *** No rule to make target 'dist'. Stop.".to_string()) => { unimplemented!(); } Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&"make[1]: *** No rule to make target 'dist'. Stop.".to_string()) => { unimplemented!(); } Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&"ninja: error: unknown target 'dist', did you mean 'dino'?".to_string()) => { unimplemented!(); } Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&"Please try running 'make manifest' and then run 'make dist' again.".to_string()) => { session.command(vec!["make", "manifest"]).run_detecting_problems()?; session.command(vec!["make", "dist"]).run_detecting_problems().map(|_| ()) } Err(AnalyzedError::Unidentified { lines, .. }) if lines.iter().any(|l| lazy_regex::regex_is_match!(r"(Makefile|GNUmakefile|makefile):[0-9]+: \*\*\* Missing 'Make.inc' Run './configure \[options\]' and retry. Stop.", l)) => { session.command(vec!["./configure"]).run_detecting_problems()?; session.command(vec!["make", "dist"]).run_detecting_problems().map(|_| ()) } Err(AnalyzedError::Unidentified { lines, .. }) if lines.iter().any(|l| lazy_regex::regex_is_match!(r"Problem opening MANIFEST: No such file or directory at .* line [0-9]+\.", l)) => { session.command(vec!["make", "manifest"]).run_detecting_problems()?; session.command(vec!["make", "dist"]).run_detecting_problems().map(|_| ()) } other => other }?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn crate::session::Session, installer: &dyn Installer, ) -> Result<(), Error> { self.setup(session, installer, None)?; for target in ["check", "test"] { match self.run_make(session, vec![target], None) { Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&format!( "make: *** No rule to make target '{}'. Stop.", target )) => { continue; } Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains(&format!( "make[1]: *** No rule to make target '{}'. Stop.", target )) => { continue; } other => other, }?; return Ok(()); } if self.path.join("t").exists() { // See https://perlmaven.com/how-to-run-the-tests-of-a-typical-perl-module session .command(vec!["prove", "-b", "t/"]) .run_detecting_problems()?; } else { log::warn!("No test target found"); } Ok(()) } fn build( &self, session: &dyn crate::session::Session, installer: &dyn Installer, ) -> Result<(), Error> { self.setup(session, installer, None)?; let default_target = match self.kind { Kind::Qmake => None, _ => Some("all"), }; let args = if let Some(target) = default_target { vec![target] } else { vec![] }; self.run_make(session, args, None)?; Ok(()) } fn clean( &self, session: &dyn crate::session::Session, installer: &dyn Installer, ) -> Result<(), Error> { self.setup(session, installer, None)?; self.run_make(session, vec!["clean"], None)?; Ok(()) } fn install( &self, session: &dyn crate::session::Session, installer: &dyn Installer, install_target: &InstallTarget, ) -> Result<(), Error> { self.setup(session, installer, install_target.prefix.as_deref())?; self.run_make(session, vec!["install"], install_target.prefix.as_deref())?; Ok(()) } fn get_declared_dependencies( &self, session: &dyn crate::session::Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, Error, > { // TODO(jelmer): Split out the perl-specific stuff? let mut ret = vec![]; let meta_yml = self.path.join("META.yml"); if meta_yml.exists() { let mut f = std::fs::File::open(meta_yml).unwrap(); ret.extend( crate::buildsystems::perl::declared_deps_from_meta_yml(&mut f) .into_iter() .map(|d| (d.0, Box::new(d.1) as Box)), ); } let cpanfile = self.path.join("cpanfile"); if cpanfile.exists() { ret.extend( crate::buildsystems::perl::declared_deps_from_cpanfile( session, fixers.unwrap_or(&[]), ) .into_iter() .map(|d| (d.0, Box::new(d.1) as Box)), ); } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug)] /// CMake build system. /// /// Handles projects built with CMake, using out-of-source builds. pub struct CMake { path: PathBuf, builddir: String, } impl CMake { /// Create a new CMake build system with the specified path. pub fn new(path: &Path) -> Self { Self { path: path.to_path_buf(), builddir: "build".to_string(), } } fn setup( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { let build_path = self.path.join(&self.builddir); if !session.exists(&build_path) { session.mkdir(&build_path)?; } match session .command(vec!["cmake", ".", &format!("-B{}", self.builddir)]) .cwd(&self.path) .run_detecting_problems() { Ok(_) => Ok(()), Err(e) => { session.rmtree(&build_path)?; Err(e.into()) } } } /// Probe a directory for a CMake build system. /// /// Returns a CMake build system if a CMakeLists.txt file is found. pub fn probe(path: &Path) -> Option> { if path.join("CMakeLists.txt").exists() { return Some(Box::new(Self::new(path))); } None } } impl crate::buildsystem::BuildSystem for CMake { fn name(&self) -> &str { "cmake" } fn dist( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _target_directory: &std::path::Path, _quiet: bool, ) -> Result { Err(crate::buildsystem::Error::Unimplemented) } fn build( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; session .command(vec!["cmake", "--build", &self.builddir]) .cwd(&self.path) .run_detecting_problems()?; Ok(()) } fn install( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; session .command(vec!["cmake", "--install", &self.builddir]) .cwd(&self.path) .run_detecting_problems()?; Ok(()) } fn clean( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; session .command(vec![ "cmake", "--build", &self.builddir, ".", "--target", "clean", ]) .cwd(&self.path) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { // TODO(jelmer): Find a proper parser for CMakeLists.txt somewhere? use std::io::BufRead; let f = std::fs::File::open(self.path.join("CMakeLists.txt")).unwrap(); let mut ret: Vec<(DependencyCategory, Box)> = vec![]; for line in std::io::BufReader::new(f).lines() { let line = line.unwrap(); if let Some((_, m)) = lazy_regex::regex_captures!( r"cmake_minimum_required\(\s*VERSION\s+(.*)\s*\)", &line ) { ret.push(( crate::buildsystem::DependencyCategory::Build, Box::new(crate::dependencies::vague::VagueDependency::new( "CMake", Some(m), )), )); } } Ok(ret) } fn test(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { Err(Error::Unimplemented) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(test)] mod tests { use super::*; use test_log::test; #[test] fn test_exists() { let mut session = crate::session::plain::PlainSession::new(); let td = tempfile::tempdir().unwrap(); session.chdir(td.path()).unwrap(); assert!(!makefile_exists(&session)); std::fs::write(td.path().join("Makefile"), b"").unwrap(); assert!(makefile_exists(&session)); } #[test] fn test_simple() { let mut session = crate::session::plain::PlainSession::new(); let td = tempfile::tempdir().unwrap(); session.chdir(td.path()).unwrap(); std::fs::write( td.path().join("Makefile"), r###" all: test: check: "###, ) .unwrap(); let make = Make::probe(td.path()).expect("make"); make.build(&session, &crate::installer::NullInstaller) .unwrap(); std::mem::drop(td); } } ognibuild-0.2.6/src/buildsystems/meson.rs000064400000000000000000001271401046102023000166170ustar 00000000000000use crate::analyze::AnalyzedError; use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; use crate::dependencies::vague::VagueDependency; use crate::dependency::Dependency; use crate::dist_catcher::DistCatcher; use crate::fix_build::BuildFixer; use crate::installer::Error as InstallerError; use crate::session::Session; use std::path::{Path, PathBuf}; #[derive(Debug)] /// Meson build system. /// /// Handles projects built with Meson and Ninja. pub struct Meson { #[allow(dead_code)] path: PathBuf, } #[derive(Debug, serde::Deserialize)] #[allow(dead_code)] struct MesonDependency { pub name: String, #[serde(deserialize_with = "version_as_vec")] pub version: Vec, #[serde(default)] pub required: bool, #[serde(default)] pub has_fallback: bool, #[serde(default)] pub conditional: bool, } // Helper to handle both string and vec for version field fn version_as_vec<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { use serde::Deserialize; #[derive(Deserialize)] #[serde(untagged)] enum StringOrVec { String(String), Vec(Vec), } match StringOrVec::deserialize(deserializer)? { StringOrVec::String(s) => Ok(if s.is_empty() { vec![] } else { vec![s] }), StringOrVec::Vec(v) => Ok(v), } } #[derive(Debug, serde::Deserialize)] #[allow(dead_code)] struct MesonTarget { r#type: String, installed: bool, filename: Vec, } impl Meson { /// Create a new Meson build system with the specified path. pub fn new(path: &Path) -> Self { Self { path: path.to_path_buf(), } } fn setup(&self, session: &dyn Session) -> Result<(), Error> { // Get the project directory (parent of meson.build) let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); let build_dir = project_dir.join("build"); if !session.exists(&build_dir) { session.mkdir(&build_dir).unwrap(); } session .command(vec!["meson", "setup", "build"]) .cwd(project_dir) .quiet(true) .run_detecting_problems()?; Ok(()) } fn introspect serde::Deserialize<'a>>( &self, session: &dyn Session, fixers: Option<&[&dyn BuildFixer]>, args: &[&str], ) -> Result { // Get the project directory (parent of meson.build) let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); // Check if we have a configured build directory let build_dir = project_dir.join("build"); let use_build_dir = session.exists(&build_dir) && session.exists(&build_dir.join("build.ninja")); let ret = if use_build_dir { // Use configured build directory let build_dir_str = build_dir.to_string_lossy(); let introspect_args = [&["meson", "introspect", &build_dir_str], args].concat(); if let Some(fixers) = fixers { session .command(introspect_args) .cwd(project_dir) .quiet(true) .run_fixing_problems::<_, Error>(fixers) .unwrap() } else { session .command(introspect_args) .cwd(project_dir) .quiet(true) .run_detecting_problems()? } } else { // For unconfigured projects, set up a temporary build and introspect from there self.setup_temp_build_for_introspect(session, fixers, args)? }; let text = ret.concat(); Ok(serde_json::from_str(&text).unwrap()) } /// Set up a temporary build directory for introspection of unconfigured projects fn setup_temp_build_for_introspect( &self, session: &dyn Session, fixers: Option<&[&dyn BuildFixer]>, args: &[&str], ) -> Result, InstallerError> { let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); // Create a temporary build directory let temp_build_dir = project_dir.join(".ognibuild-temp-build"); // Clean up any existing temp build if session.exists(&temp_build_dir) { session.rmtree(&temp_build_dir).ok(); } session.mkdir(&temp_build_dir).map_err(|e| { InstallerError::Other(format!("Failed to create temp build dir: {}", e)) })?; // Set up the build directory let temp_build_str = temp_build_dir.to_string_lossy(); let setup_result = session .command(vec!["meson", "setup", &temp_build_str]) .cwd(project_dir) .quiet(true) .run_detecting_problems(); match setup_result { Ok(_) => { // Now introspect the configured build directory let temp_build_str = temp_build_dir.to_string_lossy(); let introspect_args = [&["meson", "introspect", &temp_build_str], args].concat(); let result = if let Some(fixers) = fixers { session .command(introspect_args) .cwd(project_dir) .quiet(true) .run_fixing_problems::<_, Error>(fixers) .unwrap() } else { session .command(introspect_args) .cwd(project_dir) .quiet(true) .run_detecting_problems()? }; // Clean up temp build directory session.rmtree(&temp_build_dir).ok(); Ok(result) } Err(e) => { // Clean up temp build directory on failure session.rmtree(&temp_build_dir).ok(); Err(e.into()) } } } /// Probe a directory for a Meson build system. /// /// Returns a Meson build system if a meson.build file is found. pub fn probe(path: &Path) -> Option> { let path = path.join("meson.build"); if path.exists() { log::debug!("Found meson.build, assuming meson package."); Some(Box::new(Self::new(&path))) } else { None } } } impl BuildSystem for Meson { fn name(&self) -> &str { "meson" } fn dist( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, target_directory: &Path, _quiet: bool, ) -> Result { self.setup(session)?; let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); let dc = DistCatcher::new(vec![ session.external_path(&project_dir.join("build/meson-dist")) ]); match session .command(vec!["ninja", "-C", "build", "dist"]) .cwd(project_dir) .quiet(true) .run_detecting_problems() { Ok(_) => {} Err(AnalyzedError::Unidentified { lines, .. }) if lines.contains( &"ninja: error: unknown target 'dist', did you mean 'dino'?".to_string(), ) => { unimplemented!(); } Err(e) => return Err(e.into()), } Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { self.setup(session)?; let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); session .command(vec!["ninja", "-C", "build", "test"]) .cwd(project_dir) .quiet(true) .run_detecting_problems()?; Ok(()) } fn build( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { self.setup(session)?; let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); session .command(vec!["ninja", "-C", "build"]) .cwd(project_dir) .quiet(true) .run_detecting_problems()?; Ok(()) } fn clean( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { self.setup(session)?; let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); session .command(vec!["ninja", "-C", "build", "clean"]) .cwd(project_dir) .quiet(true) .run_detecting_problems()?; Ok(()) } fn install( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), Error> { self.setup(session)?; let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); session .command(vec!["ninja", "-C", "build", "install"]) .cwd(project_dir) .quiet(true) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { let mut ret: Vec<(DependencyCategory, Box)> = Vec::new(); // Use --scan-dependencies directly on the meson.build file // This is the correct usage - scan-dependencies works on source files, not build dirs // Get the project directory (parent of meson.build) let project_dir = self .path .parent() .expect("meson.build should have a parent directory"); let meson_file_str = self.path.to_string_lossy(); let scan_args = vec![ "meson", "introspect", "--scan-dependencies", &meson_file_str, ]; let output = if let Some(fixers) = fixers { session .command(scan_args) .cwd(project_dir) .quiet(true) .run_fixing_problems::<_, Error>(fixers) .map_err(|e| { InstallerError::Other(format!("Failed to run scan-dependencies: {:?}", e)) })? } else { session .command(scan_args) .cwd(project_dir) .quiet(true) .run_detecting_problems() .map_err(|e| { InstallerError::Other(format!("Failed to run scan-dependencies: {:?}", e)) })? }; let text = output.concat(); let resp: Vec = serde_json::from_str(&text).map_err(|e| { InstallerError::Other(format!("Failed to parse scan-dependencies JSON: {}", e)) })?; for entry in resp { let mut minimum_version = None; if entry.version.len() == 1 { if let Some(rest) = entry.version[0].strip_prefix(">=") { minimum_version = Some(rest.trim().to_string()); } } else if entry.version.len() > 1 { log::warn!("Unable to parse version constraints: {:?}", entry.version); } // TODO(jelmer): Include entry['required'] ret.push(( DependencyCategory::Universal, Box::new(VagueDependency { name: entry.name.to_string(), minimum_version, }), )); } Ok(ret) } fn get_declared_outputs( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { let mut ret: Vec> = Vec::new(); let resp = self.introspect::>(session, fixers, &["--targets"])?; for entry in resp { if !entry.installed { continue; } if entry.r#type == "executable" { for p in entry.filename { ret.push(Box::new(crate::output::BinaryOutput::new( p.file_name().unwrap().to_str().unwrap(), ))); } } // TODO(jelmer): Handle other types } Ok(ret) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(test)] mod tests { use super::*; use crate::buildsystem::detect_buildsystems; use crate::installer::NullInstaller; use crate::session::plain::PlainSession; use std::fs; use tempfile::TempDir; /// Helper function to create a minimal meson.build file fn create_meson_project(dir: &Path) -> std::io::Result<()> { fs::write( dir.join("meson.build"), r#"project('test-project', 'c', version : '1.0.0', license : 'MIT', default_options : ['warning_level=2']) # A simple dependency for testing glib_dep = dependency('glib-2.0', required: false) # Define a simple executable executable('test-app', 'main.c', dependencies : glib_dep, install : true) "#, )?; // Create a simple C source file fs::write( dir.join("main.c"), r#"#include int main(int argc, char *argv[]) { printf("Hello from Meson test!\n"); return 0; } "#, )?; Ok(()) } #[test] fn test_meson_detection() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Should not detect Meson without meson.build let buildsystems = detect_buildsystems(project_dir); assert!( !buildsystems.iter().any(|bs| bs.name() == "meson"), "Should not detect Meson without meson.build" ); // Create meson.build create_meson_project(project_dir).unwrap(); // Should detect Meson with meson.build let buildsystems = detect_buildsystems(project_dir); assert!( buildsystems.iter().any(|bs| bs.name() == "meson"), "Should detect Meson with meson.build" ); // Verify it's the first detected build system (highest priority) if !buildsystems.is_empty() { let first = &buildsystems[0]; assert_eq!( first.name(), "meson", "Meson should be the primary build system" ); } } #[test] fn test_meson_introspect_with_different_cwd() { // This test verifies the fix for the cwd issue let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("my-project"); fs::create_dir(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); // Create a session with a different working directory let mut session = PlainSession::new(); // Set the session's cwd to the temp directory, NOT the project directory session.chdir(temp_dir.path()).unwrap(); // Detect the buildsystem let buildsystems = detect_buildsystems(&project_dir); assert!(!buildsystems.is_empty(), "Should detect Meson buildsystem"); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Try to get dependencies - this should work even with different cwd match meson.get_declared_dependencies(&session, None) { Ok(deps) => { // Check that we found the glib dependency let _has_glib = deps .iter() .any(|(_, dep)| dep.family() == "glib" || dep.family() == "pkg-config"); log::debug!("Found {} dependencies", deps.len()); if !deps.is_empty() { log::debug!("Dependencies: {:?}", deps); } } Err(e) => { // It's okay if meson isn't installed, but the error should NOT be // about missing meson.build file let error_str = format!("{:?}", e); assert!( !error_str.contains("Missing Meson file"), "Should not fail with 'Missing Meson file' error: {}", error_str ); assert!( !error_str.contains("./meson.build"), "Should not reference relative path './meson.build': {}", error_str ); } } } #[test] fn test_meson_with_nested_project_structure() { // Test that Meson works correctly with nested directory structures let temp_dir = TempDir::new().unwrap(); let workspace_dir = temp_dir.path().join("workspace"); let project_dir = workspace_dir.join("subdir").join("project"); fs::create_dir_all(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); // Create session at workspace root let mut session = PlainSession::new(); session.chdir(&workspace_dir).unwrap(); // Detect buildsystem in nested project let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // This should work despite the session being 2 levels up from the project match meson.get_declared_dependencies(&session, None) { Ok(_) => { // Success - the cwd handling is working correctly } Err(e) => { let error_str = format!("{:?}", e); // Check it's not a path-related error assert!( !error_str.contains("Missing Meson file") && !error_str.contains("./meson.build"), "Path handling error: {}", error_str ); } } } #[test] #[ignore] // This test requires meson to be installed fn test_meson_build_operations() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("build-test"); fs::create_dir(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); // Create session in a different directory let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test build operation let installer = NullInstaller; // Build should work with proper cwd handling match meson.build(&session, &installer) { Ok(_) => log::debug!("Build succeeded"), Err(e) => { let error_str = format!("{:?}", e); assert!( !error_str.contains("./meson.build"), "Should not have path errors: {}", error_str ); } } // Test clean operation match meson.clean(&session, &installer) { Ok(_) => log::debug!("Clean succeeded"), Err(e) => { let error_str = format!("{:?}", e); assert!( !error_str.contains("./meson.build"), "Should not have path errors: {}", error_str ); } } } #[test] fn test_meson_project_with_subprojects() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Create main project fs::write( project_dir.join("meson.build"), r#"project('main-project', 'c', version : '2.0.0', license : 'GPL-3.0') # Subproject (would normally be in subprojects/ dir) # This tests that we handle the main meson.build correctly executable('main-app', 'main.c') "#, ) .unwrap(); fs::write(project_dir.join("main.c"), r#"int main() { return 0; }"#).unwrap(); let buildsystems = detect_buildsystems(project_dir); assert!( buildsystems.iter().any(|bs| bs.name() == "meson"), "Should detect Meson for project with subprojects structure" ); } #[test] fn test_meson_handles_symlinks() { #[cfg(unix)] { use std::os::unix::fs::symlink; let temp_dir = TempDir::new().unwrap(); let real_project = temp_dir.path().join("real-project"); let link_project = temp_dir.path().join("link-project"); fs::create_dir(&real_project).unwrap(); create_meson_project(&real_project).unwrap(); // Create a symlink to the project symlink(&real_project, &link_project).unwrap(); // Should detect Meson through the symlink let buildsystems = detect_buildsystems(&link_project); assert!( buildsystems.iter().any(|bs| bs.name() == "meson"), "Should detect Meson through symlink" ); // Create session in temp dir let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); // Operations should work through the symlink let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); match meson.get_declared_dependencies(&session, None) { Ok(_) => { // Success - symlink handling works } Err(e) => { let error_str = format!("{:?}", e); assert!( !error_str.contains("Missing Meson file"), "Should handle symlinks correctly: {}", error_str ); } } } } #[test] fn test_meson_introspect_unconfigured_project() { // This test specifically verifies the fix for introspecting unconfigured projects let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("unconfigured-project"); fs::create_dir(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); // Create session in a different directory to test path handling let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); // Detect the buildsystem let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // This should NOT fail with "Current directory is not a meson build directory" match meson.get_declared_dependencies(&session, None) { Ok(deps) => { log::debug!( "Successfully introspected unconfigured project with {} dependencies", deps.len() ); } Err(e) => { let error_str = format!("{:?}", e); // The specific error from the bug report should not occur assert!( !error_str.contains("Current directory is not a meson build directory"), "Should not fail with build directory error for unconfigured projects: {}", error_str ); assert!( !error_str.contains("Please specify a valid build dir"), "Should not fail asking for build dir when introspecting source: {}", error_str ); // Other errors (like meson not installed) are acceptable log::debug!( "Got acceptable error (likely meson not installed): {}", error_str ); } } } #[test] fn test_meson_introspect_scan_dependencies() { // This test verifies that --scan-dependencies works correctly on meson.build files let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("scan-deps-test"); fs::create_dir(&project_dir).unwrap(); // Create a project with multiple dependencies to test scan-dependencies fs::write( project_dir.join("meson.build"), r#"project('scan-deps-test', 'c', version : '1.0.0', license : 'MIT') # Test various types of dependencies glib_dep = dependency('glib-2.0', required: false) threads_dep = dependency('threads', required: false) math_dep = declare_dependency( dependencies: meson.get_compiler('c').find_library('m', required: false) ) executable('test-app', 'main.c', dependencies : [glib_dep, threads_dep, math_dep]) "#, ) .unwrap(); fs::write( project_dir.join("main.c"), r#"#include #include int main() { printf("Result: %f\n", sqrt(16.0)); return 0; }"#, ) .unwrap(); let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test dependency introspection using --scan-dependencies on meson.build match meson.get_declared_dependencies(&session, None) { Ok(deps) => { log::debug!("Found {} dependencies", deps.len()); for (category, dep) in &deps { let min_ver = if let Some(vague_dep) = dep.as_any().downcast_ref::() { vague_dep.minimum_version.as_deref().unwrap_or("any") } else { "any" }; log::debug!(" {:?}: {} ({})", category, dep.family(), min_ver); } // Should find some dependencies from our test project // Note: Exact dependencies depend on system availability // We expect to find at least glib-2.0 and threads if they're available } Err(e) => { let error_str = format!("{:?}", e); // Should NOT fail with "No command specified" since we fixed the root cause assert!( !error_str.contains("No command specified"), "Should not fail with 'No command specified' after fixing scan-dependencies usage: {}", error_str ); // Other errors (meson not installed, etc.) are acceptable log::debug!("Got acceptable error: {}", error_str); } } } #[test] fn test_meson_introspect_targets() { // Test that target introspection works correctly let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("targets-test"); fs::create_dir(&project_dir).unwrap(); fs::write( project_dir.join("meson.build"), r#"project('targets-test', 'c', version : '1.0.0') # Test various target types executable('main-app', 'main.c', install : true) executable('test-util', 'test.c', install : false) static_library('helper', 'helper.c', install : false) "#, ) .unwrap(); fs::write(project_dir.join("main.c"), "int main() { return 0; }").unwrap(); fs::write(project_dir.join("test.c"), "int main() { return 1; }").unwrap(); fs::write(project_dir.join("helper.c"), "void helper() {}").unwrap(); let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test output introspection match meson.get_declared_outputs(&session, None) { Ok(outputs) => { log::debug!("Found {} outputs", outputs.len()); for output in &outputs { log::debug!(" Output: {:?}", output); } // Should find at least the installed executable // Exact outputs depend on meson version and system // (outputs.len() is always >= 0 for Vec, so this is just a documentation comment) } Err(e) => { let error_str = format!("{:?}", e); // Check it's not a path or command error we've been fixing assert!( !error_str.contains("Missing Meson file") && !error_str.contains("./meson.build") && !error_str.contains("No command specified"), "Should not have known path/command errors: {}", error_str ); log::debug!("Got acceptable error: {}", error_str); } } } #[test] fn test_meson_introspect_complex_project() { // Test introspection on a more complex project structure let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("complex-test"); fs::create_dir_all(project_dir.join("src")).unwrap(); fs::create_dir_all(project_dir.join("include")).unwrap(); fs::write( project_dir.join("meson.build"), r#"project('complex-test', 'c', version : '2.1.0', license : 'GPL-2.0', default_options : [ 'warning_level=3', 'werror=true' ]) # Include directories inc = include_directories('include') # Dependencies with version constraints json_dep = dependency('json-c', version: '>=0.13', required: false) curl_dep = dependency('libcurl', version: '>=7.60', required: false) zlib_dep = dependency('zlib', required: false) # Subdirectory subdir('src') # Main executable executable('complex-app', sources: ['main.c', src_files], include_directories: inc, dependencies: [json_dep, curl_dep, zlib_dep], install: true) "#, ) .unwrap(); fs::write( project_dir.join("src/meson.build"), "src_files = files('utils.c', 'parser.c')", ) .unwrap(); fs::write( project_dir.join("main.c"), "#include \nint main() { printf(\"Complex app\\n\"); return 0; }", ) .unwrap(); fs::write(project_dir.join("src/utils.c"), "void utils_init() {}").unwrap(); fs::write(project_dir.join("src/parser.c"), "void parse() {}").unwrap(); fs::write( project_dir.join("include/common.h"), "#pragma once\nvoid utils_init();", ) .unwrap(); let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test that complex projects work with our introspection match meson.get_declared_dependencies(&session, None) { Ok(deps) => { log::debug!("Complex project dependencies: {} found", deps.len()); // Look for dependencies with version constraints let versioned_deps: Vec<_> = deps .iter() .filter(|(_, dep)| { if let Some(vague_dep) = dep.as_any().downcast_ref::() { vague_dep.minimum_version.is_some() } else { false } }) .collect(); if !versioned_deps.is_empty() { log::debug!("Dependencies with version constraints:"); for (cat, dep) in versioned_deps { let min_ver = if let Some(vague_dep) = dep.as_any().downcast_ref::() { vague_dep.minimum_version.as_deref().unwrap_or("any") } else { "any" }; log::debug!(" {:?}: {} >= {}", cat, dep.family(), min_ver); } } } Err(e) => { let error_str = format!("{:?}", e); assert!( !error_str.contains("No command specified") && !error_str.contains("Missing Meson file") && !error_str.contains("./meson.build"), "Should not have known errors on complex projects: {}", error_str ); log::debug!("Got acceptable error on complex project: {}", error_str); } } // Also test outputs for complex project match meson.get_declared_outputs(&session, None) { Ok(outputs) => { log::debug!("Complex project outputs: {} found", outputs.len()); } Err(e) => { let error_str = format!("{:?}", e); assert!( !error_str.contains("No command specified"), "Should not have command errors: {}", error_str ); } } } #[test] fn test_meson_setup_command_with_source_and_build_dirs() { // Test the fix for meson setup requiring both source and build directory arguments let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("setup-test"); fs::create_dir(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let meson = Meson::new(&project_dir.join("meson.build")); // Test the temporary build setup (this tests the fixed setup command) let result = meson.setup_temp_build_for_introspect(&session, None, &["--projectinfo"]); match result { Ok(_) => { log::debug!("Setup with source and build dirs succeeded"); // Verify temp build dir was cleaned up let temp_build = project_dir.join(".ognibuild-temp-build"); assert!( !session.exists(&temp_build), "Temporary build directory should be cleaned up" ); } Err(e) => { let error_str = format!("{:?}", e); // Should NOT fail with "No command specified" from setup assert!( !error_str.contains("No command specified"), "Setup should not fail with 'No command specified': {}", error_str ); log::debug!("Got acceptable setup error: {}", error_str); } } } #[test] fn test_meson_commands_work_from_different_cwd_regression() { // Regression test for the bug where meson commands fail when the current // working directory is not the project directory. // // Bug details: The Meson struct stores the path to meson.build file, but when // running meson commands, it wasn't changing the working directory to the project // directory. This caused commands like "meson setup" and "meson introspect" to fail // when run from a different directory. let temp_dir = TempDir::new().unwrap(); // Create a deeply nested project structure to test path handling let workspace = temp_dir.path().join("workspace"); let other_dir = temp_dir.path().join("other_directory"); let project_dir = workspace.join("projects").join("my-meson-project"); fs::create_dir_all(&workspace).unwrap(); fs::create_dir_all(&other_dir).unwrap(); fs::create_dir_all(&project_dir).unwrap(); create_meson_project(&project_dir).unwrap(); // Create a session in a completely different directory let mut session = PlainSession::new(); // IMPORTANT: Set working directory to somewhere completely unrelated to the project session.chdir(&other_dir).unwrap(); // Verify we're in a different directory assert_ne!(session.pwd(), project_dir); assert_ne!(session.pwd().parent(), Some(project_dir.as_path())); // Detect the buildsystem from the project directory let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test 1: get_declared_dependencies should work even from different cwd match meson.get_declared_dependencies(&session, None) { Ok(deps) => { // Should find the glib dependency we declared let has_glib = deps .iter() .any(|(_, dep)| dep.family() == "glib-2.0" || dep.family().contains("glib")); // Success - the fix is working assert!(!deps.is_empty() || has_glib, "Should find dependencies"); } Err(e) => { let error_str = format!("{:?}", e); // The bug would cause errors like: // - "ERROR: Neither source directory './meson.build' nor build directory './' contain a meson.build file" // - "ERROR: Missing Meson file in './meson.build'" assert!( !error_str.contains("Missing Meson file") && !error_str.contains("nor build directory") && !error_str.contains("contain a meson.build"), "BUG REPRODUCED: Meson command failed due to working directory issue: {}", error_str ); } } // Test 2: get_declared_outputs should also work from different cwd match meson.get_declared_outputs(&session, None) { Ok(_outputs) => { // Success - the fix is working } Err(e) => { let error_str = format!("{:?}", e); // Should not fail with path-related errors assert!( !error_str.contains("Missing Meson file") && !error_str.contains("nor build directory") && !error_str.contains("contain a meson.build"), "BUG REPRODUCED: Meson introspect failed due to working directory issue: {}", error_str ); } } // Test 3: Verify setup works from different cwd (if meson is available) let installer = NullInstaller; match meson.build(&session, &installer) { Ok(_) => { // Success - build worked from different cwd } Err(e) => { let error_str = format!("{:?}", e); // The bug would cause "meson setup build" to fail because it runs in wrong dir assert!( !error_str.contains("source directory") && !error_str.contains("meson.build"), "BUG REPRODUCED: Meson setup failed due to working directory issue: {}", error_str ); } } // Verify session is still in the other directory (commands shouldn't change it) // Canonicalize both paths to handle macOS /var -> /private/var symlink resolution let actual_pwd = session .pwd() .canonicalize() .unwrap_or_else(|_| session.pwd().to_path_buf()); let expected_pwd = other_dir .canonicalize() .unwrap_or_else(|_| other_dir.clone()); assert_eq!( actual_pwd, expected_pwd, "Session cwd should not have changed" ); } #[test] fn test_meson_error_handling_robustness() { // Test that our error handling is robust against various meson issues let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("error-test"); fs::create_dir(&project_dir).unwrap(); // Create a potentially problematic meson.build fs::write( project_dir.join("meson.build"), r#"project('error-test', 'c', version : '1.0.0') # Dependency that might not exist missing_dep = dependency('this-probably-does-not-exist-anywhere', required: false) # Still define something so meson doesn't completely fail executable('test', 'main.c', dependencies: missing_dep) "#, ) .unwrap(); fs::write(project_dir.join("main.c"), "int main() { return 0; }").unwrap(); let mut session = PlainSession::new(); session.chdir(temp_dir.path()).unwrap(); let buildsystems = detect_buildsystems(&project_dir); let meson = buildsystems .iter() .find(|bs| bs.name() == "meson") .expect("Should find Meson buildsystem"); // Test that we handle various error conditions gracefully match meson.get_declared_dependencies(&session, None) { Ok(deps) => { log::debug!( "Handled potentially problematic project: {} deps", deps.len() ); // Should be able to find at least the declared dependency // (even if it's not available on the system) let missing_deps: Vec<_> = deps .iter() .filter(|(_, dep)| dep.family().contains("this-probably-does-not-exist")) .collect(); if !missing_deps.is_empty() { log::debug!( "Found declared but unavailable dependency: {:?}", missing_deps[0].1.family() ); } } Err(e) => { let error_str = format!("{:?}", e); // Verify we don't get the specific errors we've been fixing assert!( !error_str.contains("No command specified"), "Should not get 'No command specified' after fixing scan-dependencies: {}", error_str ); log::debug!("Handled error case appropriately: {}", error_str); } } } } ognibuild-0.2.6/src/buildsystems/mod.rs000064400000000000000000000014271046102023000162540ustar 00000000000000/// Bazel build system implementation. pub mod bazel; /// GNOME build system implementation. pub mod gnome; /// Go build system implementation. pub mod go; /// Haskell build system implementation. pub mod haskell; /// Java build system implementation. pub mod java; /// Make build system implementation. pub mod make; /// Meson build system implementation. pub mod meson; /// Node.js build system implementation. pub mod node; /// Octave build system implementation. pub mod octave; /// Perl build system implementation. pub mod perl; /// Python build system implementation. pub mod python; /// R build system implementation. pub mod r; /// Ruby build system implementation. pub mod ruby; /// Rust build system implementation. pub mod rust; /// Waf build system implementation. pub mod waf; ognibuild-0.2.6/src/buildsystems/node.rs000064400000000000000000000214151046102023000164210ustar 00000000000000use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; use crate::dependencies::node::NodePackageDependency; use crate::dependencies::BinaryDependency; use crate::dependency::Dependency; use crate::installer::{Error as InstallerError, InstallationScope, Installer}; use crate::session::Session; use serde::Deserialize; use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug)] #[allow(dead_code)] /// Node.js build system. /// /// Handles Node.js projects with a package.json file. pub struct Node { path: PathBuf, package: NodePackage, } #[derive(Debug, Deserialize)] struct NodePackage { #[serde(default)] dependencies: HashMap, #[serde(rename = "devDependencies", default)] dev_dependencies: HashMap, #[serde(default)] scripts: HashMap, } impl Node { /// Create a new Node build system with the specified path to package.json. pub fn new(path: PathBuf) -> Result> { let package_path = path.join("package.json"); let package_content = std::fs::read_to_string(&package_path)?; let package: NodePackage = serde_json::from_str(&package_content)?; Ok(Self { path, package }) } fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { let binary_req = BinaryDependency::new("npm"); if !binary_req.present(session) { installer.install(&binary_req, InstallationScope::Global)?; } Ok(()) } /// Probe a directory for a Node.js build system. /// /// Returns a Node build system if a package.json file is found. pub fn probe(path: &std::path::Path) -> Option> { let package_json_path = path.join("package.json"); if package_json_path.exists() { log::debug!("Found package.json, attempting to parse as node package."); match Self::new(path.to_path_buf()) { Ok(node_system) => return Some(Box::new(node_system)), Err(e) => { log::debug!("Failed to parse package.json: {}", e); return None; } } } None } } impl BuildSystem for Node { fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result)>, Error> { let mut dependencies: Vec<(DependencyCategory, Box)> = vec![]; for (name, _version) in self.package.dependencies.iter() { // TODO(jelmer): Look at version dependencies.push(( DependencyCategory::Universal, Box::new(NodePackageDependency::new(name)), )); } for (name, _version) in self.package.dev_dependencies.iter() { // TODO(jelmer): Look at version dependencies.push(( DependencyCategory::Build, Box::new(NodePackageDependency::new(name)), )); } Ok(dependencies) } fn name(&self) -> &str { "node" } fn dist( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, target_directory: &std::path::Path, quiet: bool, ) -> Result { self.setup(session, installer)?; let dc = crate::dist_catcher::DistCatcher::new(vec![ session.external_path(std::path::Path::new(".")) ]); session .command(vec!["npm", "pack"]) .quiet(quiet) .run_detecting_problems()?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; if let Some(test_script) = self.package.scripts.get("test") { session .command(vec!["bash", "-c", test_script]) .run_detecting_problems()?; } else { log::info!("No test command defined in package.json"); } Ok(()) } fn build( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; if let Some(build_script) = self.package.scripts.get("build") { session .command(vec!["bash", "-c", build_script]) .run_detecting_problems()?; } else { log::info!("No build command defined in package.json"); } Ok(()) } fn clean( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, installer)?; if let Some(clean_script) = self.package.scripts.get("clean") { session .command(vec!["bash", "-c", clean_script]) .run_detecting_problems()?; } else { log::info!("No clean command defined in package.json"); } Ok(()) } fn install( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_node_detection_minimal_package() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Create minimal package.json std::fs::write( project_dir.join("package.json"), r#"{"name": "test-package", "version": "1.0.0"}"#, ) .unwrap(); let result = Node::probe(project_dir); match result { Some(bs) => { assert_eq!(bs.name(), "node"); } None => { panic!("Should detect node buildsystem with minimal package.json"); } } } #[test] fn test_node_detection_complex_package() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Create package.json with dependencies std::fs::write( project_dir.join("package.json"), r#"{ "name": "test-nodejs-package", "version": "1.2.3", "dependencies": { "express": "^4.18.0", "lodash": "^4.17.21" }, "devDependencies": { "jest": "^28.0.0" }, "scripts": { "test": "jest", "build": "webpack" } }"#, ) .unwrap(); let result = Node::probe(project_dir); assert!( result.is_some(), "Should detect node buildsystem with complex package.json" ); } #[test] fn test_detect_buildsystems_integration() { use crate::buildsystem::detect_buildsystems; let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Create minimal package.json std::fs::write( project_dir.join("package.json"), r#"{"name": "test-package", "version": "1.0.0"}"#, ) .unwrap(); let buildsystems = detect_buildsystems(project_dir); assert!( !buildsystems.is_empty(), "Should detect at least one buildsystem" ); let has_node = buildsystems.iter().any(|bs| bs.name() == "node"); assert!( has_node, "Should detect node buildsystem. Found: {:?}", buildsystems.iter().map(|bs| bs.name()).collect::>() ); } #[test] fn test_scoped_package_detection() { let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path(); // Create package.json with scoped name std::fs::write( project_dir.join("package.json"), r#"{"name": "@myorg/test-package", "version": "1.0.0"}"#, ) .unwrap(); let result = Node::probe(project_dir); assert!( result.is_some(), "Should detect node buildsystem with scoped package name" ); } } ognibuild-0.2.6/src/buildsystems/octave.rs000064400000000000000000000233621046102023000167600ustar 00000000000000//! Support for GNU Octave build systems. //! //! This module provides functionality for building, testing, and installing //! GNU Octave packages. use crate::buildsystem::{BuildSystem, Error}; use crate::dependencies::octave::OctavePackageDependency; use crate::dependency::Dependency; use crate::session::Session; use std::path::{Path, PathBuf}; #[derive(Debug)] /// GNU Octave build system. /// /// This build system handles GNU Octave package builds and installations. pub struct Octave { path: PathBuf, } #[allow(dead_code)] /// Version information for an Octave package. /// /// Represents a semantic version with major, minor, and patch components. pub struct Version { major: u32, minor: u32, patch: u32, } impl std::str::FromStr for Version { type Err = std::num::ParseIntError; fn from_str(s: &str) -> Result { let mut parts = s.splitn(3, '.'); let major = parts.next().unwrap().parse()?; let minor = parts.next().unwrap().parse()?; let patch = parts.next().unwrap().parse()?; Ok(Self { major, minor, patch, }) } } #[derive(Default)] /// Metadata for an Octave package. /// /// Contains the package information from the DESCRIPTION file, including /// name, version, dependencies, and other metadata. pub struct Description { name: Option, version: Option, description: Option, date: Option, author: Option, maintainer: Option, title: Option, categories: Option>, problems: Option>, url: Option>, depends: Option>, license: Option, system_requirements: Option>, build_requires: Option>, } fn read_description_fields( r: R, ) -> Result, std::io::Error> { let mut fields = Vec::new(); let mut lines = r.lines(); let line = lines.next().unwrap()?; loop { if line.is_empty() { break; } if line.starts_with('#') { continue; } let mut parts = line.splitn(2, ": "); let key = parts.next().unwrap().to_string(); let mut value = parts.next().unwrap().to_string(); for line in lines.by_ref() { let line = line?; if line.starts_with(' ') { value.push_str(line.trim_start()); } else if line.starts_with('#') { } else { fields.push((key, value)); break; } } } Ok(fields) } /// Read an Octave package description from a reader. /// /// Parses the DESCRIPTION file format used by Octave packages. /// /// # Arguments /// * `r` - A BufRead implementation containing the DESCRIPTION file contents /// /// # Returns /// The parsed Description struct or an IO error pub fn read_description(r: R) -> Result { let mut description = Description::default(); for (key, value) in read_description_fields(r)?.into_iter() { match key.as_str() { "Package" => description.name = Some(value), "Version" => description.version = Some(value.parse().unwrap()), "Description" => description.description = Some(value), "Date" => description.date = Some(value), "Author" => description.author = Some(value), "Maintainer" => description.maintainer = Some(value), "Title" => description.title = Some(value), "Categories" => { description.categories = Some(value.split(',').map(|s| s.trim().to_string()).collect()) } "Problems" => { description.problems = Some(value.split(',').map(|s| s.trim().to_string()).collect()) } "URL" => { description.url = Some( value .split(',') .map(|s| s.trim().to_string()) .map(|s| s.parse().unwrap()) .collect::>(), ) } "Depends" => { description.depends = Some(value.split(',').map(|s| s.trim().to_string()).collect()) } "License" => description.license = Some(value), "SystemRequirements" => { description.system_requirements = Some(value.split(',').map(|s| s.trim().to_string()).collect()) } "BuildRequires" => { description.build_requires = Some(value.split(',').map(|s| s.trim().to_string()).collect()) } name => log::warn!("Unknown field in DESCRIPTION: {}", name), } } Ok(description) } impl Octave { /// Create a new Octave build system with the specified path. /// /// # Arguments /// * `path` - The path to the Octave package directory /// /// # Returns /// A new Octave build system instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Check if an Octave package exists at the given path. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// `true` if an Octave package exists at the path, `false` otherwise pub fn exists(path: &Path) -> bool { if path.join("DESCRIPTION").exists() { return false; } // Urgh, isn't there a better way to see if this is an octave package? for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); if entry.file_name().to_string_lossy().ends_with(".m") { return true; } if !entry.file_type().unwrap().is_dir() { continue; } match entry.path().read_dir() { Ok(subentries) => { for subentry in subentries { let subentry = subentry.unwrap(); if subentry.file_name().to_string_lossy().ends_with(".m") { return true; } } } Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { log::debug!( "Permission denied while reading directory: {}", entry.path().display() ); } Err(e) => { panic!("Error reading directory: {}", e); } } } false } /// Probe a directory for an Octave build system. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// An Octave build system if one exists at the path, `None` otherwise pub fn probe(path: &Path) -> Option> { if Self::exists(path) { log::debug!("Found DESCRIPTION, assuming octave package."); Some(Box::new(Self::new(path.to_path_buf()))) } else { None } } } impl BuildSystem for Octave { fn name(&self) -> &str { "octave" } fn dist( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, _target_directory: &Path, _quiet: bool, ) -> Result { Err(Error::Unimplemented) } fn test( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn build( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn clean( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn install( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<(crate::buildsystem::DependencyCategory, Box)>, crate::buildsystem::Error, > { let f = std::fs::File::open(self.path.join("DESCRIPTION")).unwrap(); let description = read_description(std::io::BufReader::new(f)).unwrap(); let mut ret: Vec<(crate::buildsystem::DependencyCategory, Box)> = Vec::new(); for depend in description.depends.unwrap_or_default() { let d: OctavePackageDependency = depend.parse().unwrap(); ret.push((crate::buildsystem::DependencyCategory::Build, Box::new(d))); } for build_require in description.build_requires.unwrap_or_default() { let d: OctavePackageDependency = build_require.parse().unwrap(); ret.push((crate::buildsystem::DependencyCategory::Build, Box::new(d))); } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/perl.rs000064400000000000000000000442611046102023000164420ustar 00000000000000//! Support for Perl build systems. //! //! This module provides functionality for building, testing, and installing //! Perl packages using various build systems such as DistZilla, Makefile.PL, //! and ExtUtils::MakeMaker. use crate::analyze::AnalyzedError; use crate::buildsystem::{guaranteed_which, BuildSystem, DependencyCategory, Error}; use crate::dependencies::perl::PerlModuleDependency; use crate::fix_build::{BuildFixer, IterateBuildError}; use crate::installer::Error as InstallerError; use crate::session::Session; use std::collections::HashMap; use std::io::Read; use std::path::{Path, PathBuf}; fn read_cpanfile( session: &dyn Session, args: Vec<&str>, category: DependencyCategory, fixers: &[&dyn BuildFixer], ) -> impl Iterator { let mut argv = vec!["cpanfile-dump"]; argv.extend(args); session .command(argv) .run_fixing_problems::<_, crate::buildsystem::Error>(fixers) .unwrap() .into_iter() .filter_map(move |line| { let line = line.trim(); if !line.is_empty() { Some((category.clone(), PerlModuleDependency::simple(line))) } else { None } }) } /// Extract declared dependencies from a cpanfile. /// /// # Arguments /// * `session` - The session to use for executing commands /// * `fixers` - Fixers to apply if reading the cpanfile fails /// /// # Returns /// A list of dependencies declared in the cpanfile pub fn declared_deps_from_cpanfile( session: &dyn Session, fixers: &[&dyn BuildFixer], ) -> Vec<(DependencyCategory, PerlModuleDependency)> { read_cpanfile( session, vec!["--configure", "--build"], DependencyCategory::Build, fixers, ) .chain(read_cpanfile( session, vec!["--test"], DependencyCategory::Test, fixers, )) .collect() } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] /// Metadata from META.yml for a Perl package. /// /// This contains the parsed metadata from a META.yml file, which describes /// the package and its dependencies. pub struct Meta { name: String, #[serde(rename = "abstract")] r#abstract: String, version: String, license: String, author: Vec, distribution_type: String, requires: HashMap, recommends: HashMap, build_requires: HashMap, resources: HashMap, #[serde(rename = "meta-spec")] meta_spec: HashMap, generated_by: String, configure_requires: HashMap, } /// Extract declared dependencies from a META.yml file. /// /// # Arguments /// * `f` - A reader for the META.yml file /// /// # Returns /// A list of dependencies declared in the META.yml file pub fn declared_deps_from_meta_yml( f: R, ) -> Vec<(DependencyCategory, PerlModuleDependency)> { // See http://module-build.sourceforge.net/META-spec-v1.4.html for the specification of the format. let data: Meta = serde_yaml::from_reader(f).unwrap(); let mut ret = vec![]; // TODO: handle versions for name in data.requires.keys() { ret.push(( DependencyCategory::Universal, PerlModuleDependency::simple(name), )); } for name in data.build_requires.keys() { ret.push(( DependencyCategory::Build, PerlModuleDependency::simple(name), )); } for name in data.configure_requires.keys() { ret.push(( DependencyCategory::Build, PerlModuleDependency::simple(name), )); } // TODO(jelmer): recommends ret } #[derive(Debug)] /// DistZilla build system for Perl packages. /// /// This build system handles Perl packages that use DistZilla for building. pub struct DistZilla { path: PathBuf, dist_inkt_class: Option, } impl DistZilla { /// Create a new DistZilla build system with the specified path. /// /// # Arguments /// * `path` - The path to the Perl package directory /// /// # Returns /// A new DistZilla build system instance pub fn new(path: PathBuf) -> Self { let mut dist_inkt_class = None; let mut f = std::fs::File::open(&path).unwrap(); let mut contents = String::new(); f.read_to_string(&mut contents).unwrap(); for line in contents.lines() { let rest = if let Some(rest) = line.strip_prefix(";;") { rest } else { continue; }; let (key, value) = if let Some((key, value)) = rest.split_once('=') { (key.trim(), value.trim()) } else { continue; }; if key == "class" && value.starts_with("'Dist::Inkt") { dist_inkt_class = Some(value[1..value.len() - 1].to_string()); break; } } Self { path, dist_inkt_class, } } /// Set up the DistZilla build environment. /// /// # Arguments /// * `installer` - The installer to use for dependencies /// /// # Returns /// Ok on success or an error pub fn setup( &self, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::installer::Error> { let dep = crate::dependencies::perl::PerlModuleDependency::simple("Dist::Inkt"); installer.install(&dep, crate::installer::InstallationScope::Global)?; Ok(()) } /// Probe a directory for a DistZilla build system. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// A DistZilla build system if one exists at the path, `None` otherwise pub fn probe(path: &Path) -> Option> { let dist_ini_path = path.join("dist.ini"); if dist_ini_path.exists() && !path.join("Makefile.PL").exists() { Some(Box::new(Self::new(dist_ini_path))) } else { None } } } impl BuildSystem for DistZilla { fn name(&self) -> &str { "Dist::Zilla" } fn dist( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, target_directory: &Path, quiet: bool, ) -> Result { self.setup(installer)?; let dc = crate::dist_catcher::DistCatcher::default(&session.external_path(Path::new("."))); if self.dist_inkt_class.is_some() { session .command(vec![guaranteed_which(session, installer, "distinkt-dist") .unwrap() .to_str() .unwrap()]) .quiet(quiet) .run_detecting_problems()?; } else { // Default to invoking Dist::Zilla session .command(vec![ guaranteed_which(session, installer, "dzil") .unwrap() .to_str() .unwrap(), "build", "--tgz", ]) .quiet(quiet) .run_detecting_problems()?; } Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { // see also https://perlmaven.com/how-to-run-the-tests-of-a-typical-perl-module self.setup(installer)?; session .command(vec![ guaranteed_which(session, installer, "dzil") .unwrap() .to_str() .unwrap(), "test", ]) .run_detecting_problems()?; Ok(()) } fn build( &self, session: &dyn Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(installer)?; session .command(vec![ guaranteed_which(session, installer, "dzil") .unwrap() .to_str() .unwrap(), "build", ]) .run_detecting_problems()?; Ok(()) } fn clean( &self, _session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn install( &self, _session: &dyn Session, _installerr: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { Err(Error::Unimplemented) } fn get_declared_dependencies( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<(DependencyCategory, Box)>, crate::buildsystem::Error, > { let mut ret = vec![]; if self.path.exists() { let lines = session .command(vec!["dzil", "authordeps"]) .run_fixing_problems::<_, crate::buildsystem::Error>(fixers.unwrap_or(&[])) .unwrap(); for entry in lines { ret.push(( DependencyCategory::Build, Box::new(PerlModuleDependency::simple(entry.trim())) as Box, )); } } if self.path.parent().unwrap().join("cpanfile").exists() { ret.extend( declared_deps_from_cpanfile(session, fixers.unwrap_or(&[])) .into_iter() .map(|(category, dep)| { ( category, Box::new(dep) as Box, ) }), ); } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug)] /// Module::Build::Tiny build system for Perl packages. /// /// This build system handles Perl packages that use Module::Build::Tiny for building, /// including support for Minilla. pub struct PerlBuildTiny { path: PathBuf, minilla: bool, } impl PerlBuildTiny { /// Create a new PerlBuildTiny build system with the specified path. /// /// # Arguments /// * `path` - The path to the Perl package directory /// /// # Returns /// A new PerlBuildTiny build system instance pub fn new(path: PathBuf) -> Self { let minilla = path.join("minil.toml").exists(); Self { path, minilla } } fn setup( &self, session: &dyn Session, fixers: Option<&[&dyn BuildFixer]>, ) -> Result<(), crate::buildsystem::Error> { let fixers = fixers.unwrap_or(&[]); let argv = vec!["perl", "Build.PL"]; session .command(argv) .run_fixing_problems::<_, crate::buildsystem::Error>(fixers)?; Ok(()) } /// Probe a directory for a Module::Build::Tiny build system. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// A PerlBuildTiny build system if one exists at the path, `None` otherwise pub fn probe(path: &Path) -> Option> { if path.join("Build.PL").exists() { log::debug!("Found Build.PL, assuming Module::Build::Tiny package."); Some(Box::new(Self::new(path.to_path_buf()))) } else { None } } } impl BuildSystem for PerlBuildTiny { fn name(&self) -> &str { "Module::Build::Tiny" } fn dist( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, target_directory: &Path, quiet: bool, ) -> Result { self.setup(session, None)?; let dc = crate::dist_catcher::DistCatcher::default(&session.external_path(Path::new("."))); if self.minilla { // minil seems to return 0 even if it didn't produce a tarball :( crate::analyze::run_detecting_problems( session, vec!["minil", "dist"], Some(&|_, _| dc.find_files().is_none()), quiet, None, None, None, None, )?; } else { match session .command(vec!["./Build", "dist"]) .run_detecting_problems() { Err(AnalyzedError::Unidentified { lines, .. }) if lines.iter().any(|l| { l.contains("Can't find dist packages without a MANIFEST file") }) => { session .command(vec!["./Build", "manifest"]) .run_detecting_problems()?; session .command(vec!["./Build", "dist"]) .run_detecting_problems() } Err(AnalyzedError::Unidentified { lines, .. }) if lines.iter().any(|l| l.contains("No such action 'dist'")) => { unimplemented!("Module::Build::Tiny dist command not supported"); } other => other, }?; } Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, None)?; if self.minilla { session .command(vec!["minil", "test"]) .run_detecting_problems()?; } else { session .command(vec!["./Build", "test"]) .run_detecting_problems()?; } Ok(()) } fn build( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, None)?; session .command(vec!["./Build", "build"]) .run_detecting_problems()?; Ok(()) } fn clean( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, None)?; session .command(vec!["./Build", "clean"]) .run_detecting_problems()?; Ok(()) } fn install( &self, session: &dyn Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { self.setup(session, None)?; if self.minilla { session .command(vec!["minil", "install"]) .run_detecting_problems()?; } else { session .command(vec!["./Build", "install"]) .run_detecting_problems()?; } Ok(()) } fn get_declared_dependencies( &self, session: &dyn Session, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<(DependencyCategory, Box)>, crate::buildsystem::Error, > { self.setup(session, fixers)?; if self.minilla { // Minilla doesn't seem to have a way to just regenerate the metadata :( } else { let cmd = session.command(vec!["./Build", "distmeta"]); if let Some(fixers) = fixers { match cmd.run_fixing_problems::<_, crate::buildsystem::Error>(fixers) { Err(IterateBuildError::Unidentified { lines, .. }) if lines .iter() .any(|l| l.contains("No such action 'distmeta'")) => { // Module::Build::Tiny doesn't have a distmeta action Ok(Vec::new()) } Err(IterateBuildError::Unidentified { lines, .. }) if lines.iter().any(|l| { l.contains( "Do not run distmeta. Install Minilla and `minil install` instead.", ) }) => { log::warn!( "did not detect minilla, but it is required to get the dependencies" ); Ok(Vec::new()) } other => other, }?; } else { cmd.run_detecting_problems()?; } } let meta_yml_path = self.path.join("META.yml"); if meta_yml_path.exists() { let f = std::fs::File::open(&meta_yml_path).unwrap(); Ok(declared_deps_from_meta_yml(f) .into_iter() .map(|(category, dep)| { ( category, Box::new(dep) as Box, ) }) .collect()) } else { Ok(vec![]) } } fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { Err(Error::Unimplemented) } fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/python.rs000064400000000000000000000762111046102023000170210ustar 00000000000000//! Support for Python build systems. //! //! This module provides functionality for building, testing, and installing //! Python packages using various build systems such as setuptools, poetry, and pip. use crate::analyze::{run_detecting_problems, AnalyzedError}; use crate::buildsystem::{BuildSystem, DependencyCategory, Error, InstallTarget}; use crate::dependencies::python::{PythonDependency, PythonPackageDependency}; use crate::dependency::Dependency; use crate::dist_catcher::DistCatcher; use crate::fix_build::BuildFixer; use crate::installer::{Error as InstallerError, InstallationScope, Installer}; use crate::output::{BinaryOutput, Output, PythonPackageOutput}; use crate::session::Session; use pyo3::exceptions::{ PyFileNotFoundError, PyImportError, PyModuleNotFoundError, PyRuntimeError, PySystemExit, }; use pyo3::prelude::*; use pyo3::types::PyDict; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::io::Seek; use std::path::{Path, PathBuf}; use toml; #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Distribution { name: Option, requires: Vec, setup_requires: Vec, install_requires: Vec, tests_require: Vec, scripts: Vec, packages: Vec, entry_points: HashMap>, } fn load_toml(path: &Path) -> Result { let path = path.join("pyproject.toml"); let text = match std::fs::read_to_string(&path) { Ok(text) => text, Err(e) => { return Err(match e.kind() { std::io::ErrorKind::NotFound => { PyFileNotFoundError::new_err(format!("File not found: {}", path.display())) } _ => pyo3::exceptions::PyIOError::new_err(format!( "Failed to read {}: {}", path.display(), e )), }) } }; match toml::from_str(&text) { Ok(parsed) => Ok(parsed), Err(e) => Err(pyo3::exceptions::PyValueError::new_err(format!( "Failed to parse {}: {}", path.display(), e ))), } } #[derive(Debug)] /// A wrapper around a Python setup.cfg configuration file. /// /// This provides access to the configuration in a setup.cfg file, which is used /// by setuptools to configure Python package builds. pub struct SetupCfg(Py); impl SetupCfg { fn has_section(&self, section: &str) -> bool { Python::attach(|py| { self.0 .call_method1(py, "__contains__", (section,)) .unwrap() .extract::(py) .unwrap() }) } fn get_section(&self, section: &str) -> Option { Python::attach(|py| { if self.has_section(section) { let section: Option> = self .0 .call_method1(py, "get", (section, py.None())) .unwrap() .extract(py) .ok(); Some(SetupCfgSection(section.unwrap())) } else { None } }) } } /// A section in a Python setup.cfg configuration file. /// /// This provides access to a specific section in a setup.cfg file, allowing /// access to configuration keys within that section. pub struct SetupCfgSection(Py); impl Default for SetupCfg { fn default() -> Self { Python::attach(|py| SetupCfg(py.None())) } } impl SetupCfgSection { fn get FromPyObject<'a, 'py>>(&self, key: &str) -> Option { Python::attach(|py| { self.0 .call_method1(py, "get", (key, py.None())) .ok()? .extract::>(py) .ok()? }) } /// Check if a key exists in this section. /// /// # Arguments /// * `key` - The key to check for /// /// # Returns /// `true` if the key exists, `false` otherwise pub fn has_key(&self, key: &str) -> bool { Python::attach(|py| { self.0 .call_method1(py, "__contains__", (key,)) .unwrap() .extract::(py) .unwrap() }) } } fn load_setup_cfg(path: &Path) -> Result, PyErr> { Python::attach(|py| { let m = py.import("setuptools.config.setupcfg")?; let read_configuration = m.getattr("read_configuration")?; let p = path.join("setup.cfg"); if p.exists() { let config = read_configuration.call1((p,))?; Ok(Some(SetupCfg(config.unbind()))) } else { Ok(None) } }) } // run_setup, but setting __name__ // Imported from Python's distutils.core, Copyright (C) PSF fn run_setup(py: Python, script_name: &Path, stop_after: &str) -> PyResult> { assert!( stop_after == "init" || stop_after == "config" || stop_after == "commandline" || stop_after == "run" ); // Import setuptools, just in case it decides to replace distutils let _ = py.import("setuptools"); let core = match py.import("distutils.core") { Ok(m) => m, Err(e) if e.is_instance_of::(py) => { // Importing distutils failed, but that's fine. py.import("setuptools._distutils.core")? } Err(e) => return Err(e), }; core.setattr("_setup_stop_after", stop_after)?; let sys = py.import("sys")?; let os = py.import("os")?; let save_argv = sys.getattr("argv")?; let g = PyDict::new(py); g.set_item("__file__", script_name)?; g.set_item("__name__", "__main")?; let old_cwd = os.getattr("getcwd")?.call0()?.extract::()?; os.call_method1( "chdir", (os.getattr("path")? .call_method1("dirname", (script_name,))?,), )?; sys.setattr("argv", vec![script_name])?; let text = std::fs::read_to_string(script_name)?; let code = std::ffi::CString::new(text).unwrap(); let r = py.eval(&code, Some(&g), None); os.call_method1("chdir", (old_cwd,))?; sys.setattr("argv", save_argv)?; core.setattr("_setup_stop_after", py.None())?; match r { Ok(_) => Ok(core.getattr("_setup_distribution")?.unbind()), Err(e) if e.is_instance_of::(py) => { Ok(core.getattr("_setup_distribution")?.unbind()) } Err(e) => Err(e), } } const SETUP_WRAPPER: &str = r#""" try: import setuptools except ImportError: pass import distutils from distutils import core import sys import os script_name = "%(script_name)s" os.chdir(os.path.dirname(script_name)) g = {"__file__": os.path.basename(script_name), "__name__": "__main__"} try: core._setup_stop_after = "init" sys.argv[0] = script_name with open(script_name, "rb") as f: exec(f.read(), g) except SystemExit: # Hmm, should we do something if exiting with a non-zero code # (ie. error)? pass if core._setup_distribution is None: raise RuntimeError( ( "'distutils.core.setup()' was never called -- " "perhaps '%s' is not a Distutils setup script?" ) % script_name ) d = core._setup_distribution r = { 'name': getattr(d, "name", None) or None, 'setup_requires': getattr(d, "setup_requires", []), 'install_requires': getattr(d, "install_requires", []), 'tests_require': getattr(d, "tests_require", []) or [], 'scripts': getattr(d, "scripts", []) or [], 'entry_points': getattr(d, "entry_points", None) or {}, 'packages': getattr(d, "packages", []) or [], 'requires': d.get_requires() or [], } import os import json with open(%(output_path)s, 'w') as f: json.dump(r, f) """#; #[derive(Debug)] /// A Python setuptools-based build system. /// /// This build system handles Python packages that use setup.py for building, /// which is the traditional approach for Python packages. pub struct SetupPy { path: PathBuf, has_setup_py: bool, config: Option, pyproject: Option, #[allow(dead_code)] buildsystem: Option, } impl SetupPy { /// Create a new SetupPy build system with the specified path. /// /// This will load and parse setup.cfg and pyproject.toml if they exist. /// /// # Arguments /// * `path` - The path to the Python project directory /// /// # Returns /// A new SetupPy build system instance pub fn new(path: &Path) -> Self { let has_setup_py = path.join("setup.py").exists(); Python::attach(|py| { let config = match load_setup_cfg(path) { Ok(config) => config, Err(e) if e.is_instance_of::(py) => None, Err(e) if e.is_instance_of::(py) => { log::warn!("Error parsing setup.cfg: {}", e); None } Err(e) => { panic!("Error parsing setup.cfg: {}", e); } }; let (pyproject, buildsystem) = match load_toml(path) { Ok(pyproject) => { let buildsystem = pyproject .build_system .as_ref() .and_then(|bs| bs.build_backend.clone()); (Some(pyproject), buildsystem) } Err(e) if e.is_instance_of::(py) => (None, None), Err(e) => { panic!("Error parsing pyproject.toml: {}", e); } }; Self { has_setup_py, path: path.to_owned(), config, pyproject, buildsystem, } }) } /// Probe a directory for a Python setuptools build system. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// A SetupPy build system if one exists at the path, `None` otherwise pub fn probe(path: &Path) -> Option> { if path.join("setup.py").exists() { log::debug!("Found setup.py, assuming python project."); return Some(Box::new(Self::new(path))); } if path.join("pyproject.toml").exists() { log::debug!("Found pyproject.toml, assuming python project."); return Some(Box::new(Self::new(path))); } None } fn extract_setup_direct(&self) -> Option { let p = self.path.join("setup.py").canonicalize().unwrap(); Python::attach(|py| { let d = match run_setup(py, &p, "init") { Err(e) if e.is_instance_of::(py) => { log::warn!("Unable to load setup.py metadata: {}", e); return None; } Ok(d) => d, Err(e) => { panic!("Unable to load setup.py metadata: {}", e); } }; let name: Option = d.getattr(py, "name").unwrap().extract(py).unwrap(); let setup_requires: Vec = d .call_method1(py, "get", ("setup_requires", Vec::::new())) .unwrap() .extract(py) .unwrap(); let install_requires: Vec = d .call_method1(py, "get", ("install_requires", Vec::::new())) .unwrap() .extract(py) .unwrap(); let tests_require: Vec = d .call_method1(py, "get", ("tests_require", Vec::::new())) .unwrap() .extract(py) .unwrap(); let scripts: Vec = d .call_method1(py, "get", ("scripts", Vec::::new())) .unwrap() .extract(py) .unwrap(); let entry_points: HashMap> = d .call_method1( py, "get", ("entry_points", HashMap::>::new()), ) .unwrap() .extract(py) .unwrap(); let packages: Vec = d .call_method1(py, "get", ("packages", Vec::::new())) .unwrap() .extract(py) .unwrap(); let requires: Vec = d .call_method0(py, "get_requires") .unwrap() .extract(py) .unwrap(); Some(Distribution { name, setup_requires, install_requires, tests_require, scripts, entry_points, packages, requires, }) }) } fn determine_interpreter(&self) -> String { if let Some(config) = self.config.as_ref() { let python_requires: Option = config .get_section("options") .and_then(|s| s.get::("python_requires")); if python_requires .map(|pr| !pr.contains("2.7")) .unwrap_or(true) { return "python3".to_owned(); } } let path = self.path.join("setup.py"); let shebang_binary = crate::shebang::shebang_binary(&path).unwrap(); shebang_binary.unwrap_or("python3".to_owned()) } fn extract_setup_in_session( &self, session: &dyn Session, fixers: Option<&[&dyn BuildFixer]>, ) -> Option { let interpreter = self.determine_interpreter(); let mut output_f = tempfile::NamedTempFile::new_in(session.location().join("tmp")).unwrap(); let argv: Vec = vec![ interpreter, "-c".to_string(), SETUP_WRAPPER .replace( "%(script_name)s", session.pwd().join("setup.py").to_str().unwrap(), ) .replace( "%(output_path)s", &format!( "\"/{}\"", output_f .path() .to_str() .unwrap() .strip_prefix(session.location().to_str().unwrap()) .unwrap() ), ), ]; let r = if let Some(fixers) = fixers { session .command(argv.iter().map(|x| x.as_str()).collect::>()) .quiet(true) .run_fixing_problems::<_, Error>(fixers) .map(|_| ()) .map_err(|e| e.to_string()) } else { session .command(argv.iter().map(|x| x.as_str()).collect()) .check_call() .map_err(|e| e.to_string()) }; match r { Ok(_) => (), Err(e) => { log::warn!("Unable to load setup.py metadata: {}", e); return None; } } output_f.seek(std::io::SeekFrom::Start(0)).unwrap(); Some(serde_json::from_reader(output_f).unwrap()) } fn extract_setup( &self, session: Option<&dyn Session>, fixers: Option<&[&dyn BuildFixer]>, ) -> Option { if !self.has_setup_py { return None; } if let Some(session) = session { self.extract_setup_in_session(session, fixers) } else { self.extract_setup_direct() } } fn setup_requires(&self) -> Vec { let mut ret = vec![]; if let Some(build_system) = self .pyproject .as_ref() .and_then(|p| p.build_system.as_ref()) { let requires = &build_system.requires; for require in requires { ret.push(PythonPackageDependency::from(require.clone())); } } if let Some(config) = &self.config { let options = config.get_section("options"); let setup_requires = options .and_then(|os| os.get::>("setup_requires")) .unwrap_or(vec![]); for require in &setup_requires { ret.push(PythonPackageDependency::try_from(require.clone()).unwrap()); } } ret } fn run_setup( &self, session: &dyn Session, installer: &dyn Installer, args: Vec<&str>, ) -> Result<(), Error> { // Install the setup_requires beforehand, since otherwise // setuptools might fetch eggs instead of our preferred installer. let setup_requires = self .setup_requires() .into_iter() .map(|x| Box::new(x) as Box) .collect::>(); crate::installer::install_missing_deps( session, installer, &[crate::installer::InstallationScope::Global], setup_requires .iter() .map(|x| x.as_ref()) .collect::>() .as_slice(), )?; let interpreter = self.determine_interpreter().clone(); let mut args = args.clone(); args.insert(0, &interpreter); args.insert(1, "setup.py"); // TODO(jelmer): Perhaps this should be additive? session.command(args).run_detecting_problems()?; Ok(()) } } impl BuildSystem for SetupPy { fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { if self.path.join("tox.ini").exists() { run_detecting_problems( session, vec!["tox", "--skip-missing-interpreters"], None, false, None, None, None, None, )?; return Ok(()); } if self .config .as_ref() .map(|c| c.has_section("tool:pytest") || c.has_section("pytest")) .unwrap_or(false) { session.command(vec!["pytest"]).run_detecting_problems()?; return Ok(()); } if self.has_setup_py { // Pre-emptively install setuptools, since distutils doesn't provide // a 'test' subcommand and some packages fall back to distutils // if setuptools is not available. let setuptools_dep = PythonPackageDependency::simple("setuptools"); if !setuptools_dep.present(session) { installer.install(&setuptools_dep, InstallationScope::Global)?; } match self.run_setup(session, installer, vec!["test"]) { Ok(_) => { return Ok(()); } Err(Error::Error(AnalyzedError::Unidentified { lines, .. })) if lines.contains(&"error: invalid command 'test'".to_string()) => { return Ok(()); } Err(e) => { return Err(e); } } } unimplemented!(); } fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { if self.has_setup_py { self.run_setup(session, installer, vec!["build"]) } else { unimplemented!(); } } fn dist( &self, session: &dyn Session, installer: &dyn Installer, target_directory: &Path, quiet: bool, ) -> Result { // TODO(jelmer): Look at self.build_backend let dc = DistCatcher::new(vec![session.external_path(Path::new("dist"))]); if self.has_setup_py { let mut preargs = vec![]; if quiet { preargs.push("--quiet"); } // Preemptively install setuptools since some packages fail in some way without it. let setuptools_req = PythonPackageDependency::simple("setuptools"); if !setuptools_req.present(session) { installer.install(&setuptools_req, InstallationScope::Global)?; } preargs.push("sdist"); self.run_setup(session, installer, preargs)?; } else if self.pyproject.is_some() { run_detecting_problems( session, vec!["python3", "-m", "build", "--sdist", "."], None, false, None, None, None, None, )?; } else { panic!("No setup.py or pyproject.toml"); } Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn clean(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { if self.has_setup_py { self.run_setup(session, installer, vec!["clean"]) } else { unimplemented!(); } } fn install( &self, session: &dyn Session, installer: &dyn Installer, install_target: &InstallTarget, ) -> Result<(), Error> { if self.has_setup_py { let mut args = vec![]; if install_target.scope == InstallationScope::User { args.push("--user".to_string()); } if let Some(prefix) = install_target.prefix.as_ref() { args.push(format!("--prefix={}", prefix.to_str().unwrap())); } args.insert(0, "install".to_owned()); self.run_setup( session, installer, args.iter().map(|x| x.as_str()).collect(), )?; Ok(()) } else { unimplemented!(); } } fn get_declared_dependencies( &self, session: &dyn Session, fixers: std::option::Option<&[&dyn BuildFixer]>, ) -> Result)>, Error> { let mut ret: Vec<(DependencyCategory, Box)> = vec![]; let distribution = self.extract_setup(Some(session), fixers); if let Some(distribution) = distribution { for require in &distribution.requires { ret.push(( DependencyCategory::Universal, Box::new(PythonPackageDependency::try_from(require.clone()).unwrap()), )); } // Not present for distutils-only packages for require in &distribution.setup_requires { ret.push(( DependencyCategory::Build, Box::new(PythonPackageDependency::try_from(require.clone()).unwrap()), )); } // Not present for distutils-only packages for require in &distribution.install_requires { ret.push(( DependencyCategory::Universal, Box::new(PythonPackageDependency::try_from(require.clone()).unwrap()), )); } // Not present for distutils-only packages for require in &distribution.tests_require { ret.push(( DependencyCategory::Test, Box::new(PythonPackageDependency::try_from(require.clone()).unwrap()), )); } } if let Some(pyproject) = self.pyproject.as_ref() { if let Some(build_system) = pyproject.build_system.as_ref() { for require in &build_system.requires { ret.push(( DependencyCategory::Build, Box::new(PythonPackageDependency::from(require.clone())), )); } } } if let Some(options) = self.config.as_ref().and_then(|c| c.get_section("options")) { for require in options .get::>("setup_requires") .unwrap_or_default() { ret.push(( DependencyCategory::Build, Box::new(PythonPackageDependency::try_from(require).unwrap()), )); } for require in options .get::>("install_requires") .unwrap_or_default() { ret.push(( DependencyCategory::Universal, Box::new(PythonPackageDependency::try_from(require).unwrap()), )); } } if let Some(pyproject_toml) = self.pyproject.as_ref() { if let Some(build_system) = pyproject_toml.build_system.as_ref() { for require in &build_system.requires { ret.push(( DependencyCategory::Build, Box::new(PythonPackageDependency::from(require.clone())), )); } } if let Some(dependencies) = pyproject_toml .project .as_ref() .and_then(|p| p.dependencies.as_ref()) { for dep in dependencies { ret.push(( DependencyCategory::Universal, Box::new(PythonPackageDependency::from(dep.clone())), )); } } if let Some(extras) = pyproject_toml .project .as_ref() .and_then(|p| p.optional_dependencies.as_ref()) { for (name, deps) in extras { for dep in deps { ret.push(( DependencyCategory::RuntimeExtra(name.clone()), Box::new(PythonPackageDependency::from(dep.clone())), )); } } } if let Some(requires_python) = pyproject_toml .project .as_ref() .and_then(|p| p.requires_python.as_ref()) { ret.push(( DependencyCategory::Universal, Box::new(PythonDependency::from(requires_python)), )); } } Ok(ret) } fn get_declared_outputs( &self, session: &dyn Session, fixers: Option<&[&dyn BuildFixer]>, ) -> Result>, Error> { let mut ret: Vec> = vec![]; let distribution = self.extract_setup(Some(session), fixers); let mut all_packages = HashSet::new(); if let Some(distribution) = distribution { for script in &distribution.scripts { ret.push(Box::new(BinaryOutput( Path::new(script) .file_name() .unwrap() .to_str() .unwrap() .to_owned(), ))); } for script in distribution .entry_points .get("console_scripts") .unwrap_or(&vec![]) { ret.push(Box::new(BinaryOutput( script.split_once('=').unwrap().0.to_string().to_owned(), ))); } all_packages.extend(distribution.packages); } if let Some(options) = self.config.as_ref().and_then(|c| c.get_section("options")) { all_packages.extend(options.get::>("packages").unwrap_or_default()); for script in options.get::>("scripts").unwrap_or_default() { let p = Path::new(&script); ret.push(Box::new(BinaryOutput( p.file_name().unwrap().to_str().unwrap().to_owned(), ))); } let entry_points = options .get::>>("entry_points") .unwrap_or_default(); for script in entry_points.get("console_scripts").unwrap_or(&vec![]) { ret.push(Box::new(BinaryOutput( script.split_once('=').unwrap().0.to_string().to_owned(), ))); } } for package in all_packages { ret.push(Box::new(PythonPackageOutput::new( &package, Some("cpython3"), ))); } if let Some(pyproject) = self.pyproject.as_ref().and_then(|p| p.project.as_ref()) { if let Some(scripts) = pyproject.scripts.as_ref() { for (script, _from) in scripts { ret.push(Box::new(BinaryOutput(script.to_string()))); } } if let Some(gui_scripts) = pyproject.gui_scripts.as_ref() { for (script, _from) in gui_scripts { ret.push(Box::new(BinaryOutput(script.to_string()))); } } ret.push(Box::new(PythonPackageOutput::new( &pyproject.name, pyproject.version.as_ref().map(|v| v.to_string()).as_deref(), ))); } Ok(ret) } fn name(&self) -> &str { "setup.py" } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::tempdir; #[test] fn test_python_project_without_pyproject_toml() { pyo3::Python::initialize(); let temp_dir = tempdir().unwrap(); let path = temp_dir.path(); // Create only setup.py, no pyproject.toml fs::write( path.join("setup.py"), "from setuptools import setup\nsetup(name='test')", ) .unwrap(); // This should not panic let setup_py = SetupPy::new(path); assert!(setup_py.has_setup_py); assert!(setup_py.pyproject.is_none()); } #[test] fn test_load_toml_file_not_found() { pyo3::Python::initialize(); Python::attach(|py| { let temp_dir = tempdir().unwrap(); let path = temp_dir.path(); // Don't create pyproject.toml let result = load_toml(path); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.is_instance_of::(py)); }); } #[test] fn test_load_toml_invalid_content() { pyo3::Python::initialize(); Python::attach(|py| { let temp_dir = tempdir().unwrap(); let path = temp_dir.path(); // Create invalid pyproject.toml fs::write(path.join("pyproject.toml"), "invalid toml content").unwrap(); let result = load_toml(path); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.is_instance_of::(py)); }); } } ognibuild-0.2.6/src/buildsystems/r.rs000064400000000000000000000161461046102023000157420ustar 00000000000000//! Support for R build systems. //! //! This module provides functionality for building, testing, and installing //! R packages using R CMD build and related commands. use crate::buildsystem::guaranteed_which; use crate::buildsystem::{BuildSystem, DependencyCategory}; use crate::dependencies::r::RPackageDependency; use crate::dependency::Dependency; use crate::dist_catcher::DistCatcher; use crate::output::RPackageOutput; use std::path::{Path, PathBuf}; #[derive(Debug)] /// R build system for R packages. /// /// This build system handles R packages using R CMD commands for building, /// testing, and installation. pub struct R { path: PathBuf, } impl R { /// Create a new R build system with the specified path. /// /// # Arguments /// * `path` - The path to the R package directory /// /// # Returns /// A new R build system instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Run R CMD check on the package to check for issues. /// /// # Arguments /// * `session` - The session to run the command in /// * `installer` - The installer to use for dependencies /// /// # Returns /// Ok on success or an error pub fn lint( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { let r_path = guaranteed_which(session, installer, "R").unwrap(); session .command(vec![r_path.to_str().unwrap(), "CMD", "check"]) .run_detecting_problems()?; Ok(()) } /// Probe a directory for an R package. /// /// # Arguments /// * `path` - The path to check /// /// # Returns /// An R build system if an R package exists at the path, `None` otherwise pub fn probe(path: &Path) -> Option> { if path.join("DESCRIPTION").exists() && path.join("NAMESPACE").exists() { Some(Box::new(Self::new(path.to_path_buf()))) } else { None } } } impl BuildSystem for R { fn name(&self) -> &str { "R" } fn dist( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, target_directory: &Path, _quiet: bool, ) -> Result { let dc = DistCatcher::new(vec![session.external_path(Path::new("."))]); let r_path = guaranteed_which(session, installer, "R").unwrap(); session .command(vec![r_path.to_str().unwrap(), "CMD", "build", "."]) .run_detecting_problems()?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } fn test( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { let r_path = guaranteed_which(session, installer, "R").unwrap(); if session.exists(Path::new("run_tests.sh")) { session .command(vec!["./run_tests.sh"]) .run_detecting_problems()?; } else if session.exists(Path::new("tests/testthat")) { session .command(vec![ r_path.to_str().unwrap(), "-e", "testthat::test_dir('tests')", ]) .run_detecting_problems()?; } Ok(()) } fn build( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { // Nothing to do here Ok(()) } fn clean( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), crate::buildsystem::Error> { Err(crate::buildsystem::Error::Unimplemented) } fn install( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), crate::buildsystem::Error> { let r_path = guaranteed_which(session, installer, "R").unwrap(); let mut args = vec![ r_path.to_str().unwrap().to_string(), "CMD".to_string(), "INSTALL".to_string(), ".".to_string(), ]; if let Some(prefix) = &install_target.prefix.as_ref() { args.push(format!("--prefix={}", prefix.to_str().unwrap())); } session .command(args.iter().map(|s| s.as_str()).collect()) .run_detecting_problems()?; Ok(()) } fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, crate::buildsystem::Error, > { let mut ret: Vec<(DependencyCategory, Box)> = vec![]; let f = std::fs::File::open(self.path.join("DESCRIPTION")).unwrap(); let description = read_description(f).unwrap(); for s in description.suggests().unwrap_or_default().iter() { ret.push(( DependencyCategory::Build, /* TODO */ Box::new(RPackageDependency::from(s)), )); } for s in description.depends().unwrap_or_default().iter() { ret.push(( DependencyCategory::Build, Box::new(RPackageDependency::from(s)), )); } for s in description.imports().unwrap_or_default().iter() { ret.push(( DependencyCategory::Build, Box::new(RPackageDependency::from_str(s)), )); } for s in description.linking_to().unwrap_or_default() { ret.push(( DependencyCategory::Build, Box::new(RPackageDependency::from_str(&s)), )); } Ok(ret) } fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, crate::buildsystem::Error> { let mut ret = vec![]; let f = std::fs::File::open(self.path.join("DESCRIPTION")).unwrap(); let description = read_description(f).unwrap(); if let Some(package) = description.package() { ret.push(Box::new(RPackageOutput::new(&package)) as Box); } Ok(ret) } fn as_any(&self) -> &dyn std::any::Any { self } } fn read_description( mut r: R, ) -> Result { // See https://r-pkgs.org/description.html let mut s = String::new(); r.read_to_string(&mut s).unwrap(); let p: r_description::lossless::RDescription = s.parse().unwrap(); Ok(p) } ognibuild-0.2.6/src/buildsystems/ruby.rs000064400000000000000000000147741046102023000164670ustar 00000000000000//! Support for Ruby build systems. //! //! This module provides functionality for building, testing, and installing //! Ruby gems using the gem command. use crate::buildsystem::{guaranteed_which, BuildSystem, Error}; use std::path::{Path, PathBuf}; /// Ruby gem build system. /// /// This build system handles Ruby gems for distribution and installation. #[derive(Debug)] pub struct Gem { path: PathBuf, } impl Gem { /// Create a new Ruby gem build system. /// /// # Arguments /// * `path` - Path to the gem file /// /// # Returns /// A new Gem instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Probe a directory to check if it contains Ruby gem files. /// /// # Arguments /// * `path` - Path to check for gem files /// /// # Returns /// Some(BuildSystem) if gem files are found, None otherwise pub fn probe(path: &Path) -> Option> { let mut gemfiles = std::fs::read_dir(path) .unwrap() .filter_map(|entry| entry.ok().map(|entry| entry.path())) .filter(|path| path.extension().unwrap_or_default() == "gem") .collect::>(); if !gemfiles.is_empty() { Some(Box::new(Self::new(gemfiles.remove(0)))) } else { None } } } /// Implementation of BuildSystem for Ruby gems. impl BuildSystem for Gem { /// Get the name of this build system. /// /// # Returns /// The string "gem" fn name(&self) -> &str { "gem" } /// Create a distribution package from the gem file. /// /// # Arguments /// * `session` - Session to run commands in /// * `installer` - Installer to use for installing dependencies /// * `target_directory` - Directory to store the created distribution package /// * `quiet` - Whether to suppress output /// /// # Returns /// OsString with the name of the created distribution package, or an error fn dist( &self, session: &dyn crate::session::Session, installer: &dyn crate::installer::Installer, target_directory: &std::path::Path, quiet: bool, ) -> Result { let mut gemfiles = std::fs::read_dir(&self.path) .unwrap() .filter_map(|entry| entry.ok().map(|entry| entry.path())) .filter(|path| path.extension().unwrap_or_default() == "gem") .collect::>(); assert!(!gemfiles.is_empty()); if gemfiles.len() > 1 { log::warn!("More than one gemfile. Trying the first?"); } let dc = crate::dist_catcher::DistCatcher::default(&session.external_path(Path::new("."))); session .command(vec![ guaranteed_which(session, installer, "gem2tgz")? .to_str() .unwrap(), gemfiles.remove(0).to_str().unwrap(), ]) .quiet(quiet) .run_detecting_problems()?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } /// Run tests for this gem. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Always returns Error::Unimplemented as testing is not implemented for gems fn test( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { Err(Error::Unimplemented) } /// Build this gem. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Always returns Error::Unimplemented as building is not implemented for gems fn build( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { Err(Error::Unimplemented) } /// Clean build artifacts. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Always returns Error::Unimplemented as cleaning is not implemented for gems fn clean( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { Err(Error::Unimplemented) } /// Install the gem. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// * `_install_target` - Target installation directory /// /// # Returns /// Always returns Error::Unimplemented as installation is not implemented for gems fn install( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), Error> { Err(Error::Unimplemented) } /// Get dependencies declared by this gem. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// Always returns Error::Unimplemented as dependency discovery is not implemented for gems fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, Error, > { Err(Error::Unimplemented) } /// Get outputs declared by this gem. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// Always returns Error::Unimplemented as output discovery is not implemented for gems fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { Err(Error::Unimplemented) } /// Convert this build system to Any for downcasting. /// /// # Returns /// Reference to self as Any fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/rust.rs000064400000000000000000000273621046102023000165000ustar 00000000000000//! Support for Rust build systems. //! //! This module provides functionality for building, testing, and managing //! dependencies for Rust projects using Cargo. use crate::analyze::AnalyzedError; use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; use crate::dependencies::CargoCrateDependency; use crate::dependency::Dependency; use std::path::{Path, PathBuf}; /// A Cargo package declaration from Cargo.toml. #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] struct Package { name: String, } /// A dependency declaration in a Cargo.toml file. /// /// This can be either a simple version string or a detailed declaration /// with additional configuration options. #[derive(serde::Deserialize, Debug)] #[serde(untagged)] #[allow(dead_code)] enum CrateDependency { /// Simple version string dependency Version(String), /// Detailed dependency with configuration options Details { /// Version requirement string version: Option, /// Whether the dependency is optional optional: Option, /// List of features to enable features: Option>, /// Whether to use the workspace version workspace: Option, /// Git repository URL git: Option, /// Git branch to use branch: Option, /// Whether to enable default features #[serde(rename = "default-features")] default_features: Option, }, } /// Methods for accessing CrateDependency information. #[allow(dead_code)] impl CrateDependency { /// Get the version string if available. /// /// # Returns /// The version string, or None if not specified fn version(&self) -> Option<&str> { match self { Self::Version(v) => Some(v.as_str()), Self::Details { version, .. } => version.as_deref(), } } /// Get the list of features if available. /// /// # Returns /// A slice of feature strings, or None if not specified fn features(&self) -> Option<&[String]> { match self { Self::Version(_) => None, Self::Details { features, .. } => features.as_ref().map(|v| v.as_slice()), } } } /// A binary target declared in a Cargo.toml file. #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] pub struct CrateBinary { /// Name of the binary name: String, /// Path to the binary source file path: Option, /// List of features that must be enabled for this binary to be built #[serde(rename = "required-features")] required_features: Option>, } /// A library target declared in a Cargo.toml file. #[derive(serde::Deserialize, Debug)] pub struct CrateLibrary {} /// Representation of a Cargo.toml file. #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] struct CargoToml { /// Package metadata package: Option, /// Map of dependency name to dependency details dependencies: Option>, /// List of binary targets bin: Option>, /// Library target (if any) lib: Option, } /// Cargo build system for Rust projects. /// /// This build system handles Rust projects that use Cargo for building, /// testing, and dependency management. #[derive(Debug)] pub struct Cargo { /// Path to the Cargo project #[allow(dead_code)] path: PathBuf, /// Parsed Cargo.toml file local_crate: CargoToml, } impl Cargo { /// Create a new Cargo build system. /// /// # Arguments /// * `path` - Path to the Cargo project /// /// # Returns /// A new Cargo instance with parsed Cargo.toml pub fn new(path: PathBuf) -> Self { let cargo_toml = std::fs::read_to_string(path.join("Cargo.toml")).unwrap(); let local_crate: CargoToml = toml::from_str(&cargo_toml).unwrap(); Self { path, local_crate } } /// Probe a directory to check if it contains a Cargo project. /// /// # Arguments /// * `path` - Path to check for a Cargo.toml file /// /// # Returns /// Some(BuildSystem) if a Cargo.toml is found, None otherwise pub fn probe(path: &Path) -> Option> { if path.join("Cargo.toml").exists() { log::debug!("Found Cargo.toml, assuming rust cargo package."); Some(Box::new(Cargo::new(path.to_path_buf()))) } else { None } } } /// Implementation of BuildSystem for Cargo. impl BuildSystem for Cargo { /// Get the name of this build system. /// /// # Returns /// The string "cargo" fn name(&self) -> &str { "cargo" } /// Create a distribution package. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// * `_target_directory` - Directory to store the created distribution package /// * `_quiet` - Whether to suppress output /// /// # Returns /// Always returns Error::Unimplemented as dist is not implemented for Cargo fn dist( &self, _session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, _target_directory: &std::path::Path, _quiet: bool, ) -> Result { Err(Error::Unimplemented) } /// Run tests using cargo test command. /// /// # Arguments /// * `session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn test( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { session .command(vec!["cargo", "test"]) .run_detecting_problems()?; Ok(()) } /// Build the project using cargo build command. /// /// Attempts to run cargo generate first, if available. /// /// # Arguments /// * `session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn build( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { match session .command(vec!["cargo", "generate"]) .run_detecting_problems() { Ok(_) => {} Err(AnalyzedError::Unidentified { lines, .. }) if lines == ["error: no such subcommand: `generate`"] => {} Err(e) => return Err(e.into()), } session .command(vec!["cargo", "build"]) .run_detecting_problems()?; Ok(()) } /// Clean build artifacts using cargo clean command. /// /// # Arguments /// * `session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn clean( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, ) -> Result<(), Error> { session .command(vec!["cargo", "clean"]) .run_detecting_problems()?; Ok(()) } /// Install the built software using cargo install command. /// /// # Arguments /// * `session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// * `install_target` - Target installation directory /// /// # Returns /// Ok on success, Error otherwise fn install( &self, session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), Error> { let mut args = vec!["cargo", "install", "--path=."]; let root_arg; if let Some(prefix) = install_target.prefix.as_ref() { root_arg = format!("--root={}", prefix.display()); args.push(&root_arg); } session.command(args).run_detecting_problems()?; Ok(()) } /// Get dependencies declared in the Cargo.toml file. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// A list of dependencies with their categories fn get_declared_dependencies( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, Error, > { let mut ret: Vec<(DependencyCategory, Box)> = vec![]; for (name, details) in self .local_crate .dependencies .as_ref() .unwrap_or(&std::collections::HashMap::new()) { ret.push(( DependencyCategory::Build, Box::new(CargoCrateDependency { name: name.to_owned(), features: details.features().map(|f| f.to_vec()), api_version: None, minimum_version: None, }), )); } Ok(ret) } /// Get outputs declared in the Cargo.toml file. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// A list of binary outputs from the bin section of Cargo.toml fn get_declared_outputs( &self, _session: &dyn crate::session::Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { let mut ret: Vec> = vec![]; if let Some(bins) = &self.local_crate.bin { for bin in bins { ret.push(Box::new(crate::output::BinaryOutput::new(&bin.name))); } } // TODO: library output Ok(ret) } /// Install declared dependencies using cargo fetch. /// /// # Arguments /// * `_categories` - Categories of dependencies to install /// * `scopes` - Installation scopes to consider /// * `session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// * `fixers` - Build fixers to use if needed /// /// # Returns /// Ok on success, Error otherwise fn install_declared_dependencies( &self, _categories: &[crate::buildsystem::DependencyCategory], scopes: &[crate::installer::InstallationScope], session: &dyn crate::session::Session, _installer: &dyn crate::installer::Installer, fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result<(), Error> { if !scopes.contains(&crate::installer::InstallationScope::Vendor) { return Err(crate::installer::Error::UnsupportedScopes(scopes.to_vec()).into()); } if let Some(fixers) = fixers { session .command(vec!["cargo", "fetch"]) .run_fixing_problems::<_, Error>(fixers) .unwrap(); } else { session .command(vec!["cargo", "fetch"]) .run_detecting_problems()?; } Ok(()) } /// Convert this build system to Any for downcasting. /// /// # Returns /// Reference to self as Any fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/buildsystems/waf.rs000064400000000000000000000161611046102023000162530ustar 00000000000000//! Support for Waf build systems. //! //! This module provides functionality for building, testing, and distributing //! software that uses the Waf build system. use crate::buildsystem::{BuildSystem, Error}; use crate::dependency::Dependency; use crate::installer::{InstallationScope, Installer}; use crate::session::Session; use std::path::PathBuf; /// Waf build system. /// /// This build system handles projects that use Waf for building and testing. #[derive(Debug)] pub struct Waf { #[allow(dead_code)] path: PathBuf, } impl Waf { /// Create a new Waf build system. /// /// # Arguments /// * `path` - Path to the waf script /// /// # Returns /// A new Waf instance pub fn new(path: PathBuf) -> Self { Self { path } } /// Set up the environment for using Waf. /// /// Ensures Python 3 is installed as it's required by Waf. /// /// # Arguments /// * `session` - Session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { let binary_req = crate::dependencies::BinaryDependency::new("python3"); if !binary_req.present(session) { installer.install(&binary_req, InstallationScope::Global)?; } Ok(()) } /// Probe a directory to check if it contains a Waf build system. /// /// # Arguments /// * `path` - Path to check for a waf script /// /// # Returns /// Some(BuildSystem) if a waf script is found, None otherwise pub fn probe(path: &std::path::Path) -> Option> { let path = path.join("waf"); if path.exists() { log::debug!("Found waf, assuming waf package."); Some(Box::new(Self::new(path))) } else { None } } } /// Implementation of BuildSystem for Waf. impl BuildSystem for Waf { /// Get the name of this build system. /// /// # Returns /// The string "waf" fn name(&self) -> &str { "waf" } /// Create a distribution package using waf dist command. /// /// # Arguments /// * `session` - Session to run commands in /// * `installer` - Installer to use for installing dependencies /// * `target_directory` - Directory to store the created distribution package /// * `quiet` - Whether to suppress output /// /// # Returns /// OsString with the name of the created distribution package, or an error fn dist( &self, session: &dyn Session, installer: &dyn Installer, target_directory: &std::path::Path, quiet: bool, ) -> Result { self.setup(session, installer)?; let dc = crate::dist_catcher::DistCatcher::default( &session.external_path(std::path::Path::new(".")), ); session .command(vec!["./waf", "dist"]) .quiet(quiet) .run_detecting_problems()?; Ok(dc.copy_single(target_directory).unwrap().unwrap()) } /// Run tests using waf test command. /// /// # Arguments /// * `session` - Session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { self.setup(session, installer)?; session .command(vec!["./waf", "test"]) .run_detecting_problems()?; Ok(()) } /// Build the project using waf build command. /// /// Automatically runs configure if necessary. /// /// # Arguments /// * `session` - Session to run commands in /// * `installer` - Installer to use for installing dependencies /// /// # Returns /// Ok on success, Error otherwise fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { self.setup(session, installer)?; match session .command(vec!["./waf", "build"]) .run_detecting_problems() { Err(crate::analyze::AnalyzedError::Unidentified { lines, .. }) if lines.contains( &"The project was not configured: run \"waf configure\" first!".to_string(), ) => { session .command(vec!["./waf", "configure"]) .run_detecting_problems()?; session .command(vec!["./waf", "build"]) .run_detecting_problems() } other => other, }?; Ok(()) } /// Clean build artifacts. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// /// # Returns /// Always returns Error::Unimplemented as cleaning is not implemented for Waf fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { Err(Error::Unimplemented) } /// Install the built software. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_installer` - Installer to use for installing dependencies /// * `_install_target` - Target installation directory /// /// # Returns /// Always returns Error::Unimplemented as installation is not implemented for Waf fn install( &self, _session: &dyn Session, _installer: &dyn Installer, _install_target: &crate::buildsystem::InstallTarget, ) -> Result<(), Error> { Err(Error::Unimplemented) } /// Get dependencies declared by this project. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// Always returns Error::Unimplemented as dependency discovery is not implemented for Waf fn get_declared_dependencies( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result< Vec<( crate::buildsystem::DependencyCategory, Box, )>, Error, > { Err(Error::Unimplemented) } /// Get outputs declared by this project. /// /// # Arguments /// * `_session` - Session to run commands in /// * `_fixers` - Build fixers to use if needed /// /// # Returns /// Always returns Error::Unimplemented as output discovery is not implemented for Waf fn get_declared_outputs( &self, _session: &dyn Session, _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, ) -> Result>, Error> { Err(Error::Unimplemented) } /// Convert this build system to Any for downcasting. /// /// # Returns /// Reference to self as Any fn as_any(&self) -> &dyn std::any::Any { self } } ognibuild-0.2.6/src/debian/apt.rs000064400000000000000000000617111046102023000147560ustar 00000000000000//! APT package management functionality for Debian packages. //! //! This module provides interfaces for installing and managing packages //! using the APT package manager, as well as utilities for converting //! generic dependencies to Debian package dependencies. use crate::dependencies::debian::{ default_tie_breakers, DebianDependency, IntoDebianDependency, TieBreaker, }; use crate::dependency::Dependency; use crate::installer::{Error as InstallerError, Explanation, InstallationScope, Installer}; use crate::session::{get_user, Session}; use debversion::Version; use std::sync::RwLock; /// Errors that can occur when using APT. pub enum Error { /// An unidentified error occurred while running apt. Unidentified { /// The return code from apt. retcode: i32, /// The command-line arguments passed to apt. args: Vec, /// The output lines from apt. lines: Vec, /// Secondary match information from buildlog-consultant. secondary: Option>, }, /// A detailed error occurred while running apt. Detailed { /// The return code from apt. retcode: i32, /// The command-line arguments passed to apt. args: Vec, /// The error from buildlog-consultant. error: Option>, }, /// An error occurred in the session. Session(crate::session::Error), /// An error occurred in file search operations. FileSearch(crate::debian::file_search::Error), } impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::Unidentified { retcode, args, lines, secondary: _, } => { write!( f, "Unidentified error: apt failed with retcode {}: {:?}\n{}", retcode, args, lines.join("\n") ) } Error::Detailed { retcode, args, error, } => { write!( f, "Detailed error: apt failed with retcode {}: {:?}\n{}", retcode, args, error.as_ref().map_or("".to_string(), |e| e.to_string()) ) } Error::Session(error) => write!(f, "{:?}", error), Error::FileSearch(error) => write!(f, "File search error: {:?}", error), } } } impl From for Error { fn from(error: crate::session::Error) -> Self { Error::Session(error) } } impl From for Error { fn from(error: crate::debian::file_search::Error) -> Self { Error::FileSearch(error) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::Unidentified { retcode, args, lines, secondary: _, } => { write!( f, "apt failed with retcode {}: {:?}\n{}", retcode, args, lines.join("\n") ) } Error::Detailed { retcode, args, error, } => { write!( f, "apt failed with retcode {}: {:?}\n{}", retcode, args, error.as_ref().map_or("".to_string(), |e| e.to_string()) ) } Error::Session(error) => write!(f, "{}", error), Error::FileSearch(error) => write!(f, "{}", error), } } } impl std::error::Error for Error {} /// Manager for APT operations. /// /// This struct provides methods for interacting with APT, /// including dependency resolution and package installation. pub struct AptManager<'a> { /// The session to run APT commands in. session: &'a dyn Session, /// Command prefix (e.g., "sudo") for APT commands. prefix: Vec, /// File searchers for finding packages containing files. searchers: RwLock + 'a>>>>, } /// Entry for APT satisfy command. /// /// Represents either a required package or a package conflict. pub enum SatisfyEntry { /// A required package dependency. Required(String), /// A package that conflicts with installation. Conflict(String), } impl<'a> AptManager<'a> { /// Get the session associated with this APT manager. /// /// # Returns /// Reference to the session pub fn session(&self) -> &'a dyn Session { self.session } /// Create a new APT manager. /// /// # Arguments /// * `session` - Session to run APT commands in /// * `prefix` - Optional command prefix (e.g., "sudo") /// /// # Returns /// A new AptManager instance pub fn new(session: &'a dyn Session, prefix: Option>) -> Self { Self { session, prefix: prefix.unwrap_or_default(), searchers: RwLock::new(None), } } /// Set file searchers for finding packages containing files. /// /// # Arguments /// * `searchers` - List of file searchers to use pub fn set_searchers( &self, searchers: Vec + 'a>>, ) { *self.searchers.write().unwrap() = Some(searchers); } /// Create a new APT manager from a session with appropriate sudo prefix. /// /// Automatically adds "sudo" to the command prefix if the session user is not root. /// /// # Arguments /// * `session` - Session to run APT commands in /// /// # Returns /// A new AptManager instance pub fn from_session(session: &'a dyn Session) -> Self { let prefix = if get_user(session).as_str() != "root" { vec!["sudo".to_string()] } else { vec![] }; return Self::new(session, Some(prefix)); } /// Run an APT command with the configured prefix. /// /// # Arguments /// * `args` - Arguments to pass to APT /// /// # Returns /// Ok on success, Error otherwise fn run_apt(&self, args: Vec<&str>) -> Result<(), Error> { run_apt( self.session, args, self.prefix.iter().map(|s| s.as_str()).collect(), ) } /// Satisfy package dependencies using APT. /// /// # Arguments /// * `deps` - List of dependencies to satisfy (required or conflicts) /// /// # Returns /// Ok on success, Error if dependencies cannot be satisfied pub fn satisfy(&self, deps: Vec) -> Result<(), Error> { let mut args = vec!["satisfy".to_string()]; args.extend(deps.iter().map(|dep| match dep { SatisfyEntry::Required(s) => s.clone(), SatisfyEntry::Conflict(s) => format!("Conflict: {}", s), })); self.run_apt(args.iter().map(|s| s.as_str()).collect()) } /// Generate a satisfy command for the given dependencies. /// /// # Arguments /// * `deps` - List of dependency strings /// /// # Returns /// Command-line arguments for satisfying the dependencies pub fn satisfy_command<'b>(&'b self, deps: Vec<&'b str>) -> Vec<&'b str> { let mut args = self .prefix .iter() .map(|s| s.as_str()) .collect::>(); args.push("apt"); args.push("satisfy"); args.extend(deps); args } /// Find packages that contain the specified paths. /// /// # Arguments /// * `paths` - List of file paths to search for /// * `regex` - Whether to treat paths as regular expressions /// * `case_insensitive` - Whether to ignore case in path matching /// /// # Returns /// List of package names that contain the paths pub fn get_packages_for_paths( &self, paths: Vec<&str>, regex: bool, case_insensitive: bool, ) -> Result, Error> { if regex { log::debug!("Searching for packages containing regexes {:?}", paths); } else { log::debug!("Searching for packages containing {:?}", paths); } if self.searchers.read().unwrap().is_none() { *self.searchers.write().unwrap() = Some(vec![ crate::debian::file_search::get_apt_contents_file_searcher(self.session)?, Box::new(crate::debian::file_search::GENERATED_FILE_SEARCHER.clone()), ]); } Ok(crate::debian::file_search::get_packages_for_paths( paths, self.searchers .read() .unwrap() .as_ref() .unwrap() .iter() .map(|s| s.as_ref()) .collect::>() .as_slice(), regex, case_insensitive, )) } } /// Find simple Debian dependencies for the given paths. /// /// # Arguments /// * `apt_mgr` - APT manager to use /// * `paths` - List of file paths to search for /// * `regex` - Whether to treat paths as regular expressions /// * `case_insensitive` - Whether to ignore case in path matching /// /// # Returns /// List of Debian dependencies for packages containing the paths pub fn find_deps_simple( apt_mgr: &AptManager, paths: Vec<&str>, regex: bool, case_insensitive: bool, ) -> Result, Error> { let packages = apt_mgr.get_packages_for_paths(paths, regex, case_insensitive)?; Ok(packages .iter() .map(|package| DebianDependency::simple(package)) .collect()) } /// Find Debian dependencies with minimum version for the given paths. /// /// # Arguments /// * `apt_mgr` - APT manager to use /// * `paths` - List of file paths to search for /// * `regex` - Whether to treat paths as regular expressions /// * `minimum_version` - Minimum version requirement /// * `case_insensitive` - Whether to ignore case in path matching /// /// # Returns /// List of versioned Debian dependencies for packages containing the paths pub fn find_deps_with_min_version( apt_mgr: &AptManager, paths: Vec<&str>, regex: bool, minimum_version: &Version, case_insensitive: bool, ) -> Result, Error> { let packages = apt_mgr.get_packages_for_paths(paths, regex, case_insensitive)?; Ok(packages .iter() .map(|package| DebianDependency::new_with_min_version(package, minimum_version)) .collect()) } /// Run an APT command with the given prefix. /// /// # Arguments /// * `session` - Session to run the command in /// * `args` - Arguments to pass to APT /// * `prefix` - Command prefix (e.g., "sudo") /// /// # Returns /// Ok on success, Error otherwise pub fn run_apt(session: &dyn Session, args: Vec<&str>, prefix: Vec<&str>) -> Result<(), Error> { let args = [prefix, vec!["apt", "-y"], args].concat(); log::info!("apt: running {:?}", args); let (status, mut lines) = session .command(args.clone()) .cwd(std::path::Path::new("/")) .user("root") .run_with_tee()?; if status.success() { return Ok(()); } let (r#match, error) = buildlog_consultant::apt::find_apt_get_failure(lines.iter().map(|s| s.as_str()).collect()); if let Some(error) = error { return Err(Error::Detailed { retcode: status.code().unwrap_or(1), args: args.iter().map(|s| s.to_string()).collect(), error: Some(error), }); } while lines.last().map_or(false, |line| line.trim().is_empty()) { lines.pop(); } return Err(Error::Unidentified { retcode: status.code().unwrap_or(1), args: args.iter().map(|s| s.to_string()).collect(), lines, secondary: r#match, }); } /// Pick the best Debian dependency from a list of candidates. /// /// Uses tie breakers to determine the best dependency when multiple candidates exist. /// /// # Arguments /// * `dependencies` - List of Debian dependency candidates /// * `tie_breakers` - List of tie breakers to use /// /// # Returns /// The best dependency, or None if no candidates are available fn pick_best_deb_dependency( mut dependencies: Vec, tie_breakers: &[Box], ) -> Option { if dependencies.is_empty() { return None; } if dependencies.len() == 1 { return Some(dependencies.remove(0)); } log::warn!("Multiple candidates for dependency {:?}", dependencies); for tie_breaker in tie_breakers { let winner = tie_breaker.break_tie(dependencies.iter().collect::>().as_slice()); if let Some(winner) = winner { return Some(winner.clone()); } } log::info!( "No tie breaker could determine a winner for dependency {:?}", dependencies ); Some(dependencies.remove(0)) } /// Convert a generic dependency to possible Debian dependencies. /// /// Attempts to convert a dependency to Debian-specific dependencies using /// various conversion strategies for different dependency types. /// /// # Arguments /// * `apt` - APT manager to use for lookups /// * `dep` - The generic dependency to convert /// /// # Returns /// List of possible Debian dependencies pub fn dependency_to_possible_deb_dependencies( apt: &AptManager, dep: &dyn Dependency, ) -> Vec { let mut candidates = vec![]; macro_rules! try_into_debian_dependency { ($apt:expr, $dep:expr, $type:ty) => { if let Some(dep) = $dep.as_any().downcast_ref::<$type>() { if let Some(alts) = dep.try_into_debian_dependency($apt) { candidates.extend(alts); } } }; } // TODO: More idiomatic way to do this? try_into_debian_dependency!(apt, dep, crate::dependencies::go::GoPackageDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::go::GoDependency); try_into_debian_dependency!( apt, dep, crate::dependencies::haskell::HaskellPackageDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::java::JavaClassDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::java::JDKDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::java::JREDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::java::JDKFileDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::BinaryDependency); try_into_debian_dependency!( apt, dep, crate::dependencies::pytest::PytestPluginDependency ); try_into_debian_dependency!( apt, dep, crate::dependencies::VcsControlDirectoryAccessDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::CargoCrateDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::PkgConfigDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::PathDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::CHeaderDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::JavaScriptRuntimeDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::ValaPackageDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::RubyGemDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::DhAddonDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::LibraryDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::StaticLibraryDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::RubyFileDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::xml::XmlEntityDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::SprocketsFileDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::CMakeFileDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::MavenArtifactDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::GnomeCommonDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::QtModuleDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::QTDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::X11Dependency); try_into_debian_dependency!( apt, dep, crate::dependencies::CertificateAuthorityDependency ); try_into_debian_dependency!( apt, dep, crate::dependencies::autoconf::AutoconfMacroDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::LibtoolDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::BoostComponentDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::KF5ComponentDependency); try_into_debian_dependency!( apt, dep, crate::dependencies::IntrospectionTypelibDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::node::NodePackageDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::node::NodeModuleDependency); try_into_debian_dependency!( apt, dep, crate::dependencies::octave::OctavePackageDependency ); try_into_debian_dependency!( apt, dep, crate::dependencies::perl::PerlPreDeclaredDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::perl::PerlModuleDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::perl::PerlFileDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::php::PhpClassDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::php::PhpExtensionDependency); try_into_debian_dependency!( apt, dep, crate::dependencies::python::PythonModuleDependency ); try_into_debian_dependency!( apt, dep, crate::dependencies::python::PythonPackageDependency ); try_into_debian_dependency!(apt, dep, crate::dependencies::python::PythonDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::r::RPackageDependency); try_into_debian_dependency!(apt, dep, crate::dependencies::vague::VagueDependency); candidates } /// Convert a generic dependency to the best Debian dependency. /// /// First finds all possible Debian dependencies for the given generic dependency, /// then uses tie breakers to pick the best one if multiple candidates exist. /// /// # Arguments /// * `apt` - APT manager to use for lookups /// * `dep` - The generic dependency to convert /// * `tie_breakers` - List of tie breakers to use /// /// # Returns /// The best Debian dependency, or None if no candidates are available pub fn dependency_to_deb_dependency( apt: &AptManager, dep: &dyn Dependency, tie_breakers: &[Box], ) -> Result, InstallerError> { let mut candidates = dependency_to_possible_deb_dependencies(apt, dep); if candidates.is_empty() { log::debug!("No Debian dependency candidates for dependency {:?}", dep); Ok(None) } else if candidates.len() == 1 { let deb_dep = candidates.remove(0); log::debug!( "Only one Debian dependency candidate for dependency {:?}: {:?}", dep, deb_dep ); Ok(Some(deb_dep)) } else { Ok(pick_best_deb_dependency(candidates, tie_breakers)) } } /// Installer that uses APT to install dependencies. /// /// This installer converts generic dependencies to Debian package dependencies /// and installs them using APT. pub struct AptInstaller<'a> { /// The APT manager to use for package operations apt: AptManager<'a>, /// Tie breakers for selecting among multiple dependency candidates tie_breakers: Vec>, } impl<'a> AptInstaller<'a> { /// Create a new APT installer with default tie breakers. /// /// # Arguments /// * `apt` - APT manager to use /// /// # Returns /// A new AptInstaller instance pub fn new(apt: AptManager<'a>) -> Self { let tie_breakers = default_tie_breakers(apt.session); Self { apt, tie_breakers } } /// Create a new APT installer with custom tie breakers. /// /// # Arguments /// * `apt` - APT manager to use /// * `tie_breakers` - Custom tie breakers for selecting among dependencies /// /// # Returns /// A new AptInstaller instance pub fn new_with_tie_breakers( apt: AptManager<'a>, tie_breakers: Vec>, ) -> Self { Self { apt, tie_breakers } } /// Create a new APT installer from a session. /// /// Creates an APT manager with appropriate sudo prefix if needed. /// /// # Arguments /// * `session` - Session to run APT commands in /// /// # Returns /// A new AptInstaller instance pub fn from_session(session: &'a dyn Session) -> Self { Self::new(AptManager::from_session(session)) } } /// Implementation of the Installer trait for AptInstaller. impl<'a> Installer for AptInstaller<'a> { /// Install a dependency using APT. /// /// Only supports the Global installation scope. /// /// # Arguments /// * `dep` - Dependency to install /// * `scope` - Installation scope /// /// # Returns /// Ok on success, Error if installation fails fn install( &self, dep: &dyn Dependency, scope: InstallationScope, ) -> Result<(), InstallerError> { match scope { InstallationScope::User => { return Err(InstallerError::UnsupportedScope(scope)); } InstallationScope::Global => {} InstallationScope::Vendor => { return Err(InstallerError::UnsupportedScope(scope)); } } let apt_deb = if let Some(apt_deb) = dependency_to_deb_dependency(&self.apt, dep, self.tie_breakers.as_slice())? { apt_deb } else { return Err(InstallerError::UnknownDependencyFamily); }; match self .apt .satisfy(vec![SatisfyEntry::Required(apt_deb.relation_string())]) { Ok(_) => {} Err(e) => { return Err(InstallerError::Other(e.to_string())); } } Ok(()) } /// Explain how to install a dependency using APT. /// /// # Arguments /// * `dep` - Dependency to explain /// * `_scope` - Installation scope (ignored) /// /// # Returns /// An explanation with message and optional command fn explain( &self, dep: &dyn Dependency, _scope: InstallationScope, ) -> Result { let apt_deb = if let Some(apt_deb) = dependency_to_deb_dependency(&self.apt, dep, self.tie_breakers.as_slice())? { apt_deb } else { return Err(InstallerError::UnknownDependencyFamily); }; let apt_deb_str = apt_deb.relation_string(); let cmd = self.apt.satisfy_command(vec![apt_deb_str.as_str()]); Ok(Explanation { message: format!( "Install {}", apt_deb .package_names() .iter() .map(|x| x.as_str()) .collect::>() .join(", ") ), command: Some(cmd.iter().map(|s| s.to_string()).collect()), }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_pick_best_deb_dependency() { struct DummyTieBreaker; impl crate::dependencies::debian::TieBreaker for DummyTieBreaker { fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { reqs.iter().next().cloned() } } let mut tie_breakers = vec![Box::new(DummyTieBreaker) as Box]; let dep1 = DebianDependency::new("libssl-dev"); let dep2 = DebianDependency::new("libssl1.1-dev"); // Single dependency assert_eq!( pick_best_deb_dependency(vec![dep1.clone()], tie_breakers.as_mut_slice()), Some(dep1.clone()) ); // No dependencies assert_eq!( pick_best_deb_dependency(vec![], tie_breakers.as_mut_slice()), None ); // Multiple dependencies assert_eq!( pick_best_deb_dependency( vec![dep1.clone(), dep2.clone()], tie_breakers.as_mut_slice() ), Some(dep1.clone()) ); } } ognibuild-0.2.6/src/debian/build.rs000064400000000000000000000760751046102023000153020ustar 00000000000000//! Debian package building functionality. //! //! This module provides utilities for building Debian packages, including //! functions for managing changelog entries, running build commands, //! and handling build failures. use breezyshim::workingtree::PyWorkingTree; use buildlog_consultant::Problem; use debian_changelog::{ChangeLog, Urgency}; use debversion::Version; use std::io::Seek; use std::path::{Path, PathBuf}; /// Get the current Debian build architecture. /// /// Uses dpkg-architecture to determine the build architecture. /// /// # Returns /// The build architecture string (e.g., "amd64") /// /// # Panics /// Panics if dpkg-architecture is not available or fails to run. pub fn get_build_architecture() -> String { std::process::Command::new("dpkg-architecture") .arg("-qDEB_BUILD_ARCH") .output() .map(|output| String::from_utf8(output.stdout).unwrap().trim().to_string()) .unwrap() } /// Default build command for Debian packages. /// /// Uses sbuild with the --no-clean-source option. pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source"; /// Get the path to the current Python interpreter. /// /// Uses PyO3 to get the path from sys.executable. /// /// # Returns /// The path to the Python interpreter as a String. fn python_command() -> String { pyo3::Python::attach(|py| { use pyo3::types::PyAnyMethods; let sys_module = py.import("sys").unwrap(); sys_module .getattr("executable") .unwrap() .extract::() .unwrap() }) } /// Generate the command for building Debian packages with bzr-builddeb. /// /// # Arguments /// * `build_command` - Custom build command to use (defaults to DEFAULT_BUILDER) /// * `result_dir` - Directory to store build results /// * `apt_repository` - APT repository to use /// * `apt_repository_key` - APT repository key to use /// * `extra_repositories` - Additional APT repositories /// /// # Returns /// Vector of command line arguments for the build command pub fn builddeb_command( build_command: Option<&str>, result_dir: Option<&std::path::Path>, apt_repository: Option<&str>, apt_repository_key: Option<&str>, extra_repositories: Option<&Vec<&str>>, ) -> Vec { let mut build_command = build_command.unwrap_or(DEFAULT_BUILDER).to_string(); if let Some(extra_repositories) = extra_repositories { for repo in extra_repositories { build_command.push_str(&format!( " --extra-repository={}", shlex::try_quote(repo).unwrap() )); } } let mut args = vec![ python_command(), "-m".to_string(), "breezy".to_string(), "builddeb".to_string(), "--guess-upstream-branch-url".to_string(), format!("--builder={}", build_command), ]; if let Some(apt_repository) = apt_repository { args.push(format!("--apt-repository={}", apt_repository)); } if let Some(apt_repository_key) = apt_repository_key { args.push(format!("--apt-repository-key={}", apt_repository_key)); } if let Some(result_dir) = result_dir { args.push(format!("--result-dir={}", result_dir.to_string_lossy())); } args } /// Error indicating a build failure with an exit code. #[derive(Debug)] pub struct BuildFailedError(pub i32); impl std::fmt::Display for BuildFailedError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Build failed: {}", self.0) } } impl std::error::Error for BuildFailedError {} /// Build a Debian package from source. /// /// # Arguments /// * `local_tree` - Working tree containing the package source /// * `outf` - File to write build output to /// * `build_command` - Command to use for building /// * `result_dir` - Directory to store build results /// * `distribution` - Debian distribution to target /// * `subpath` - Path within the tree where the package is located /// * `source_date_epoch` - Source date epoch to use for reproducible builds /// * `apt_repository` - APT repository to use /// * `apt_repository_key` - APT repository key to use /// * `extra_repositories` - Additional APT repositories /// /// # Returns /// Ok on success, BuildFailedError with exit code on failure pub fn build( local_tree: &dyn PyWorkingTree, outf: std::fs::File, build_command: &str, result_dir: Option<&std::path::Path>, distribution: Option<&str>, subpath: &std::path::Path, source_date_epoch: Option>, apt_repository: Option<&str>, apt_repository_key: Option<&str>, extra_repositories: Option<&Vec<&str>>, ) -> Result<(), BuildFailedError> { let args = builddeb_command( Some(build_command), result_dir, apt_repository, apt_repository_key, extra_repositories, ); // Make a copy of the environment variables let mut env = std::env::vars().collect::>(); if let Some(distribution) = distribution { env.insert("DISTRIBUTION".to_owned(), distribution.to_owned()); } if let Some(source_date_epoch) = source_date_epoch { env.insert( "SOURCE_DATE_EPOCH".to_owned(), format!("{}", source_date_epoch.timestamp()), ); } log::info!("Building debian packages, running {}.", build_command); match std::process::Command::new(&args[0]) .args(&args[1..]) .current_dir(local_tree.abspath(subpath).unwrap()) .stdout(outf.try_clone().unwrap()) .stderr(outf) .envs(env) .status() { Ok(status) => { if status.success() { log::info!("Build succeeded."); Ok(()) } else { Err(BuildFailedError(status.code().unwrap_or(1))) } } Err(e) => { log::error!("Failed to run build command: {}", e); Err(BuildFailedError(1)) } } } /// Default filename for build logs. pub const BUILD_LOG_FILENAME: &str = "build.log"; #[derive(Debug)] /// Error that can occur during a build attempt. /// /// Contains details about the build failure, including stage, phase, /// return code, command, and error information. pub enum BuildOnceError { /// Detailed error with specific problem information. Detailed { /// Build stage where failure occurred. stage: Option, /// Build phase where failure occurred. phase: Option, /// Process return code. retcode: i32, /// Command that was run. command: Vec, /// Specific error that was detected. error: Box, /// Human-readable description of the error. description: String, }, /// Unidentified error without specific problem information. Unidentified { /// Build stage where failure occurred. stage: Option, /// Build phase where failure occurred. phase: Option, /// Process return code. retcode: i32, /// Command that was run. command: Vec, /// Human-readable description of the error. description: String, }, } /// Result of a successful build attempt. /// /// Contains information about the built package, including source package name, /// version, and paths to changes files. pub struct BuildOnceResult { /// Name of the source package. pub source_package: String, /// Version of the built package. pub version: debversion::Version, /// Paths to the generated .changes files. pub changes_names: Vec, } /// Build a Debian package once and capture detailed error information. /// /// This function builds a package and provides more detailed error information /// than the basic `build` function by parsing the build log. /// /// # Arguments /// * `local_tree` - Working tree containing the package source /// * `build_suite` - Debian distribution to target /// * `output_directory` - Directory to store build results /// * `build_command` - Command to use for building /// * `subpath` - Path within the tree where the package is located /// * `source_date_epoch` - Source date epoch to use for reproducible builds /// * `apt_repository` - APT repository to use /// * `apt_repository_key` - APT repository key to use /// * `extra_repositories` - Additional APT repositories /// /// # Returns /// BuildOnceResult on success, detailed BuildOnceError on failure pub fn build_once( local_tree: &dyn PyWorkingTree, build_suite: Option<&str>, output_directory: &Path, build_command: &str, subpath: &Path, source_date_epoch: Option>, apt_repository: Option<&str>, apt_repository_key: Option<&str>, extra_repositories: Option<&Vec<&str>>, ) -> Result { use buildlog_consultant::problems::debian::DpkgSourceLocalChanges; use buildlog_consultant::sbuild::{worker_failure_from_sbuild_log, SbuildLog}; let build_log_path = output_directory.join(BUILD_LOG_FILENAME); log::debug!("Writing build log to {}", build_log_path.display()); let mut logf = std::fs::File::create(&build_log_path).unwrap(); match build( local_tree, logf.try_clone().unwrap(), build_command, Some(output_directory), build_suite, subpath, source_date_epoch, apt_repository, apt_repository_key, extra_repositories, ) { Ok(()) => (), Err(e) => { logf.sync_all().unwrap(); logf.seek(std::io::SeekFrom::Start(0)).unwrap(); let sbuildlog = SbuildLog::try_from(std::fs::File::open(&build_log_path).unwrap()).unwrap(); let sbuild_failure = worker_failure_from_sbuild_log(&sbuildlog); // Preserve the diff for later inspection if let Some(error) = sbuild_failure .error .as_ref() .and_then(|e| e.as_any().downcast_ref::()) { if let Some(diff_file) = error.diff_file.as_ref() { let diff_file_name = output_directory.join(Path::new(&diff_file).file_name().unwrap()); std::fs::copy(diff_file, &diff_file_name).unwrap(); } } let retcode = e.0; if let Some(error) = sbuild_failure.error { return Err(BuildOnceError::Detailed { stage: sbuild_failure.stage, phase: sbuild_failure.phase, retcode, command: shlex::split(build_command).unwrap(), error, description: sbuild_failure.description.unwrap_or_default(), }); } else { return Err(BuildOnceError::Unidentified { stage: sbuild_failure.stage, phase: sbuild_failure.phase, retcode, command: shlex::split(build_command).unwrap(), description: sbuild_failure .description .unwrap_or_else(|| format!("Build failed with exit code {}", retcode)), }); } } } let (package, version) = get_last_changelog_entry(local_tree, subpath); let mut changes_names = vec![]; for (_kind, entry) in find_changes_files(output_directory, &package, &version) { changes_names.push(entry.path()); } Ok(BuildOnceResult { source_package: package, version, changes_names, }) } /// Check if Debian control files are in the root of the project. /// /// # Arguments /// * `tree` - Tree to check for control files /// * `subpath` - Path within the tree to check /// /// # Returns /// true if control files are in root, false if they are in a debian/ directory fn control_files_in_root(tree: &dyn PyWorkingTree, subpath: &std::path::Path) -> bool { let debian_path = subpath.join("debian"); if tree.has_filename(&debian_path) { return false; } let control_path = subpath.join("control"); if tree.has_filename(&control_path) { return true; } tree.has_filename(std::path::Path::new(&format!( "{}.in", control_path.to_string_lossy() ))) } /// Get the last entry from the debian/changelog file. /// /// # Arguments /// * `local_tree` - Working tree containing the package source /// * `subpath` - Path within the tree where the package is located /// /// # Returns /// Tuple of (package name, version) from the last changelog entry fn get_last_changelog_entry( local_tree: &dyn PyWorkingTree, subpath: &std::path::Path, ) -> (String, debversion::Version) { let path = if control_files_in_root(local_tree, subpath) { subpath.join("changelog") } else { subpath.join("debian/changelog") }; let f = local_tree.get_file(&path).unwrap(); let cl = ChangeLog::read_relaxed(f).unwrap(); let e = cl.iter().next().unwrap(); (e.package().unwrap(), e.version().unwrap()) } /// Run gbp-dch to update the changelog. /// /// # Arguments /// * `path` - Path to the package directory /// /// # Returns /// Ok on success, Error if gbp-dch fails pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> { let cmd = std::process::Command::new("gbp-dch") .arg("--ignore-branch") .current_dir(path) .output()?; if !cmd.status.success() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "gbp-dch failed", )); } Ok(()) } /// Find changes files for a specific package and version. /// /// # Arguments /// * `path` - Directory to search for changes files /// * `package` - Package name to match /// * `version` - Version to match /// /// # Returns /// Iterator of (architecture, DirEntry) pairs for matching changes files pub fn find_changes_files( path: &std::path::Path, package: &str, version: &debversion::Version, ) -> impl Iterator { let mut non_epoch_version = version.upstream_version.to_string(); if let Some(debian_version) = version.debian_revision.as_ref() { non_epoch_version.push_str(&format!("-{}", debian_version)); } let regex = format!( "{}_{}_(.*)", regex::escape(package), regex::escape(&non_epoch_version) ); let c = regex::Regex::new(®ex).unwrap(); std::fs::read_dir(path).unwrap().filter_map(move |entry| { let entry = entry.unwrap(); c.captures(entry.file_name().to_str().unwrap()) .map(|m| (m.get(1).unwrap().as_str().to_owned(), entry)) }) } /// Attempt a build, with a custom distribution set. /// /// # Arguments /// * `local_tree` - The tree to build in. /// * `suffix` - Suffix to add to version string. /// * `build_suite` - Name of suite (i.e. distribution) to build for. /// * `output_directory` - Directory to write output to. /// * `build_command` - Build command to build package. /// * `build_changelog_entry` - Changelog entry to use. /// * `subpath` - Sub path in tree where package lives. /// * `source_date_epoch` - Source date epoch to set. /// * `run_gbp_dch` - Whether to run gbp-dch. /// * `apt_repository` - APT repository to use. /// * `apt_repository_key` - APT repository key to use. /// * `extra_repositories` - Extra repositories to use. pub fn attempt_build( local_tree: &dyn PyWorkingTree, suffix: Option<&str>, build_suite: Option<&str>, output_directory: &std::path::Path, build_command: &str, build_changelog_entry: Option<&str>, subpath: &std::path::Path, source_date_epoch: Option>, run_gbp_dch: bool, apt_repository: Option<&str>, apt_repository_key: Option<&str>, extra_repositories: Option<&Vec<&str>>, ) -> Result { if run_gbp_dch && subpath == std::path::Path::new("") && local_tree .abspath(std::path::Path::new(".git")) .map_or(false, |p| std::path::Path::new(&p).exists()) { gbp_dch(&local_tree.abspath(subpath).unwrap()).unwrap(); } if let Some(build_changelog_entry) = build_changelog_entry { assert!( suffix.is_some(), "build_changelog_entry specified, but suffix is None" ); assert!( build_suite.is_some(), "build_changelog_entry specified, but build_suite is None" ); add_dummy_changelog_entry( local_tree, subpath, suffix.unwrap(), build_suite.unwrap(), build_changelog_entry, None, None, ); } build_once( local_tree, build_suite, output_directory, build_command, subpath, source_date_epoch, apt_repository, apt_repository_key, extra_repositories, ) } /// Add a suffix to a version string. /// /// If the version already has the same suffix, increments the number at the end. /// /// # Arguments /// * `version` - Version to modify /// * `suffix` - Suffix to add to the version /// /// # Returns /// New version with the suffix added or incremented pub fn version_add_suffix(version: &Version, suffix: &str) -> Version { fn add_suffix(v: &str, suffix: &str) -> String { if let Some(m) = regex::Regex::new(&format!("(.*)({})([0-9]+)", regex::escape(suffix))) .unwrap() .captures(v) { let main = m.get(1).unwrap().as_str(); let suffix = m.get(2).unwrap().as_str(); let revision = m.get(3).unwrap().as_str(); format!("{}{}{}", main, suffix, revision.parse::().unwrap() + 1) } else { format!("{}{}1", v, suffix) } } let mut version = version.clone(); if let Some(r) = version.debian_revision { version.debian_revision = Some(add_suffix(&r, suffix)); } else { version.upstream_version = add_suffix(&version.upstream_version, suffix); } version } /// Add a dummy changelog entry to a package. /// /// # Arguments /// * `tree` - The tree to add the entry to. /// * `subpath` - Sub path in tree where package lives. /// * `suffix` - Suffix to add to version string. /// * `suite` - Name of suite (i.e. distribution) to build for. /// * `message` - Changelog message to use. /// * `timestamp` - Timestamp to use. /// * `maintainer` - Maintainer to use. /// * `allow_reformatting` - Whether to allow reformatting. /// /// # Returns /// The version of the newly added entry. pub fn add_dummy_changelog_entry( tree: &dyn PyWorkingTree, subpath: &Path, suffix: &str, suite: &str, message: &str, timestamp: Option>, maintainer: Option<(String, String)>, ) -> Version { let path = if control_files_in_root(tree, subpath) { subpath.join("changelog") } else { subpath.join("debian/changelog") }; let mut cl = ChangeLog::read_relaxed(tree.get_file(&path).unwrap()).unwrap(); let prev_entry = cl.iter().next().unwrap(); let prev_version = prev_entry.version().unwrap(); let version = version_add_suffix(&prev_version, suffix); log::debug!("Adding dummy changelog entry {} for build", &version); let mut entry = cl.auto_add_change( &[&format!("* {}", message)], maintainer.unwrap_or_else(|| debian_changelog::get_maintainer().unwrap()), timestamp.map(|t| t.into()), Some(Urgency::Low), ); entry.set_version(&version); entry.set_distributions(vec![suite.to_string()]); tree.put_file_bytes_non_atomic(&path, cl.to_string().as_bytes()) .unwrap(); entry.version().unwrap() } #[cfg(test)] mod tests { use super::*; use breezyshim::tree::MutableTree; use std::fs::File; use tempfile::tempdir; #[test] #[cfg(target_os = "linux")] fn test_get_build_architecture() { let arch = get_build_architecture(); assert!(!arch.is_empty() && arch.len() < 10); } #[test] #[cfg(not(target_os = "linux"))] #[should_panic(expected = "No such file or directory")] fn test_get_build_architecture_panics_on_non_linux() { // This test verifies that the function panics when dpkg-architecture isn't available let _ = get_build_architecture(); } #[test] fn test_build_fails_with_invalid_command() { // Set up a test environment let td = tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); // Create a file to capture output let log_file = File::create(td.path().join("build.log")).unwrap(); // Test with a command that will definitely fail let result = build( &tree, log_file, "command_that_does_not_exist", None, None, Path::new(""), None, None, None, None, ); // Verify the build fails assert!(result.is_err()); if let Err(BuildFailedError(code)) = result { // The code can be 1 on Linux or other values on different platforms assert!(code > 0); } else { panic!("Expected BuildFailedError"); } } #[test] fn test_build_once_error_conversion() { // Test conversion between BuildFailedError and BuildOnceError // Set up a test environment let td = tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); // Create a stub debian directory structure std::fs::create_dir_all(td.path().join("debian")).unwrap(); // Create minimal changelog file std::fs::write( td.path().join("debian/changelog"), r#"test-package (1.0) unstable; urgency=low * Initial release -- Test User Thu, 01 Jan 2023 00:00:00 +0000 "#, ) .unwrap(); tree.add(&[Path::new("debian"), Path::new("debian/changelog")]) .unwrap(); // Build will fail but we're testing error conversion let output_dir = tempdir().unwrap(); let result = build_once( &tree, None, output_dir.path(), "command_that_does_not_exist", Path::new(""), None, None, None, None, ); assert!(result.is_err()); match result { Err(BuildOnceError::Unidentified { .. }) | Err(BuildOnceError::Detailed { .. }) => { // Success - proper error conversion happened } _ => panic!("Expected Unidentified or Detailed BuildOnceError"), } } #[test] fn test_builddeb_command() { let command = builddeb_command( Some("sbuild --no-clean-source"), Some(std::path::Path::new("/tmp")), Some("ppa:my-ppa/ppa"), Some("my-ppa-key"), Some(&vec!["deb http://example.com/debian buster main"]), ); assert_eq!(command, vec![ python_command(), "-m".to_string(), "breezy".to_string(), "builddeb".to_string(), "--guess-upstream-branch-url".to_string(), "--builder=sbuild --no-clean-source --extra-repository='deb http://example.com/debian buster main'".to_string(), "--apt-repository=ppa:my-ppa/ppa".to_string(), "--apt-repository-key=my-ppa-key".to_string(), "--result-dir=/tmp".to_string(), ]); } #[test] fn test_python_command() { let _ = python_command(); } #[test] fn test_control_files_not_in_root() { let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); let subpath = std::path::Path::new(""); tree.mkdir(&subpath.join("debian")).unwrap(); tree.put_file_bytes_non_atomic(&subpath.join("debian/control"), b"") .unwrap(); assert!(!control_files_in_root(&tree, subpath)); } #[test] fn test_control_files_in_root() { let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); let subpath = std::path::Path::new(""); tree.put_file_bytes_non_atomic(&subpath.join("control"), b"") .unwrap(); assert!(control_files_in_root(&tree, subpath)); } mod test_version_add_suffix { use super::*; #[test] fn test_native() { assert_eq!( "1.0~jan+lint4".parse::().unwrap(), version_add_suffix(&"1.0~jan+lint3".parse().unwrap(), "~jan+lint"), ); assert_eq!( "1.0~jan+lint1".parse::().unwrap(), version_add_suffix(&"1.0".parse().unwrap(), "~jan+lint"), ); } #[test] fn test_normal() { assert_eq!( "1.0-1~jan+lint4".parse::().unwrap(), version_add_suffix(&"1.0-1~jan+lint3".parse().unwrap(), "~jan+lint"), ); assert_eq!( "1.0-1~jan+lint1".parse::().unwrap(), version_add_suffix(&"1.0-1".parse().unwrap(), "~jan+lint"), ); assert_eq!( "0.0.12-1~jan+lint1".parse::().unwrap(), version_add_suffix(&"0.0.12-1".parse().unwrap(), "~jan+lint"), ); assert_eq!( "0.0.12-1~jan+unchanged1~jan+lint1" .parse::() .unwrap(), version_add_suffix(&"0.0.12-1~jan+unchanged1".parse().unwrap(), "~jan+lint"), ); } } mod test_add_dummy_changelog { use super::*; use std::path::Path; #[test] fn test_simple() { let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); std::fs::create_dir(td.path().join("debian")).unwrap(); std::fs::write( td.path().join("debian/changelog"), r#"janitor (0.1-1) UNRELEASED; urgency=medium * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, ) .unwrap(); tree.add(&[Path::new("debian"), Path::new("debian/changelog")]) .unwrap(); add_dummy_changelog_entry( &tree, Path::new(""), "jan+some", "some-fixes", "Dummy build.", Some( chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z") .unwrap() .to_utc(), ), Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())), ); let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap(); assert_eq!( r#"janitor (0.1-1jan+some1) some-fixes; urgency=medium * Initial release. (Closes: #XXXXXX) * Dummy build. -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, contents ); } #[test] fn test_native() { let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); std::fs::create_dir(td.path().join("debian")).unwrap(); std::fs::write( td.path().join("debian/changelog"), r#"janitor (0.1) UNRELEASED; urgency=medium * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, ) .unwrap(); tree.add(&[Path::new("debian"), Path::new("debian/changelog")]) .unwrap(); add_dummy_changelog_entry( &tree, Path::new(""), "jan+some", "some-fixes", "Dummy build.", Some( chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z") .unwrap() .to_utc(), ), Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())), ); let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap(); assert_eq!( r#"janitor (0.1jan+some1) some-fixes; urgency=medium * Initial release. (Closes: #XXXXXX) * Dummy build. -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, contents ); } #[test] fn test_exists() { let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); std::fs::create_dir(td.path().join("debian")).unwrap(); std::fs::write( td.path().join("debian/changelog"), r#"janitor (0.1-1jan+some1) UNRELEASED; urgency=medium * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, ) .unwrap(); tree.add(&[Path::new("debian"), Path::new("debian/changelog")]) .unwrap(); add_dummy_changelog_entry( &tree, Path::new(""), "jan+some", "some-fixes", "Dummy build.", Some( chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z") .unwrap() .to_utc(), ), Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())), ); let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap(); assert_eq!( r#"janitor (0.1-1jan+some2) some-fixes; urgency=medium * Initial release. (Closes: #XXXXXX) * Dummy build. -- Jelmer Vernooij Sat, 05 Sep 2020 12:35:04 -0000 "#, contents ); } } } ognibuild-0.2.6/src/debian/build_deps.rs000064400000000000000000000131531046102023000163010ustar 00000000000000//! Debian build dependency handling. //! //! This module provides functionality for handling Debian build dependencies, //! including tie-breaking between multiple potential dependencies. use crate::dependencies::debian::DebianDependency; use crate::dependencies::debian::TieBreaker; use crate::session::Session; use breezyshim::debian::apt::{Apt, LocalApt}; use std::cell::RefCell; use std::collections::HashMap; /// Tie-breaker for Debian build dependencies. /// /// This tie-breaker selects the most commonly used dependency based on /// analyzing build dependencies across all source packages in the APT cache. pub struct BuildDependencyTieBreaker { /// Local APT instance for accessing package information apt: LocalApt, /// Cached counts of build dependency usage counts: RefCell>>, } impl BuildDependencyTieBreaker { /// Create a new BuildDependencyTieBreaker from a session. /// /// # Arguments /// * `session` - Session to use for accessing the local APT cache /// /// # Returns /// A new BuildDependencyTieBreaker instance pub fn from_session(session: &dyn Session) -> Self { Self { apt: LocalApt::new(Some(&session.location())).unwrap(), counts: RefCell::new(None), } } /// Try to create a new BuildDependencyTieBreaker from a session. /// /// This method attempts to create a BuildDependencyTieBreaker but returns /// an error if the LocalApt instance cannot be created (e.g., due to /// network restrictions or permission issues). /// /// # Arguments /// * `session` - Session to use for accessing the local APT cache /// /// # Returns /// Result containing a new BuildDependencyTieBreaker instance or an error pub fn try_from_session(session: &dyn Session) -> Result> { let apt = LocalApt::new(Some(&session.location())) .map_err(|e| Box::new(e) as Box)?; Ok(Self { apt, counts: RefCell::new(None), }) } /// Count the occurrences of each build dependency across all source packages. /// /// This method scans all source packages in the APT cache and counts how many /// times each package is used as a build dependency. /// /// # Returns /// HashMap mapping package names to their usage count fn count(&self) -> HashMap { let mut counts = HashMap::new(); for source in self.apt.iter_sources() { source .build_depends() .into_iter() .chain(source.build_depends_indep().into_iter()) .chain(source.build_depends_arch().into_iter()) .for_each(|r| { for e in r.entries() { e.relations().for_each(|r| { let count = counts.entry(r.name().clone()).or_insert(0); *count += 1; }); } }); } counts } } /// Implementation of TieBreaker for BuildDependencyTieBreaker. impl TieBreaker for BuildDependencyTieBreaker { /// Break a tie between multiple Debian dependencies by selecting the most commonly used one. /// /// # Arguments /// * `reqs` - Slice of Debian dependency candidates to choose from /// /// # Returns /// The most commonly used dependency, or None if no candidates are available fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { if self.counts.borrow().is_none() { let counts = self.count(); self.counts.replace(Some(counts)); } let c = self.counts.borrow(); let count = c.clone().unwrap(); let mut by_count = HashMap::new(); for req in reqs { let name = req.package_names().into_iter().next().unwrap(); by_count.insert(req, count[&name]); } if by_count.is_empty() { return None; } let top = by_count.iter().max_by_key(|k| k.1).unwrap(); log::info!( "Breaking tie between [{:?}] to {:?} based on build-depends count", reqs.iter().map(|r| r.relation_string()).collect::>(), top.0.relation_string(), ); Some(*top.0) } } #[cfg(test)] mod tests { use super::*; use crate::session::plain::PlainSession; use tempfile::TempDir; #[test] fn test_build_dependency_tie_breaker_from_session() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); // This test verifies that we can create a BuildDependencyTieBreaker // Note: The actual functionality depends on APT cache being available let _tie_breaker = BuildDependencyTieBreaker::from_session(&session); } #[test] fn test_build_dependency_tie_breaker_count_empty() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let tie_breaker = BuildDependencyTieBreaker::from_session(&session); // With no APT cache, count should return an empty HashMap let counts = tie_breaker.count(); assert!(counts.is_empty()); } #[test] fn test_build_dependency_tie_breaker_break_tie_empty() { let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); let tie_breaker = BuildDependencyTieBreaker::from_session(&session); // With empty input, should return None let result = tie_breaker.break_tie(&[]); assert!(result.is_none()); } } ognibuild-0.2.6/src/debian/context.rs000064400000000000000000000352721046102023000156610ustar 00000000000000//! Context for working with Debian packages. //! //! This module provides a context for operations on Debian packages, //! including editing, committing changes, and managing dependencies. use crate::dependencies::debian::DebianDependency; use breezyshim::commit::PyCommitReporter; use breezyshim::debian::debcommit::debcommit; use breezyshim::error::Error as BrzError; use breezyshim::tree::{MutableTree, Tree}; use breezyshim::workingtree::{GenericWorkingTree, WorkingTree}; pub use buildlog_consultant::sbuild::Phase; use debian_analyzer::abstract_control::AbstractControlEditor; use debian_analyzer::editor::{Editor, EditorError, Marshallable, MutableTreeEdit, TreeEditor}; use std::path::{Path, PathBuf}; /// Errors that can occur when working with Debian packages. #[derive(Debug)] pub enum Error { /// Circular dependency detected. CircularDependency(String), /// No source stanza found in debian/control. MissingSource, /// Error from breezyshim. BrzError(BrzError), /// Error from debian_analyzer editor. EditorError(debian_analyzer::editor::EditorError), /// I/O error when accessing files. IoError(std::io::Error), /// Invalid field value in control file. InvalidField(String, String), } impl From for Error { fn from(e: BrzError) -> Self { Error::BrzError(e) } } impl From for Error { fn from(e: debian_analyzer::editor::EditorError) -> Self { Error::EditorError(e) } } impl From for Error { fn from(e: std::io::Error) -> Self { Error::IoError(e) } } impl From for crate::fix_build::InterimError { fn from(e: Error) -> crate::fix_build::InterimError { crate::fix_build::InterimError::Other(e) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::CircularDependency(pkg) => write!(f, "Circular dependency on {}", pkg), Error::MissingSource => write!(f, "No source stanza"), Error::BrzError(e) => write!(f, "{}", e), Error::EditorError(e) => write!(f, "{}", e), Error::IoError(e) => write!(f, "{}", e), Error::InvalidField(field, e) => write!(f, "Invalid field {}: {}", field, e), } } } impl std::error::Error for Error {} /// Context for working with Debian packages. /// /// This structure provides methods for modifying Debian package files, /// committing changes, and managing dependencies. pub struct DebianPackagingContext { /// Working tree containing the package source. pub tree: GenericWorkingTree, /// Path within the tree where the package is located. pub subpath: PathBuf, /// Committer information (name, email). pub committer: (String, String), /// Whether to update the changelog during commits. pub update_changelog: bool, /// Optional reporter for commit operations. pub commit_reporter: Option>, } impl DebianPackagingContext { /// Create a new Debian packaging context. /// /// # Arguments /// * `tree` - Working tree containing the package source /// * `subpath` - Path within the tree where the package is located /// * `committer` - Optional committer information (name, email) /// * `update_changelog` - Whether to update the changelog during commits /// * `commit_reporter` - Optional reporter for commit operations /// /// # Returns /// A new DebianPackagingContext instance pub fn new( tree: GenericWorkingTree, subpath: &Path, committer: Option<(String, String)>, update_changelog: bool, commit_reporter: Option>, ) -> Self { Self { tree, subpath: subpath.to_path_buf(), committer: committer.unwrap_or_else(|| debian_changelog::get_maintainer().unwrap()), update_changelog, commit_reporter, } } /// Check if a file exists in the package tree. /// /// # Arguments /// * `path` - Path to check /// /// # Returns /// true if the file exists, false otherwise pub fn has_filename(&self, path: &Path) -> bool { self.tree.has_filename(&self.subpath.join(path)) } /// Get the absolute path of a file in the package tree. /// /// # Arguments /// * `path` - Relative path within the package /// /// # Returns /// Absolute path to the file pub fn abspath(&self, path: &Path) -> PathBuf { self.tree.abspath(&self.subpath.join(path)).unwrap() } /// Create an editor for a file in the package tree. /// /// # Arguments /// * `path` - Path to the file to edit /// * `allow_generated` - Whether to allow editing generated files /// /// # Returns /// A TreeEditor for the specified file pub fn edit_file( &self, path: &std::path::Path, allow_generated: bool, ) -> Result, EditorError> { let path = self.subpath.join(path); self.tree.edit_file(&path, allow_generated, true) } /// Commit changes to the package tree. /// /// # Arguments /// * `summary` - Commit message summary /// * `update_changelog` - Whether to update the changelog (overrides context setting) /// /// # Returns /// Ok(true) if changes were committed, Ok(false) if no changes to commit, Error otherwise pub fn commit(&self, summary: &str, update_changelog: Option) -> Result { let update_changelog = update_changelog.unwrap_or(self.update_changelog); let committer = format!("{} <{}>", self.committer.0, self.committer.1); let lock_write = self.tree.lock_write(); let r = if update_changelog { let mut cl = self .edit_file::(Path::new("debian/changelog"), false)?; cl.auto_add_change(&[summary], self.committer.clone(), None, None); cl.commit()?; debcommit( &self.tree, Some(&committer), &self.subpath, None, self.commit_reporter.as_deref(), None, ) } else { let mut builder = self .tree .build_commit() .message(summary) .committer(&committer); if !self.subpath.as_os_str().is_empty() { builder = builder.specific_files(&[&self.subpath]); } if let Some(commit_reporter) = self.commit_reporter.as_ref() { builder = builder.reporter(commit_reporter.as_ref()); } builder.commit() }; std::mem::drop(lock_write); match r { Ok(_) => Ok(true), Err(BrzError::PointlessCommit) => Ok(false), Err(e) => Err(e.into()), } } /// Add a dependency to the package. /// /// # Arguments /// * `phase` - Build phase for the dependency /// * `requirement` - Debian dependency to add /// /// # Returns /// Ok(true) if dependency was added, Ok(false) if already present, Error otherwise pub fn add_dependency( &self, phase: &Phase, requirement: &DebianDependency, ) -> Result { match phase { Phase::AutoPkgTest(n) => self.add_test_dependency(n, requirement), Phase::Build => self.add_build_dependency(requirement), Phase::BuildEnv => { // TODO(jelmer): Actually, we probably just want to install it on the host system? log::warn!("Unknown phase {:?}", phase); Ok(false) } Phase::CreateSession => { log::warn!("Unknown phase {:?}", phase); Ok(false) } } } /// Create an editor for the debian/control file. /// /// # Returns /// An editor for the control file, or Error if not found or cannot be edited pub fn edit_control<'a>(&'a self) -> Result, Error> { if self .tree .has_filename(&self.subpath.join("debian/debcargo.toml")) { Ok(Box::new( debian_analyzer::debcargo::DebcargoEditor::from_directory( &self.tree.abspath(&self.subpath).unwrap(), )?, )) } else { let control_path = Path::new("debian/control"); Ok( Box::new(self.edit_file::(control_path, false)?) as Box, ) } } fn add_build_dependency(&self, requirement: &DebianDependency) -> Result { assert!(!requirement.is_empty()); let mut control = self.edit_control()?; for binary in control.binaries() { if requirement.touches_package(&binary.name().unwrap()) { return Err(Error::CircularDependency(binary.name().unwrap())); } } let mut source = if let Some(source) = control.source() { source } else { return Err(Error::MissingSource); }; for rel in requirement.iter() { source.ensure_build_dep(rel); } std::mem::drop(source); let desc = requirement.relation_string(); if !control.commit() { log::info!("Giving up; build dependency {} was already present.", desc); return Ok(false); } log::info!("Adding build dependency: {}", desc); self.commit(&format!("Add missing build dependency on {}.", desc), None)?; Ok(true) } /// Create an editor for the debian/tests/control file. /// /// # Returns /// An editor for the tests control file, or Error if not found or cannot be edited pub fn edit_tests_control(&self) -> Result, Error> { Ok(self.edit_file::(Path::new("debian/tests/control"), false)?) } /// Create an editor for the debian/rules file. /// /// # Returns /// An editor for the rules file, or Error if not found or cannot be edited pub fn edit_rules(&self) -> Result, Error> { Ok(self.edit_file::(Path::new("debian/rules"), false)?) } fn add_test_dependency( &self, testname: &str, requirement: &DebianDependency, ) -> Result { // TODO(jelmer): If requirement is for one of our binary packages but "@" is already // present then don't do anything. let editor = self.edit_tests_control()?; let mut command_counter = 1; for mut para in editor.paragraphs() { let name = para.get("Tests").unwrap_or_else(|| { let name = format!("command{}", command_counter); command_counter += 1; name }); if name != testname { continue; } for rel in requirement.iter() { let depends = para.get("Depends").unwrap_or_default(); let mut rels: debian_control::lossless::relations::Relations = depends.parse().map_err(|e| { Error::InvalidField(format!("Test Depends for {}", testname), e) })?; debian_analyzer::relations::ensure_relation(&mut rels, rel); para.insert("Depends", &rels.to_string()); } } let desc = requirement.relation_string(); if editor.commit()?.is_empty() { log::info!( "Giving up; dependency {} for test {} was already present.", desc, testname, ); return Ok(false); } log::info!("Adding dependency to test {}: {}", testname, desc); self.commit( &format!("Add missing dependency for test {} on {}.", testname, desc), None, )?; Ok(true) } } #[cfg(test)] mod tests { use super::*; use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat}; pub const COMMITTER: &str = "ognibuild "; fn setup(path: &Path) -> DebianPackagingContext { let tree = create_standalone_workingtree(path, &ControlDirFormat::default()).unwrap(); std::fs::create_dir_all(path.join("debian")).unwrap(); std::fs::write( path.join("debian/control"), r###"Source: blah Build-Depends: libc6 Package: python-blah Depends: ${python3:Depends} Description: A python package Foo "###, ) .unwrap(); std::fs::write( path.join("debian/changelog"), r###"blah (0.1) UNRELEASED; urgency=medium * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Sat, 04 Apr 2020 14:12:13 +0000 "###, ) .unwrap(); tree.add(&[ Path::new("debian"), Path::new("debian/control"), Path::new("debian/changelog"), ]) .unwrap(); tree.build_commit() .message("Initial commit") .committer(COMMITTER) .commit() .unwrap(); DebianPackagingContext::new( tree, Path::new(""), Some(("ognibuild".to_owned(), "".to_owned())), false, Some(Box::new(breezyshim::commit::NullCommitReporter::new())), ) } #[test] fn test_already_present() { let td = tempfile::tempdir().unwrap(); let context = setup(td.path()); let dep = DebianDependency::simple("libc6"); assert!(!context.add_build_dependency(&dep).unwrap()); } #[test] fn test_basic() { let td = tempfile::tempdir().unwrap(); let context = setup(td.path()); let dep = DebianDependency::simple("foo"); assert!(context.add_build_dependency(&dep).unwrap()); let control = std::fs::read_to_string(td.path().join("debian/control")).unwrap(); assert_eq!( control, r###"Source: blah Build-Depends: foo, libc6 Package: python-blah Depends: ${python3:Depends} Description: A python package Foo "### ); } #[test] fn test_circular() { let td = tempfile::tempdir().unwrap(); let context = setup(td.path()); let dep = DebianDependency::simple("python-blah"); assert!(matches!( context.add_build_dependency(&dep), Err(Error::CircularDependency(_)) )); } } ognibuild-0.2.6/src/debian/dep_server.rs000064400000000000000000000140271046102023000163260ustar 00000000000000//! Dependency server integration for Debian packages. //! //! This module provides functionality for resolving dependencies using //! a remote dependency server that can translate generic dependencies //! into Debian package dependencies. use crate::debian::apt::AptManager; use crate::dependencies::debian::DebianDependency; use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use reqwest::StatusCode; use tokio::runtime::Runtime; use url::Url; /// Resolve a requirement to an APT requirement with a dep server. /// /// # Arguments /// * `url` - Dep server URL /// * `req` - Dependency to resolve /// /// # Returns /// List of APT requirements. async fn resolve_apt_requirement_dep_server( url: &url::Url, _dep: &dyn Dependency, ) -> Result, Error> { let client = reqwest::Client::new(); let response = client .post(url.join("resolve-apt").unwrap()) .json(&serde_json::json!( { "requirement": { // TODO: Use the actual dependency } })) .send() .await .unwrap(); match response.status() { StatusCode::NOT_FOUND => { if response .headers() .get("Reason") .map(|x| x.to_str().unwrap()) == Some("family-unknown") { return Err(Error::UnknownDependencyFamily); } Ok(None) } StatusCode::OK => { let body = response.json::().await.unwrap(); Ok(Some(body)) } _ => { panic!("Unexpected response status: {}", response.status()); } } } /// Installer that uses a dependency server to resolve and install dependencies. /// /// This installer connects to a remote dependency server that can translate /// generic dependencies into Debian package dependencies and then installs them /// using APT. pub struct DepServerAptInstaller<'a> { /// APT manager for package operations apt: AptManager<'a>, /// URL of the dependency server dep_server_url: Url, } impl<'a> DepServerAptInstaller<'a> { /// Create a new DepServerAptInstaller with the given APT manager and server URL. /// /// # Arguments /// * `apt` - APT manager to use for installing dependencies /// * `dep_server_url` - URL of the dependency server /// /// # Returns /// A new DepServerAptInstaller instance pub fn new(apt: AptManager<'a>, dep_server_url: &Url) -> Self { Self { apt, dep_server_url: dep_server_url.clone(), } } /// Create a new DepServerAptInstaller from a session and server URL. /// /// # Arguments /// * `session` - Session to use for running commands /// * `dep_server_url` - URL of the dependency server /// /// # Returns /// A new DepServerAptInstaller instance pub fn from_session(session: &'a dyn Session, dep_server_url: &'_ Url) -> Self { let apt = AptManager::from_session(session); Self::new(apt, dep_server_url) } /// Resolve a dependency to a Debian package dependency using the dependency server. /// /// # Arguments /// * `req` - Generic dependency to resolve /// /// # Returns /// Some(DebianDependency) if the server could resolve it, None if not found, /// or Error if there was a problem communicating with the server pub fn resolve(&self, req: &dyn Dependency) -> Result, Error> { let rt = Runtime::new().unwrap(); match rt.block_on(resolve_apt_requirement_dep_server( &self.dep_server_url, req, )) { Ok(deps) => Ok(deps), Err(o) => { log::warn!("Falling back to resolving error locally"); Err(Error::Other(o.to_string())) } } } } /// Implementation of the Installer trait for DepServerAptInstaller. impl<'a> Installer for DepServerAptInstaller<'a> { fn install( &self, dep: &dyn Dependency, scope: crate::installer::InstallationScope, ) -> Result<(), Error> { match scope { InstallationScope::User => { return Err(Error::UnsupportedScope(scope)); } InstallationScope::Global => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } let dep = self.resolve(dep)?; if let Some(dep) = dep { match self .apt .satisfy(vec![crate::debian::apt::SatisfyEntry::Required( dep.relation_string(), )]) { Ok(_) => {} Err(e) => { return Err(Error::Other(e.to_string())); } } Ok(()) } else { Err(Error::UnknownDependencyFamily) } } fn explain( &self, dep: &dyn Dependency, scope: crate::installer::InstallationScope, ) -> Result { match scope { InstallationScope::User => { return Err(Error::UnsupportedScope(scope)); } InstallationScope::Global => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } let dep = self.resolve(dep)?; let dep = dep.ok_or_else(|| Error::UnknownDependencyFamily)?; let apt_deb_str = dep.relation_string(); let cmd = self.apt.satisfy_command(vec![apt_deb_str.as_str()]); Ok(Explanation { message: format!( "Install {}", dep.package_names() .iter() .map(|x| x.as_str()) .collect::>() .join(", ") ), command: Some(cmd.iter().map(|s| s.to_string()).collect()), }) } } ognibuild-0.2.6/src/debian/file_search.rs000064400000000000000000001335401046102023000164360ustar 00000000000000//! File searching utilities for Debian packages. //! //! This module provides functionality for searching files in Debian //! packages, including using apt-file and other package contents databases. use crate::session::{Error as SessionError, Session}; use apt_sources::{ error::{LoadError, RepositoryError}, Repository, RepositoryType, }; use debian_control::apt::Release; use flate2::read::GzDecoder; use lzma_rs::lzma_decompress; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{BufRead, BufReader, Read}; use std::path::{Path, PathBuf}; use url::Url; /// Errors that can occur when searching for files in Debian packages. #[derive(Debug)] pub enum Error { /// Error accessing apt-file or its cache. AptFileAccessError(String), /// File not found in the package contents database. FileNotFoundError(String), /// I/O error accessing files or network resources. IoError(std::io::Error), /// Decompression error when unpacking compressed files. DecompressionError(String), } impl From for Error { fn from(e: std::io::Error) -> Error { Error::IoError(e) } } impl From for Error { fn from(e: RepositoryError) -> Error { Error::AptFileAccessError(format!("Repository error: {}", e)) } } impl From for Error { fn from(e: LoadError) -> Error { Error::AptFileAccessError(format!("Load error: {}", e)) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::AptFileAccessError(e) => write!(f, "AptFileAccessError: {}", e), Error::FileNotFoundError(e) => write!(f, "FileNotFoundError: {}", e), Error::IoError(e) => write!(f, "IoError: {}", e), Error::DecompressionError(e) => write!(f, "DecompressionError: {}", e), } } } impl std::error::Error for Error {} /// Trait for searching files in Debian packages. /// /// Implementors of this trait provide methods for searching files /// by exact path or regular expression. pub trait FileSearcher<'b> { /// Search for files by exact path. /// /// # Arguments /// * `path` - Path to search for /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing the file fn search_files<'a>( &'a self, path: &'a Path, case_insensitive: bool, ) -> Box + 'a>; /// Search for files by regular expression. /// /// # Arguments /// * `path` - Regular expression pattern to match against file paths /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_regex<'a>( &'a self, path: &'a str, case_insensitive: bool, ) -> Box + 'a>; } /// Read a Debian contents file. /// /// Contents files map file paths to package names. /// /// # Arguments /// * `f` - Reader for the contents file /// /// # Returns /// Iterator of (file path, package name) pairs pub fn read_contents_file(f: R) -> impl Iterator { BufReader::new(f).lines().map(|line| { let line = line.unwrap(); let (path, rest) = line.rsplit_once(' ').unwrap(); (path.to_string(), rest.to_string()) }) } /// Get URLs for contents files from a repository entry. /// /// # Arguments /// * `repo` - Repository to get contents URLs from /// * `arches` - List of architectures to include /// * `load_url` - Function to load a URL and get a reader /// /// # Returns /// Iterator of URLs for contents files pub fn contents_urls_from_repository<'a>( repo: &'a Repository, arches: Vec<&'a str>, load_url: impl Fn(&url::Url) -> Result, Error>, ) -> Box + 'a> { // Only process binary repositories (deb), not source repositories (deb-src) if !repo.types.contains(&RepositoryType::Binary) { return Box::new(vec![].into_iter()); } // Process all URIs and suites combinations let mut all_urls = Vec::new(); for uri in &repo.uris { for dist in &repo.suites { let comps = repo .components .as_ref() .map(|c| c.as_slice()) .unwrap_or(&[]); let base_url = uri.as_str().trim_end_matches('/'); let name = dist.trim_end_matches('/'); let dists_url: url::Url = if comps.is_empty() { base_url.to_string() } else { format!("{}/dists", base_url) } .parse() .unwrap(); let inrelease_url: Url = dists_url.join(&format!("{}/InRelease", name)).unwrap(); let mut response = match load_url(&inrelease_url) { Ok(response) => response, Err(_) => { let release_url = dists_url.join(&format!("{}/Release", name)).unwrap(); match load_url(&release_url) { Ok(response) => response, Err(e) => { log::warn!( "Unable to download {} or {}: {}", inrelease_url, release_url, e ); return Box::new(vec![].into_iter()); } } } }; let mut release = String::new(); response.read_to_string(&mut release).unwrap(); let mut existing_names = HashMap::new(); let release: Release = release.parse().unwrap(); for name in release .checksums_md5() .into_iter() .map(|x| x.filename) .chain(release.checksums_sha256().into_iter().map(|x| x.filename)) .chain(release.checksums_sha1().into_iter().map(|x| x.filename)) .chain(release.checksums_sha512().into_iter().map(|x| x.filename)) { existing_names.insert( std::path::PathBuf::from(name.clone()) .file_stem() .unwrap() .to_owned(), name, ); } let mut contents_files = HashSet::new(); if comps.is_empty() { for arch in &arches { contents_files.insert(format!("Contents-{}", arch)); } } else { for comp in comps { for arch in &arches { contents_files.insert(format!("{}/Contents-{}", comp, arch)); } } } let urls: Vec<_> = contents_files .into_iter() .filter_map(move |f| { if let Some(name) = existing_names .get(&std::path::Path::new(&f).file_stem().unwrap().to_owned()) { return Some(dists_url.join(name).unwrap().join(&f).unwrap()); } None }) .collect(); all_urls.extend(urls); } } Box::new(all_urls.into_iter()) } /// Get URLs for contents files from APT sources. /// /// # Arguments /// * `repositories` - Repositories to get contents URLs from /// * `arch` - Architecture to include /// * `load_url` - Function to load a URL and get a reader /// /// # Returns /// Iterator of URLs for contents files pub fn contents_urls_from_sources<'a>( repositories: &'a apt_sources::Repositories, arch: &'a str, load_url: impl Fn(&'_ url::Url) -> Result, Error> + 'a + Copy, ) -> impl Iterator + 'a { // TODO(jelmer): Verify signatures, etc. let arches = vec![arch, "all"]; repositories .iter() .flat_map(move |repo| contents_urls_from_repository(repo, arches.clone(), load_url)) } /// Unwrap a compressed file based on its extension. /// /// # Arguments /// * `f` - Reader for the compressed file /// * `ext` - File extension (e.g., "gz", "xz") /// /// # Returns /// Reader for the decompressed contents pub fn unwrap<'a, R: Read + 'a>(f: R, ext: &str) -> Result, Error> { match ext { ".gz" => Ok(Box::new(GzDecoder::new(f))), ".xz" => { let mut compressed_reader = BufReader::new(f); let mut decompressed_data = Vec::new(); lzma_decompress(&mut compressed_reader, &mut decompressed_data).map_err(|e| { Error::DecompressionError(format!("LZMA decompression failed: {}", e)) })?; Ok(Box::new(std::io::Cursor::new(decompressed_data))) } ".lz4" => Ok(Box::new(lz4_flex::frame::FrameDecoder::new(f))), _ => Ok(Box::new(f)), } } /// Load a URL directly without caching. /// /// # Arguments /// * `url` - URL to load /// /// # Returns /// Reader for the URL contents pub fn load_direct_url(url: &url::Url) -> Result, Error> { // Create a client with reasonable timeouts for downloading large APT Contents files let client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(300)) // 5 minutes for large files .connect_timeout(std::time::Duration::from_secs(30)) // 30 seconds to establish connection .build() .map_err(|e| Error::AptFileAccessError(format!("Failed to create HTTP client: {}", e)))?; for ext in [".xz", ".gz", ""] { let response = match client.get(url.to_string() + ext).send() { Ok(response) => response, Err(e) => { if e.status() == Some(reqwest::StatusCode::NOT_FOUND) { continue; } log::debug!("Failed to fetch APT contents from {}{}: {}", url, ext, e); return Err(Error::AptFileAccessError(format!( "Unable to access apt URL {}{}: {}", url, ext, e ))); } }; return unwrap(response, ext); } Err(Error::FileNotFoundError(format!("{} not found", url))) } /// Get the user cache directory for ognibuild APT Contents files. fn get_user_cache_dir() -> Option { dirs::cache_dir().map(|d| d.join("ognibuild").join("apt-contents")) } /// Load a URL with caching in the specified directories. /// /// # Arguments /// * `url` - URL to load /// * `cache_dirs` - Directories to check for cached content /// /// # Returns /// Reader for the URL contents pub fn load_url_with_cache(url: &url::Url, cache_dirs: &[&Path]) -> Result, Error> { // First check system cache directories for cache_dir in cache_dirs { match load_apt_cache_file(url, cache_dir) { Ok(f) => return Ok(Box::new(f)), Err(e) => { if e.kind() != std::io::ErrorKind::NotFound { return Err(e.into()); } } } } // Then check user cache directory if let Some(user_cache_dir) = get_user_cache_dir() { match load_apt_cache_file(url, &user_cache_dir) { Ok(f) => { log::debug!( "Found cached APT contents in user cache: {}", user_cache_dir.display() ); return Ok(Box::new(f)); } Err(e) => { if e.kind() != std::io::ErrorKind::NotFound { log::debug!("Error reading from user cache: {}", e); } } } } // If not found in any cache, download and cache it download_and_cache_url(url) } /// Download a URL and cache it in the user cache directory. fn download_and_cache_url(url: &url::Url) -> Result, Error> { // Download the file let content = load_direct_url(url)?; // Try to cache it in user directory if let Some(user_cache_dir) = get_user_cache_dir() { // Ensure cache directory exists if let Err(e) = std::fs::create_dir_all(&user_cache_dir) { log::debug!( "Failed to create cache directory {}: {}", user_cache_dir.display(), e ); } else { // Read the content into memory so we can both cache and return it let mut buffer = Vec::new(); let mut reader = content; if let Err(e) = std::io::Read::read_to_end(&mut reader, &mut buffer) { log::debug!("Failed to read content for caching: {}", e); return Ok(reader); // Return the original reader if we can't cache } // Write to cache file let cache_file_path = user_cache_dir.join(uri_to_filename(url)); match std::fs::write(&cache_file_path, &buffer) { Ok(_) => { log::info!("Cached APT contents to: {}", cache_file_path.display()); } Err(e) => { log::debug!( "Failed to write cache file {}: {}", cache_file_path.display(), e ); } } // Return the buffer as a reader return Ok(Box::new(std::io::Cursor::new(buffer))); } } // If we can't cache, just return the downloaded content Ok(content) } /// Convert a URI into a safe filename. It quotes all unsafe characters and converts / to _ and removes the scheme identifier. pub fn uri_to_filename(url: &url::Url) -> String { let mut url = url.clone(); url.set_username("").unwrap(); url.set_password(None).unwrap(); use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; // Define the set of characters that need to be percent-encoded const BAD_CHARS: &AsciiSet = &CONTROLS .add(b' ') // Add space .add(b'\"') // Add " .add(b'\\') // Add \ .add(b'{') .add(b'}') .add(b'[') .add(b']') .add(b'<') .add(b'>') .add(b'^') .add(b'~') .add(b'_') .add(b'=') .add(b'!') .add(b'@') .add(b'#') .add(b'$') .add(b'%') .add(b'^') .add(b'&') .add(b'*'); let mut u = url.to_string(); if let Some(pos) = u.find("://") { u = u[(pos + 3)..].to_string(); // Remove the scheme } // Percent-encode the bad characters let encoded_uri = utf8_percent_encode(&u, BAD_CHARS).to_string(); // Replace '/' with '_' encoded_uri.replace('/', "_") } /// Load a file from the APT cache directory. /// /// # Arguments /// * `url` - URL to load /// * `cache_dir` - APT cache directory /// /// # Returns /// Reader for the cached file pub fn load_apt_cache_file( url: &url::Url, cache_dir: &Path, ) -> Result, std::io::Error> { let f = uri_to_filename(url); for ext in [".xz", ".gz", ".lz4", ""] { let p = cache_dir.join([&f, ext].concat()); if !p.exists() { continue; } log::debug!("Loading cached contents file {}", p.display()); // return os.popen('/usr/lib/apt/apt-helper cat-file %s' % p) let f = File::open(p)?; return unwrap(f, ext).map_err(|e| match e { Error::IoError(io_err) => io_err, Error::DecompressionError(msg) => { std::io::Error::new(std::io::ErrorKind::InvalidData, msg) } other => std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", other)), }); } Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!("{} not found", url), )) } lazy_static::lazy_static! { /// Path to the file that indicates the apt-file cache is empty. pub static ref CACHE_IS_EMPTY_PATH: &'static Path = Path::new("/usr/share/apt-file/is-cache-empty"); } /// File searcher that uses apt-file to find files in Debian packages. pub struct AptFileFileSearcher<'a> { /// Session for running commands session: &'a dyn Session, } impl<'a> AptFileFileSearcher<'a> { /// Check if the apt-file cache exists and is not empty. /// /// # Arguments /// * `session` - Session for running commands /// /// # Returns /// `true` if the cache exists and is not empty, `false` otherwise pub fn has_cache(session: &dyn Session) -> Result { if !session.exists(&CACHE_IS_EMPTY_PATH) { return Ok(false); } match session .command(vec![&CACHE_IS_EMPTY_PATH.to_str().unwrap()]) .check_call() { Ok(_) => Ok(true), Err(SessionError::CalledProcessError(status)) => { if status.code() == Some(1) { Ok(true) } else { Ok(false) } } Err(e) => Err(e), } } /// Create a new AptFileFileSearcher from a session. /// /// This ensures that apt-file is installed and the cache is updated. /// /// # Arguments /// * `session` - Session for running commands /// /// # Returns /// A new AptFileFileSearcher instance pub fn from_session(session: &dyn Session) -> AptFileFileSearcher<'_> { log::debug!("Using apt-file to search apt contents"); if !session.exists(&CACHE_IS_EMPTY_PATH) { crate::debian::apt::AptManager::from_session(session) .satisfy(vec![crate::debian::apt::SatisfyEntry::Required( "apt-file".to_string(), )]) .unwrap(); } if !Self::has_cache(session).unwrap() { session .command(vec!["apt-file", "update"]) .user("root") .check_call() .unwrap(); } AptFileFileSearcher { session } } /// Search for files in Debian packages. /// /// This is an internal implementation method used by the FileSearcher trait methods. /// /// # Arguments /// * `path` - Path or pattern to search for /// * `regex` - Whether to treat the path as a regular expression /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_ex( &self, path: &str, regex: bool, case_insensitive: bool, ) -> Result, Error> { let mut args = vec!["apt-file", "search", "--stream-results"]; if regex { args.push("-x"); } else { args.push("-F"); } if case_insensitive { args.push("-i"); } args.push(path); let output = self .session .command(args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .map_err(|e| { Error::AptFileAccessError(format!( "Unable to search for files matching {}: {}", path, e )) })?; match output.status.code() { Some(0) | Some(1) => { // At least one search result let output_str = std::str::from_utf8(&output.stdout).unwrap(); let entries = output_str .split('\n') .filter_map(|line| { if line.is_empty() { return None; } let (pkg, _path) = line.split_once(": ").unwrap(); Some(pkg.to_string()) }) .collect::>(); log::debug!("Found entries {:?} for {}", entries, path); Ok(entries.into_iter()) } Some(2) => { // Error Err(Error::AptFileAccessError(format!( "Error searching for files matching {}: {}", path, std::str::from_utf8(&output.stderr).unwrap() ))) } Some(3) => Err(Error::AptFileAccessError( "apt-file cache is empty".to_owned(), )), Some(4) => Err(Error::AptFileAccessError( "apt-file has no entries matching restrictions".to_owned(), )), _ => Err(Error::AptFileAccessError( "apt-file returned an unknown error".to_owned(), )), } } } impl<'b> FileSearcher<'b> for AptFileFileSearcher<'b> { /// Search for files by exact path. /// /// This implementation uses apt-file to search for packages /// containing the specified file path. /// /// # Arguments /// * `path` - Path to search for /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing the file fn search_files<'a>( &'a self, path: &'a Path, case_insensitive: bool, ) -> Box + 'a> { return Box::new( self.search_files_ex(path.to_str().unwrap(), false, case_insensitive) .unwrap(), ); } /// Search for files by regular expression. /// /// This implementation uses apt-file with the -x flag to search for packages /// containing files matching the specified regex pattern. /// /// # Arguments /// * `path` - Regular expression pattern to match against file paths /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_regex<'a>( &'a self, path: &'a str, case_insensitive: bool, ) -> Box + 'a> { Box::new(self.search_files_ex(path, true, case_insensitive).unwrap()) } } /// Set up apt-file in a session. /// /// This function installs apt-file if needed and updates the apt-file cache. /// /// # Arguments /// * `session` - Session to set up /// /// # Returns /// Ok(()) if setup was successful, Error otherwise pub fn setup_apt_file(session: &dyn Session) -> Result<(), Error> { // Update APT package lists first log::info!("Updating APT package lists..."); session .command(vec!["apt-get", "update"]) .user("root") .check_call() .map_err(|e| Error::AptFileAccessError(format!("Failed to run apt-get update: {}", e)))?; // Install apt-file if not already installed log::info!("Installing apt-file..."); session .command(vec!["apt-get", "install", "-y", "apt-file"]) .user("root") .check_call() .map_err(|e| Error::AptFileAccessError(format!("Failed to install apt-file: {}", e)))?; // Update apt-file cache log::info!("Updating apt-file cache..."); session .command(vec!["apt-file", "update"]) .user("root") .check_call() .map_err(|e| Error::AptFileAccessError(format!("Failed to run apt-file update: {}", e)))?; log::info!("apt-file setup complete"); Ok(()) } /// Get a file searcher that uses apt-file or remote contents. /// /// This function returns the appropriate file searcher based on whether /// apt-file cache is available. If apt-file cache is available, it returns /// an AptFileFileSearcher; otherwise, it returns a RemoteContentsFileSearcher. /// /// # Arguments /// * `session` - Session for running commands /// /// # Returns /// A file searcher implementation pub fn get_apt_contents_file_searcher<'a>( session: &'a dyn Session, ) -> Result + 'a>, Error> { if AptFileFileSearcher::has_cache(session).unwrap() { Ok(Box::new(AptFileFileSearcher::from_session(session)) as Box>) } else { // Try to load remote contents, but with timeouts to prevent hanging RemoteContentsFileSearcher::from_session(session) .map(|searcher| Box::new(searcher) as Box>) } } /// File searcher that uses remote Contents files from Debian repositories. /// /// This searcher downloads and parses Contents files from Debian repositories /// to find packages containing specific files. pub struct RemoteContentsFileSearcher { /// Database mapping file paths to package names db: HashMap>, } impl RemoteContentsFileSearcher { /// Create a new RemoteContentsFileSearcher from a session. /// /// This loads contents information from the APT sources configured in /// the session. /// /// # Arguments /// * `session` - Session for running commands /// /// # Returns /// A new RemoteContentsFileSearcher instance pub fn from_session(session: &dyn Session) -> Result { log::debug!("Loading apt contents information"); let mut ret = RemoteContentsFileSearcher { db: HashMap::new() }; ret.load_from_session(session)?; Ok(ret) } /// Load contents information from local APT sources. /// /// # Returns /// Ok(()) if successful, Error otherwise pub fn load_local(&mut self) -> Result<(), Error> { let repositories = apt_sources::Repositories::default(); let arch = crate::debian::build::get_build_architecture(); let cache_dirs = vec![Path::new("/var/lib/apt/lists")]; let load_url = |url: &url::Url| load_url_with_cache(url, cache_dirs.as_slice()); let urls = contents_urls_from_sources(&repositories, &arch, load_url); self.load_urls(urls, load_url, false) } /// Load contents information from APT sources configured in a session. /// /// # Arguments /// * `session` - Session for running commands /// /// # Returns /// Ok(()) if successful, Error otherwise pub fn load_from_session(&mut self, session: &dyn Session) -> Result<(), Error> { let (repositories, _errors) = apt_sources::Repositories::load_from_directory( &session.external_path(Path::new("/etc/apt")), ); let arch = crate::debian::build::get_build_architecture(); let cache_dirs = [session.external_path(Path::new("/var/lib/apt/lists"))]; let load_url = |url: &url::Url| { load_url_with_cache( url, cache_dirs .iter() .map(|p| p.as_ref()) .collect::>() .as_slice(), ) }; let urls = contents_urls_from_sources(&repositories, &arch, load_url); self.load_urls(urls, load_url, false) } /// Load contents information from multiple URLs. /// /// # Arguments /// * `urls` - Iterator of URLs to load /// * `load_url` - Function to load a URL and get a reader /// /// # Returns /// Ok(()) if successful, Error otherwise fn load_urls( &mut self, urls: impl Iterator, load_url: impl Fn(&url::Url) -> Result, Error>, fail_on_error: bool, ) -> Result<(), Error> { let urls: Vec = urls.collect(); let num_urls = urls.len(); if num_urls == 0 { return Ok(()); } log::info!( "Loading {} APT Contents files (this may take several minutes)...", num_urls ); let mut success_count = 0; let mut contents = Vec::new(); // Try to load each URL for (idx, url) in urls.iter().enumerate() { log::info!("Loading Contents file {}/{}: {}", idx + 1, num_urls, url); match load_url(&url) { Ok(reader) => { // Read the entire content into memory let mut content = Vec::new(); let mut reader = reader; match std::io::Read::read_to_end(&mut reader, &mut content) { Ok(size) => { log::info!("Successfully loaded {} bytes from {}", size, url); contents.push((url.clone(), content)); success_count += 1; } Err(e) => { if fail_on_error { return Err(Error::AptFileAccessError(format!( "Failed to read Contents from {}: {}", url, e ))); } else { log::warn!("Failed to read Contents from {}: {}", url, e); } } } } Err(e) => { if fail_on_error { return Err(e); } else { log::warn!("Failed to load Contents from {}: {}", url, e); } } } } log::info!( "Successfully loaded {}/{} Contents files", success_count, num_urls ); if success_count == 0 { return Err(Error::AptFileAccessError( "Failed to download any APT Contents files".to_string(), )); } // Process the successfully loaded files for (url, content) in contents { let reader = Box::new(std::io::Cursor::new(content)); self.load_file(reader, url); } Ok(()) } /// Search for files in Debian packages using a matcher function. /// /// This is an internal implementation method used by the FileSearcher trait methods. /// /// # Arguments /// * `matches` - Function that returns true for paths that match the search criteria /// /// # Returns /// Iterator of package names containing matching files pub fn search_files_ex<'a>( &'a self, mut matches: impl FnMut(&Path) -> bool + 'a, ) -> Box + 'a> { Box::new( self.db .iter() .filter(move |(p, _)| matches(Path::new(p))) .map(|(_, rest)| { std::str::from_utf8(rest.split(|c| *c == b'/').last().unwrap()) .unwrap() .to_string() }), ) } /// Load contents information from a file. /// /// # Arguments /// * `f` - Reader for the contents file /// * `url` - URL of the contents file (for logging) fn load_file(&mut self, f: impl Read, url: url::Url) { let start_time = std::time::Instant::now(); for (path, rest) in read_contents_file(f) { self.db.insert(path, rest.into()); } log::debug!("Read {} in {:?}", url, start_time.elapsed()); } } impl FileSearcher<'_> for RemoteContentsFileSearcher { /// Search for files by exact path. /// /// This implementation uses the remote Contents database to find packages /// containing the specified file path. /// /// # Arguments /// * `path` - Path to search for /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing the file fn search_files<'a>( &'a self, path: &'a Path, case_insensitive: bool, ) -> Box + 'a> { let path = if case_insensitive { PathBuf::from(path.to_str().unwrap().to_lowercase()) } else { path.to_owned() }; return Box::new(self.search_files_ex(move |p| { if case_insensitive { p.to_str().unwrap().to_lowercase() == path.to_str().unwrap() } else { p == path } })); } /// Search for files by regular expression. /// /// This implementation uses the remote Contents database to find packages /// containing files matching the specified regex pattern. /// /// # Arguments /// * `path` - Regular expression pattern to match against file paths /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_regex<'a>( &'a self, path: &'a str, case_insensitive: bool, ) -> Box + 'a> { let re = regex::RegexBuilder::new(path) .case_insensitive(case_insensitive) .build() .unwrap(); return Box::new(self.search_files_ex(move |p| { if case_insensitive { re.is_match(&p.to_str().unwrap().to_lowercase()) } else { re.is_match(p.to_str().unwrap()) } })); } } #[derive(Debug, Clone)] /// File searcher that uses a pre-generated list of file paths and package names. /// /// This searcher is useful for static file path to package mappings that /// are known in advance. pub struct GeneratedFileSearcher { /// Database of file path and package name pairs db: Vec<(PathBuf, String)>, } impl GeneratedFileSearcher { /// Create a new GeneratedFileSearcher. pub fn new(db: Vec<(PathBuf, String)>) -> GeneratedFileSearcher { Self { db } } /// Create an empty GeneratedFileSearcher. pub fn empty() -> GeneratedFileSearcher { Self::new(vec![]) } /// Create a new GeneratedFileSearcher from a file. /// /// # Arguments /// * `path` - The path to the file to load. pub fn from_path(path: &Path) -> GeneratedFileSearcher { let mut ret = Self::new(vec![]); ret.load_from_path(path); ret } /// Load the contents of a file into the database. /// /// # Arguments /// * `path` - The path to the file to load. pub fn load_from_path(&mut self, path: &Path) { let f = File::open(path).unwrap(); let f = BufReader::new(f); for line in f.lines() { let line = line.unwrap(); let (path, pkg) = line.split_once(' ').unwrap(); self.db.push((path.into(), pkg.to_owned())); } } fn search_files_ex<'a>( &'a self, mut matches: impl FnMut(&Path) -> bool + 'a, ) -> Box + 'a> { let x = self .db .iter() .filter(move |(p, _)| matches(p)) .map(|(_, pkg)| pkg.to_string()); Box::new(x) } } impl FileSearcher<'_> for GeneratedFileSearcher { /// Search for files by exact path. /// /// This implementation uses the pre-generated database to find packages /// containing the specified file path. /// /// # Arguments /// * `path` - Path to search for /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing the file fn search_files<'a>( &'a self, path: &'a Path, case_insensitive: bool, ) -> Box + 'a> { let path = if case_insensitive { PathBuf::from(path.to_str().unwrap().to_lowercase()) } else { path.to_owned() }; self.search_files_ex(move |p: &Path| { if case_insensitive { PathBuf::from(p.to_str().unwrap().to_lowercase()) == path } else { p == path } }) } /// Search for files by regular expression. /// /// This implementation uses the pre-generated database to find packages /// containing files matching the specified regex pattern. /// /// # Arguments /// * `path` - Regular expression pattern to match against file paths /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_regex<'a>( &'a self, path: &'a str, case_insensitive: bool, ) -> Box + 'a> { let re = regex::RegexBuilder::new(path) .case_insensitive(case_insensitive) .build() .unwrap(); return self.search_files_ex(move |p| re.is_match(p.to_str().unwrap())); } } // TODO(jelmer): read from a file lazy_static::lazy_static! { /// Pre-generated static file searcher with common Debian package files. /// /// This provides a mapping of common file paths to their providing packages. pub static ref GENERATED_FILE_SEARCHER: GeneratedFileSearcher = GeneratedFileSearcher::new(vec![ (PathBuf::from("/etc/locale.gen"), "locales".to_string()), // Alternative (PathBuf::from("/usr/bin/rst2html"), "python3-docutils".to_string()), // aclocal is a symlink to aclocal-1.XY (PathBuf::from("/usr/bin/aclocal"), "automake".to_string()), (PathBuf::from("/usr/bin/automake"), "automake".to_string()), // maven lives in /usr/share (PathBuf::from("/usr/bin/mvn"), "maven".to_string()), ]); } /// Get a list of packages that provide the given paths. /// /// # Arguments /// * `paths` - A list of paths to search for. /// * `searchers` - A list of searchers to use. /// * `regex` - Whether the paths are regular expressions. /// * `case_insensitive` - Whether the search should be case-insensitive. /// /// # Returns /// A list of packages that provide the given paths. /// Get packages that contain the specified paths. /// /// # Arguments /// * `paths` - Paths to search for /// * `searchers` - File searchers to use /// * `regex` - Whether to treat paths as regular expressions /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// List of package names that contain the specified paths pub fn get_packages_for_paths( paths: Vec<&str>, searchers: &[&dyn FileSearcher], regex: bool, case_insensitive: bool, ) -> Vec { let mut candidates = vec![]; // TODO(jelmer): Combine these, perhaps by creating one gigantic regex? for path in paths { for searcher in searchers { for pkg in if regex { searcher.search_files_regex(path, case_insensitive) } else { searcher.search_files(Path::new(path), case_insensitive) } { if !candidates.contains(&pkg) { candidates.push(pkg); } } } } candidates } /// File searcher that uses an in-memory map of file paths to package names. /// /// This searcher is more efficient for small datasets that can fit entirely /// in memory. pub struct MemoryAptSearcher(std::collections::HashMap); impl MemoryAptSearcher { /// Create a new MemoryAptSearcher with the given database. /// /// # Arguments /// * `db` - Map of file paths to package names /// /// # Returns /// A new MemoryAptSearcher instance pub fn new(db: std::collections::HashMap) -> MemoryAptSearcher { MemoryAptSearcher(db) } } impl FileSearcher<'_> for MemoryAptSearcher { /// Search for files by exact path. /// /// This implementation uses the in-memory database to find packages /// containing the specified file path. /// /// # Arguments /// * `path` - Path to search for /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing the file fn search_files<'a>( &'a self, path: &'a Path, case_insensitive: bool, ) -> Box + 'a> { if case_insensitive { Box::new( self.0 .iter() .filter(move |(p, _)| { p.to_str().unwrap().to_lowercase() == path.to_str().unwrap() }) .map(|(_, pkg)| pkg.to_string()), ) } else { let hit = self.0.get(path); if let Some(hit) = hit { Box::new(std::iter::once(hit.clone())) } else { Box::new(std::iter::empty()) } } } /// Search for files by regular expression. /// /// This implementation uses the in-memory database to find packages /// containing files matching the specified regex pattern. /// /// # Arguments /// * `path` - Regular expression pattern to match against file paths /// * `case_insensitive` - Whether to ignore case when matching /// /// # Returns /// Iterator of package names containing matching files fn search_files_regex<'a>( &'a self, path: &str, case_insensitive: bool, ) -> Box + 'a> { log::debug!("Searching for {} in {:?}", path, self.0.keys()); let re = regex::RegexBuilder::new(path) .case_insensitive(case_insensitive) .build() .unwrap(); Box::new( self.0 .iter() .filter(move |(p, _)| re.is_match(p.to_str().unwrap())) .map(|(_, pkg)| pkg.to_string()), ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_uri_to_filename() { assert_eq!( uri_to_filename(&"http://example.com/foo/bar".parse().unwrap()), "example.com_foo_bar" ); } #[test] fn test_generated_file_searchers() { let searchers = &GENERATED_FILE_SEARCHER; assert_eq!( searchers .search_files(Path::new("/etc/locale.gen"), false) .collect::>(), vec!["locales"] ); assert_eq!( searchers .search_files(Path::new("/etc/LOCALE.GEN"), true) .collect::>(), vec!["locales"] ); assert_eq!( searchers .search_files(Path::new("/usr/bin/rst2html"), false) .collect::>(), vec!["python3-docutils"] ); } #[test] fn test_unwrap_plain() { let data = b"hello world"; let f = std::io::Cursor::new(data); let mut f = unwrap(f, "").unwrap(); let mut buf = Vec::new(); f.read_to_end(&mut buf).unwrap(); assert_eq!(buf, b"hello world"); } #[test] fn test_unwrap_gz() { use flate2::write::GzEncoder; use flate2::Compression; use std::io::Write; let original = b"hello world from gzip"; let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); encoder.write_all(original).unwrap(); let compressed = encoder.finish().unwrap(); let f = std::io::Cursor::new(compressed); let mut f = unwrap(f, ".gz").unwrap(); let mut buf = Vec::new(); f.read_to_end(&mut buf).unwrap(); assert_eq!(buf, original); } #[test] fn test_unwrap_xz() { use lzma_rs::lzma_compress; let original = b"hello world from xz"; let mut compressed = Vec::new(); lzma_compress(&mut original.as_ref(), &mut compressed).unwrap(); let f = std::io::Cursor::new(compressed); let mut f = unwrap(f, ".xz").unwrap(); let mut buf = Vec::new(); f.read_to_end(&mut buf).unwrap(); assert_eq!(buf, original); } #[test] fn test_unwrap_corrupt_xz() { // Test that corrupt XZ data returns an error, not a panic let corrupt_data = b"this is not valid xz data"; let f = std::io::Cursor::new(corrupt_data); let result = unwrap(f, ".xz"); assert!(result.is_err()); if let Err(Error::DecompressionError(msg)) = result { assert!(msg.contains("LZMA")); } else { panic!("Expected DecompressionError"); } } #[test] fn test_setup_apt_file() { use crate::session::unshare::{create_debian_session_for_testing, UnshareSession}; fn test_session() -> Option { // Don't run tests if we're in github actions (CI environment restrictions) if std::env::var("GITHUB_ACTIONS").is_ok() { return None; } create_debian_session_for_testing("sid", false).ok() } let session = if let Some(session) = test_session() { session } else { return; }; // Test that setup_apt_file runs without errors let result = setup_apt_file(&session); assert!(result.is_ok(), "setup_apt_file failed: {:?}", result); // Verify apt-file is installed and functional let output = session .command(vec!["apt-file", "--help"]) .output() .expect("Failed to run apt-file --help"); assert!(output.status.success(), "apt-file --help failed"); // Verify apt-file cache exists (Contents files should be downloaded) let cache_check = session .command(vec!["ls", "/var/cache/apt/apt-file/"]) .output() .expect("Failed to check apt-file cache"); assert!( cache_check.status.success(), "apt-file cache directory not found" ); // Test that apt-file can actually search for a file let search_result = session .command(vec!["apt-file", "search", "bin/ls"]) .output() .expect("Failed to run apt-file search"); assert!(search_result.status.success(), "apt-file search failed"); let search_output = String::from_utf8_lossy(&search_result.stdout); assert!( !search_output.trim().is_empty(), "apt-file search returned no results" ); assert!( search_output.contains("coreutils"), "Expected coreutils package in search results" ); } } ognibuild-0.2.6/src/debian/fix_build.rs000064400000000000000000000227661046102023000161460ustar 00000000000000use crate::debian::build::{attempt_build, BuildOnceError, BuildOnceResult}; use crate::debian::context::Error; use crate::debian::context::Phase; pub use crate::fix_build::InterimError; use breezyshim::error::Error as BrzError; use breezyshim::workingtree::{PyWorkingTree, WorkingTree}; use breezyshim::workspace::reset_tree; use buildlog_consultant::Match; use buildlog_consultant::Problem; use std::path::{Path, PathBuf}; /// Rescue a build log and store it in the users' cache directory pub fn rescue_build_log( output_directory: &Path, tree: Option<&dyn WorkingTree>, ) -> Result<(), std::io::Error> { let xdg_cache_dir = match dirs::cache_dir() { Some(dir) => dir, None => { log::warn!("Unable to determine cache directory, not saving build log."); return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Unable to find cache directory", )); } }; let buildlogs_dir = xdg_cache_dir.join("ognibuild/buildlogs"); std::fs::create_dir_all(&buildlogs_dir)?; let target_log_file = buildlogs_dir.join(format!( "{}-{}.log", tree.map_or_else(|| PathBuf::from("build"), |t| t.basedir()) .display(), chrono::Local::now().format("%Y-%m-%d_%H%M%s"), )); std::fs::copy(output_directory.join("build.log"), &target_log_file)?; log::info!("Build log available in {}", target_log_file.display()); Ok(()) } /// A fixer is a struct that can resolve a specific type of problem. pub trait DebianBuildFixer: std::fmt::Debug + std::fmt::Display { /// Check if this fixer can potentially resolve the given problem. fn can_fix(&self, problem: &dyn Problem) -> bool; /// Attempt to resolve the given problem. fn fix(&self, problem: &dyn Problem, phase: &Phase) -> Result>; } /// Attempt to resolve a build error by applying appropriate fixers. /// /// This function finds and applies fixers that can handle the given problem /// in the specified build phase. /// /// # Arguments /// * `problem` - The build problem to fix /// * `phase` - The build phase in which the problem occurred /// * `fixers` - List of available fixers to try /// /// # Returns /// * `Ok(true)` if a fixer successfully resolved the issue /// * `Ok(false)` if no applicable fixer was found /// * `Err(InterimError)` if a fixer encountered an error pub fn resolve_error( problem: &dyn Problem, phase: &Phase, fixers: &[&dyn DebianBuildFixer], ) -> Result> { let relevant_fixers = fixers .iter() .filter(|fixer| fixer.can_fix(problem)) .collect::>(); if relevant_fixers.is_empty() { log::warn!("No fixer found for {:?}", problem); return Ok(false); } for fixer in relevant_fixers { log::info!("Attempting to use fixer {} to address {:?}", fixer, problem); let made_changes = fixer.fix(problem, phase)?; if made_changes { return Ok(true); } } Ok(false) } /// Error result from repeatedly running and attemptin to fix issues. #[derive(Debug)] pub enum IterateBuildError { /// The limit of fixing attempts was reached. FixerLimitReached(usize), /// A problem was detected that was recognized but could not be fixed. Persistent(Phase, Box), /// An error that we could not identify. Unidentified { /// The return code of the failed command retcode: i32, /// The output lines from the command lines: Vec, /// Optional secondary information about the error secondary: Option>, /// The build phase in which the error occurred phase: Option, }, /// The build phase could not be determined MissingPhase, /// An error occurred while resetting the tree ResetTree(BrzError), /// Another error raised specifically by the callback function that is not fixable. Other(Error), } /// Build a Debian package incrementally, with automatic error fixing. /// /// This function attempts to build a Debian package, and if the build fails, /// it tries to fix the errors automatically using the provided fixers. /// It will retry the build after each fix until either the build succeeds, /// or it encounters an unfixable error. /// /// # Arguments /// * `local_tree` - Working tree containing the package source /// * `suffix` - Optional suffix for the binary package version /// * `build_suite` - Optional distribution suite to build for /// * `output_directory` - Directory where build artifacts will be stored /// * `build_command` - Command to use for building (e.g., "dpkg-buildpackage") /// * `fixers` - List of fixers to apply if build errors are encountered /// * `build_changelog_entry` - Optional changelog entry to add before building /// * `max_iterations` - Maximum number of fix attempts before giving up /// * `subpath` - Path within the working tree where the package is located /// * `source_date_epoch` - Optional timestamp for reproducible builds /// * `apt_repository` - Optional URL of an APT repository to use /// * `apt_repository_key` - Optional GPG key for the APT repository /// * `extra_repositories` - Optional additional APT repositories to use /// * `run_gbp_dch` - Whether to run git-buildpackage's dch command /// /// # Returns /// * `Ok(BuildOnceResult)` if the build succeeded /// * `Err(IterateBuildError)` if the build failed and could not be fixed pub fn build_incrementally( local_tree: &dyn PyWorkingTree, suffix: Option<&str>, build_suite: Option<&str>, output_directory: &Path, build_command: &str, fixers: &[&dyn DebianBuildFixer], build_changelog_entry: Option<&str>, max_iterations: Option, subpath: &Path, source_date_epoch: Option>, apt_repository: Option<&str>, apt_repository_key: Option<&str>, extra_repositories: Option>, run_gbp_dch: bool, ) -> Result { let mut fixed_errors: Vec<(Box, Phase)> = vec![]; log::info!("Using fixers: {:?}", fixers); loop { match attempt_build( local_tree, suffix, build_suite, output_directory, build_command, build_changelog_entry, subpath, source_date_epoch, run_gbp_dch, apt_repository, apt_repository_key, extra_repositories.as_ref(), ) { Ok(result) => { return Ok(result); } Err(BuildOnceError::Unidentified { stage: _, phase, retcode, command: _, description: _, }) => { log::warn!("Build failed with unidentified error. Giving up."); return Err(IterateBuildError::Unidentified { phase, retcode, lines: vec![], secondary: None, }); } Err(BuildOnceError::Detailed { phase, error, .. }) => { if phase.is_none() { log::info!("No relevant context, not making any changes."); return Err(IterateBuildError::MissingPhase); } let phase = phase.unwrap(); if fixed_errors.iter().any(|(e, p)| e == &error && p == &phase) { log::warn!("Error was still not fixed on second try. Giving up."); return Err(IterateBuildError::Persistent(phase, error)); } if max_iterations .map(|max| fixed_errors.len() >= max) .unwrap_or(false) { log::warn!("Max iterations reached. Giving up."); return Err(IterateBuildError::FixerLimitReached( max_iterations.unwrap(), )); } reset_tree(local_tree, None, Some(subpath)) .map_err(IterateBuildError::ResetTree)?; match resolve_error(error.as_ref(), &phase, fixers) { Ok(false) => { log::warn!("Failed to resolve error {:?}. Giving up.", error); return Err(IterateBuildError::Persistent(phase, error)); } Ok(true) => {} Err(InterimError::Other(e)) => { return Err(IterateBuildError::Other(e)); } Err(InterimError::Recognized(p)) => { if &error != &p { log::warn!("Detected problem while fixing {:?}: {:?}", error, p); } return Err(IterateBuildError::Persistent(phase, error)); } Err(InterimError::Unidentified { retcode, lines, secondary, }) => { log::warn!("Recognized error but unable to resolve: {:?}", lines); return Err(IterateBuildError::Unidentified { phase: Some(phase), retcode, lines, secondary, }); } } fixed_errors.push((error, phase)); } } } } ognibuild-0.2.6/src/debian/fixers.rs000064400000000000000000000436261046102023000154770ustar 00000000000000use crate::debian::apt::AptManager; use crate::debian::context::{DebianPackagingContext, Error}; use crate::debian::fix_build::DebianBuildFixer; use crate::dependencies::debian::{DebianDependency, TieBreaker}; use crate::session::Session; use breezyshim::tree::Tree; use breezyshim::workingtree::WorkingTree; use buildlog_consultant::problems::common::NeedPgBuildExtUpdateControl; use buildlog_consultant::sbuild::Phase; use buildlog_consultant::Problem; use debian_analyzer::editor::Editor; use std::path::Path; /// Extract targeted Python versions from a package's build dependencies. /// /// This function parses the debian/control file to determine which Python /// versions (python3, pypy, etc.) are targeted by the package's build dependencies. /// /// # Arguments /// * `tree` - The working tree containing the package /// * `subpath` - Path to the package within the tree /// /// # Returns /// A list of Python version strings that the package targets fn targeted_python_versions(tree: &dyn Tree, subpath: &Path) -> Vec { let f = tree.get_file(&subpath.join("debian/control")).unwrap(); let control = debian_control::Control::read(f).unwrap(); let source = control.source().unwrap(); let all = if let Some(build_depends) = source.build_depends() { build_depends } else { return vec![]; }; let targeted = vec![]; for entry in all.entries() { for relation in entry.relations() { let mut targeted = vec![]; if relation.name().starts_with("python3-") { targeted.push("python3".to_owned()); } if relation.name().starts_with("pypy") { targeted.push("pypy".to_owned()); } if relation.name().starts_with("python-") { targeted.push("python".to_owned()); } } } targeted } /// Tie-breaker for Python dependencies based on targeted Python versions. /// /// This tie-breaker helps select appropriate Python dependencies based on /// the Python versions targeted by the package as specified in its build /// dependencies. pub struct PythonTieBreaker { /// List of targeted Python versions (e.g., "python3", "pypy") targeted: Vec, } impl PythonTieBreaker { fn from_tree(tree: &dyn Tree, subpath: &Path) -> Self { let targeted = targeted_python_versions(tree, subpath); Self { targeted } } } impl TieBreaker for PythonTieBreaker { fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { if self.targeted.is_empty() { return None; } fn same(pkg: &str, python_version: &str) -> bool { if pkg.starts_with(&format!("{}-", python_version)) { return true; } if pkg.starts_with(&format!("lib{}-", python_version)) { return true; } pkg == format!("lib{}-dev", python_version) } for python_version in &self.targeted { for req in reqs { if req .package_names() .iter() .any(|name| same(name, &python_version)) { log::info!( "Breaking tie between {:?} to {:?}, since package already has {} build-dependencies", reqs, req, python_version, ); return Some(req); } } } None } } /// Handle APT fetch failures by simply retrying. /// /// This fixer deals with transient APT fetch failures by indicating that /// the build should be retried. /// /// # Arguments /// * `_error` - The APT fetch failure problem /// * `_phase` - The build phase in which the error occurred /// * `_context` - The Debian packaging context /// /// # Returns /// Always returns Ok(true) to indicate the build should be retried fn retry_apt_failure( _error: &dyn Problem, _phase: &Phase, _context: &DebianPackagingContext, ) -> Result { Ok(true) } /// Enable dh-autoreconf in debian/rules. /// /// This function adds dh-autoreconf to debian/rules to handle autoconf-related /// build issues. /// /// # Arguments /// * `context` - The Debian packaging context /// * `phase` - The build phase in which autoreconf is needed /// /// # Returns /// Ok(true) if successful, Error otherwise fn enable_dh_autoreconf(context: &DebianPackagingContext, phase: &Phase) -> Result { // Debhelper >= 10 depends on dh-autoreconf and enables autoreconf by default. let debhelper_compat_version = debian_analyzer::debhelper::get_debhelper_compat_level(&context.abspath(Path::new("."))) .unwrap(); if !debhelper_compat_version .map(|dcv| dcv < 10) .unwrap_or(false) { return Ok(false); } let mut modified = false; let rules = context.edit_rules()?; for mut rule in rules.rules_by_target("%") { for (i, line) in rule.recipes().enumerate() { if !line.starts_with("dh ") { continue; } let new_line = debian_analyzer::rules::dh_invoke_add_with(&line, "autoreconf"); if line != new_line { rule.replace_command(i, &new_line); modified = true; } } } if modified { context.add_dependency(phase, &DebianDependency::simple("dh-autoreconf")) } else { Ok(false) } } fn fix_missing_configure( _error: &dyn Problem, phase: &Phase, context: &DebianPackagingContext, ) -> Result { if !context.has_filename(Path::new("configure.ac")) && !context.has_filename(Path::new("configure.in")) { return Ok(false); } enable_dh_autoreconf(context, phase) } fn fix_missing_automake_input( _error: &dyn Problem, phase: &Phase, context: &DebianPackagingContext, ) -> Result { // TODO(jelmer): If it's ./NEWS, ./AUTHORS or ./README that's missing, then // try to set 'export AUTOMAKE = automake --foreign' in debian/rules. // https://salsa.debian.org/jelmer/debian-janitor/issues/88 enable_dh_autoreconf(context, phase) } fn fix_missing_config_status_input( _error: &dyn Problem, _phase: &Phase, context: &DebianPackagingContext, ) -> Result { let autogen_path = "autogen.sh"; if !context.has_filename(Path::new(autogen_path)) { return Ok(false); } let mut rules = context.edit_rules()?; let rule_exists = rules .rules_by_target("override_dh_autoreconf") .next() .is_some(); if rule_exists { return Ok(false); } let mut rule = rules.add_rule("override_dh_autoreconf"); rule.push_command("dh_autoreconf ./autogen.sh"); rules.commit()?; context.commit("Run autogen.sh during build.", None) } /// Fixer that resolves missing package dependencies. /// /// This fixer identifies missing dependencies in build errors and adds them /// to the package's build dependencies in debian/control. pub struct PackageDependencyFixer<'a, 'b, 'c> where 'c: 'a, { /// APT package manager for dependency resolution apt: &'a AptManager<'c>, /// Debian packaging context for making changes to the package context: &'b DebianPackagingContext, /// List of tie-breakers for selecting between alternative dependencies tie_breakers: Vec>, } impl<'a, 'b, 'c> std::fmt::Display for PackageDependencyFixer<'a, 'b, 'c> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PackageDependencyFixer") } } impl<'a, 'b, 'c> std::fmt::Debug for PackageDependencyFixer<'a, 'b, 'c> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PackageDependencyFixer") } } impl<'a, 'b, 'c> DebianBuildFixer for PackageDependencyFixer<'a, 'b, 'c> { fn can_fix(&self, problem: &dyn Problem) -> bool { crate::buildlog::problem_to_dependency(problem).is_some() } fn fix( &self, problem: &dyn Problem, phase: &Phase, ) -> Result> { let dep = crate::buildlog::problem_to_dependency(problem).unwrap(); let deb_dep = crate::debian::apt::dependency_to_deb_dependency( &self.apt, dep.as_ref(), self.tie_breakers.as_slice(), ) .unwrap(); let deb_dep = if let Some(deb_dep) = deb_dep { deb_dep } else { return Ok(false); }; Ok(self.context.add_dependency(phase, &deb_dep).unwrap()) } } /// Fixer that updates PostgreSQL build extension control files. /// /// This fixer handles the case where pg_buildext detects that control files /// are out of date and need to be updated. pub struct PgBuildExtOutOfDateControlFixer<'a, 'b, 'c, 'd> where 'a: 'c, { /// Session for executing commands session: &'a dyn Session, /// Debian packaging context for making changes to the package context: &'b DebianPackagingContext, /// APT package manager for dependency resolution apt: &'c AptManager<'d>, } impl<'a, 'b, 'c, 'd> std::fmt::Debug for PgBuildExtOutOfDateControlFixer<'a, 'b, 'c, 'd> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PgBuildExtOutOfDateControlFixer") } } impl<'a, 'b, 'c, 'd> std::fmt::Display for PgBuildExtOutOfDateControlFixer<'a, 'b, 'c, 'd> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PgBuildExtOutOfDateControlFixer") } } impl<'a, 'b, 'c, 'd> DebianBuildFixer for PgBuildExtOutOfDateControlFixer<'a, 'b, 'c, 'd> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix( &self, error: &dyn Problem, _phase: &Phase, ) -> std::result::Result> { let error = error .as_any() .downcast_ref::() .unwrap(); log::info!("Running 'pg_buildext updatecontrol'"); self.apt .satisfy(vec![crate::debian::apt::SatisfyEntry::Required( "postgresql-common".to_string(), )]) .unwrap(); let project = self .session .project_from_directory(&self.context.tree.abspath(Path::new(".")).unwrap(), None) .unwrap(); self.session .command(vec!["pg_buildext", "updatecontrol"]) .cwd(&project.internal_path()) .check_call() .unwrap(); std::fs::copy( project.internal_path().join(&error.generated_path), self.context.abspath(Path::new(&error.generated_path)), ) .unwrap(); self.context .commit("Run 'pgbuildext updatecontrol'.", Some(false))?; Ok(true) } } fn fix_missing_makefile_pl( error: &buildlog_consultant::problems::common::MissingPerlFile, _phase: &Phase, context: &DebianPackagingContext, ) -> Result { if error.filename == "Makefile.PL" && !context.has_filename(Path::new("Makefile.PL")) && context.has_filename(Path::new("dist.ini")) { // TODO(jelmer): add dist-zilla add-on to debhelper unimplemented!() } return Ok(false); } fn debcargo_coerce_unacceptable_prerelease( _error: &dyn Problem, _phase: &Phase, context: &DebianPackagingContext, ) -> Result { let path = context.abspath(Path::new("debian/debcargo.toml")); let text = std::fs::read_to_string(&path)?; let mut doc: toml_edit::DocumentMut = text.parse().unwrap(); doc.as_table_mut()["allow_prerelease_deps"] = toml_edit::value(true); std::fs::write(&path, doc.to_string())?; context.commit("Enable allow_prerelease_deps.", None)?; Ok(true) } /// Macro to generate simple build fixers. /// /// This macro creates structs that implement the DebianBuildFixer trait /// for specific problem types, delegating the actual fixing to the provided /// function. macro_rules! simple_build_fixer { ($name:ident, $problem_cls:ty, $fn:expr) => { #[doc = concat!("Fixer for ", stringify!($problem_cls), " problems.")] /// /// This fixer detects and attempts to resolve specific build problems /// by delegating to an appropriate fixing function. pub struct $name<'a>(&'a DebianPackagingContext); impl<'a> std::fmt::Display for $name<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", stringify!($name)) } } impl<'a> std::fmt::Debug for $name<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", stringify!($name)) } } impl<'a> DebianBuildFixer for $name<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem.as_any().downcast_ref::<$problem_cls>().is_some() } fn fix( &self, error: &dyn Problem, phase: &Phase, ) -> std::result::Result< bool, crate::fix_build::InterimError, > { let error = error.as_any().downcast_ref::<$problem_cls>().unwrap(); $fn(error, phase, self.0).map_err(|e| crate::fix_build::InterimError::Other(e)) } } }; } simple_build_fixer!( MissingConfigureFixer, buildlog_consultant::problems::common::MissingConfigure, fix_missing_configure ); simple_build_fixer!( MissingAutomakeInputFixer, buildlog_consultant::problems::common::MissingAutomakeInput, fix_missing_automake_input ); simple_build_fixer!( MissingConfigStatusInputFixer, buildlog_consultant::problems::common::MissingConfigStatusInput, fix_missing_config_status_input ); simple_build_fixer!( MissingPerlFileFixer, buildlog_consultant::problems::common::MissingPerlFile, fix_missing_makefile_pl ); simple_build_fixer!( DebcargoUnacceptablePredicateFixer, buildlog_consultant::problems::debian::DebcargoUnacceptablePredicate, debcargo_coerce_unacceptable_prerelease ); simple_build_fixer!( DebcargoUnacceptableComparatorFixer, buildlog_consultant::problems::debian::DebcargoUnacceptableComparator, debcargo_coerce_unacceptable_prerelease ); simple_build_fixer!( RetryAptFetchFailure, buildlog_consultant::problems::debian::AptFetchFailure, retry_apt_failure ); /// Create a collection of all available Debian build fixers. /// /// This function creates and returns all the build fixers available for /// fixing Debian package build problems. /// /// # Arguments /// * `session` - Session for running commands /// * `packaging_context` - Packaging context for making changes to the package /// * `apt` - APT manager for package installation and queries /// /// # Returns /// A vector of boxed build fixers pub fn versioned_package_fixers<'a, 'b, 'c, 'd, 'e>( session: &'c dyn Session, packaging_context: &'b DebianPackagingContext, apt: &'a AptManager<'e>, ) -> Vec> where 'a: 'd, 'b: 'd, 'c: 'd, 'c: 'a, { vec![ Box::new(PgBuildExtOutOfDateControlFixer { context: packaging_context, session, apt, }), Box::new(MissingConfigureFixer(packaging_context)), Box::new(MissingAutomakeInputFixer(packaging_context)), Box::new(MissingConfigStatusInputFixer(packaging_context)), Box::new(MissingPerlFileFixer(packaging_context)), Box::new(DebcargoUnacceptablePredicateFixer(packaging_context)), Box::new(DebcargoUnacceptableComparatorFixer(packaging_context)), ] } /// Create APT-specific Debian build fixers. /// /// This function creates fixers that handle APT-related build issues. /// /// # Arguments /// * `apt` - APT manager for package installation and queries /// * `packaging_context` - Packaging context for making changes to the package /// /// # Returns /// A vector of boxed build fixers for APT-related issues pub fn apt_fixers<'a, 'b, 'c, 'd>( apt: &'a AptManager<'d>, packaging_context: &'b DebianPackagingContext, ) -> Vec> where 'a: 'c, 'b: 'c, { let apt_tie_breakers: Vec> = vec![ Box::new(PythonTieBreaker::from_tree( &packaging_context.tree, &packaging_context.subpath, )), Box::new(crate::debian::build_deps::BuildDependencyTieBreaker::from_session(apt.session())), #[cfg(feature = "udd")] Box::new(crate::debian::udd::PopconTieBreaker), ]; vec![ Box::new(RetryAptFetchFailure(packaging_context)) as Box, Box::new(PackageDependencyFixer { context: packaging_context, apt, tie_breakers: apt_tie_breakers, }) as Box, ] } /// Create a set of default Debian build fixers. /// /// This function creates a standard set of build fixers that can handle /// common build problems. /// /// # Arguments /// * `packaging_context` - Packaging context for making changes to the package /// * `apt` - APT manager for package installation and queries /// /// # Returns /// A vector of boxed build fixers for common build issues pub fn default_fixers<'a, 'b, 'c, 'd>( packaging_context: &'a DebianPackagingContext, apt: &'b AptManager<'d>, ) -> Vec> where 'a: 'c, 'b: 'c, { let mut ret = Vec::new(); ret.extend(versioned_package_fixers( apt.session(), packaging_context, apt, )); ret.extend(apt_fixers(apt, packaging_context)); ret } ognibuild-0.2.6/src/debian/mod.rs000064400000000000000000000045431046102023000147510ustar 00000000000000//! Debian packaging support for ognibuild. //! //! This module provides functionality for working with Debian packages, //! including managing build dependencies, interacting with APT, //! fixing build issues, and working with Debian package sources. /// APT package management functionality. pub mod apt; /// Debian package build functionality. pub mod build; /// Build dependency resolution for Debian packages. pub mod build_deps; /// Context management for Debian operations. pub mod context; /// Dependency server integration. #[cfg(feature = "dep-server")] pub mod dep_server; /// File search utilities for Debian packages. pub mod file_search; /// Debian-specific build fixing functionality. pub mod fix_build; /// Build fixers for Debian packages. pub mod fixers; /// Ultimate Debian Database integration. #[cfg(feature = "udd")] pub mod udd; /// Upstream dependency handling for Debian packages. pub mod upstream_deps; use breezyshim::tree::{Path, Tree}; use crate::session::Session; /// Satisfy build dependencies for a Debian package. /// /// This function parses the debian/control file and installs all required /// build dependencies while ensuring conflicts are resolved. /// /// # Arguments /// * `session` - Session to run commands in /// * `tree` - Tree representing the package source /// * `debian_path` - Path to the debian directory /// /// # Returns /// Ok on success, Error if dependencies cannot be satisfied pub fn satisfy_build_deps( session: &dyn Session, tree: &dyn Tree, debian_path: &Path, ) -> Result<(), apt::Error> { let path = debian_path.join("control"); let f = tree.get_file_text(&path).unwrap(); let control: debian_control::Control = String::from_utf8(f).unwrap().parse().unwrap(); let source = control.source().unwrap(); let mut deps = vec![]; for dep in source .build_depends() .iter() .chain(source.build_depends_indep().iter()) .chain(source.build_depends_arch().iter()) { deps.push(apt::SatisfyEntry::Required(dep.to_string())); } for dep in source .build_conflicts() .iter() .chain(source.build_conflicts_indep().iter()) .chain(source.build_conflicts_arch().iter()) { deps.push(apt::SatisfyEntry::Conflict(dep.to_string())); } let apt_mgr = apt::AptManager::new(session, None); apt_mgr.satisfy(deps) } ognibuild-0.2.6/src/debian/sources_list.rs000064400000000000000000000136661046102023000167160ustar 00000000000000use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::Path; /// Entry in a Debian APT sources.list file. /// /// This enum represents the two types of entries that can appear in a /// sources.list file: 'deb' for binary packages and 'deb-src' for source packages. #[derive(Debug, PartialEq, Eq)] pub enum SourcesEntry { /// Binary package repository entry (deb line). Deb { /// Repository URI uri: String, /// Distribution name (e.g., "stable", "bullseye") dist: String, /// Component names (e.g., "main", "contrib", "non-free") comps: Vec, }, /// Source package repository entry (deb-src line). DebSrc { /// Repository URI uri: String, /// Distribution name (e.g., "stable", "bullseye") dist: String, /// Component names (e.g., "main", "contrib", "non-free") comps: Vec, }, } /// Parse a line from a sources.list file into a SourcesEntry. /// /// # Arguments /// * `line` - Line from sources.list to parse /// /// # Returns /// Some(SourcesEntry) if the line is a valid deb or deb-src line, /// None otherwise (e.g., comments, blank lines, invalid syntax) pub fn parse_sources_list_entry(line: &str) -> Option { let parts = line.split_whitespace().collect::>(); if parts.len() < 3 { return None; } let uri = parts[1]; let dist = parts[2]; let comps = parts[3..].iter().map(|x| x.to_string()).collect::>(); if parts[0] == "deb" { return Some(SourcesEntry::Deb { uri: uri.to_string(), dist: dist.to_string(), comps, }); } if parts[0] == "deb-src" { return Some(SourcesEntry::DebSrc { uri: uri.to_string(), dist: dist.to_string(), comps, }); } None } /// Representation of a Debian APT sources.list file. /// /// This struct holds a collection of SourcesEntry objects, representing /// the contents of one or more sources.list files. pub struct SourcesList { /// List of sources entries list: Vec, } impl SourcesList { /// Create an empty sources list. /// /// # Returns /// A new SourcesList with no entries pub fn empty() -> SourcesList { SourcesList { list: vec![] } } /// Get an iterator over the entries in this sources list. /// /// # Returns /// An iterator over references to SourcesEntry objects pub fn iter(&self) -> std::slice::Iter { self.list.iter() } /// Load sources entries from a file. /// /// # Arguments /// * `path` - Path to the sources.list file to load pub fn load(&mut self, path: &Path) { let f = File::open(path).unwrap(); for line in BufReader::new(f).lines() { let line = line.unwrap(); if let Some(entry) = parse_sources_list_entry(&line) { self.list.push(entry); } } } /// Create a SourcesList from an APT directory. /// /// This loads both the main sources.list file and any additional files /// in the sources.list.d directory. /// /// # Arguments /// * `apt_dir` - Path to the APT configuration directory (usually /etc/apt) /// /// # Returns /// A new SourcesList containing entries from all sources files pub fn from_apt_dir(apt_dir: &Path) -> SourcesList { let mut sl = SourcesList::empty(); sl.load(&apt_dir.join("sources.list")); for entry in apt_dir.read_dir().unwrap() { let entry = entry.unwrap(); if entry.file_type().unwrap().is_file() { let path = entry.path(); sl.load(&path); } } sl } } impl Default for SourcesList { fn default() -> Self { Self::from_apt_dir(Path::new("/etc/apt")) } } #[cfg(test)] mod tests { #[test] fn test_parse_sources_list_entry() { use super::parse_sources_list_entry; use super::SourcesEntry; assert_eq!( parse_sources_list_entry( "deb http://archive.ubuntu.com/ubuntu/ bionic main restricted" ), Some(SourcesEntry::Deb { uri: "http://archive.ubuntu.com/ubuntu/".to_string(), dist: "bionic".to_string(), comps: vec!["main".to_string(), "restricted".to_string()] }) ); assert_eq!( parse_sources_list_entry( "deb-src http://archive.ubuntu.com/ubuntu/ bionic main restricted" ), Some(SourcesEntry::DebSrc { uri: "http://archive.ubuntu.com/ubuntu/".to_string(), dist: "bionic".to_string(), comps: vec!["main".to_string(), "restricted".to_string()] }) ); // Test edge cases that should return None assert_eq!(parse_sources_list_entry(""), None); assert_eq!(parse_sources_list_entry("# comment"), None); assert_eq!(parse_sources_list_entry("deb"), None); assert_eq!(parse_sources_list_entry("deb http://example.com"), None); assert_eq!( parse_sources_list_entry("invalid-type http://example.com bionic main"), None ); } #[test] fn test_sources_list() { let td = tempfile::tempdir().unwrap(); let path = td.path().join("sources.list"); std::fs::write( &path, "deb http://archive.ubuntu.com/ubuntu/ bionic main restricted\n", ) .unwrap(); let mut sl = super::SourcesList::empty(); sl.load(&path); assert_eq!(sl.list.len(), 1); assert_eq!( sl.list[0], super::SourcesEntry::Deb { uri: "http://archive.ubuntu.com/ubuntu/".to_string(), dist: "bionic".to_string(), comps: vec!["main".to_string(), "restricted".to_string()] } ); } } ognibuild-0.2.6/src/debian/udd.rs000064400000000000000000000053131046102023000147420ustar 00000000000000use crate::dependencies::debian::DebianDependency; use crate::dependencies::debian::TieBreaker; use sqlx::{Error, PgPool}; use tokio::runtime::Runtime; /// Connection to the Ultimate Debian Database (UDD). /// /// UDD is a central Debian database that combines data from various /// Debian sources, such as the archive, the BTS, popcon, etc. pub struct UDD { /// Database connection pool pool: PgPool, } impl UDD { // Function to create a new instance of UDD with a database connection /// Connect to the UDD database. /// /// # Returns /// A new UDD instance connected to the database, or an error if the connection fails pub async fn connect() -> Result { let pool = PgPool::connect("postgres://udd-mirror:udd-mirror@udd-mirror.debian.net:5432/udd") .await .unwrap(); Ok(UDD { pool }) } } /// Find the most popular package from a list of dependencies according to popcon. /// /// # Arguments /// * `reqs` - List of Debian dependencies to choose from /// /// # Returns /// The name of the most popular package, or None if no package is found in popcon async fn get_most_popular(reqs: &[&DebianDependency]) -> Result, Error> { let udd = UDD::connect().await.unwrap(); let names = reqs .iter() .flat_map(|req| req.package_names()) .collect::>(); let (max_popcon_name,): (Option,) = sqlx::query_as( "SELECT package FROM popcon WHERE package IN $1 ORDER BY insts DESC LIMIT 1", ) .bind(names) .fetch_one(&udd.pool) .await .unwrap(); Ok(max_popcon_name) } /// Tie-breaker that selects dependencies based on popcon popularity. /// /// This tie-breaker uses the Debian Popularity Contest (popcon) statistics /// to determine which package is most commonly installed among Debian users. pub struct PopconTieBreaker; impl TieBreaker for PopconTieBreaker { fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { // TODO(jelmer): Pick package based on what appears most commonly in // build-depends{-indep,-arch} let rt = Runtime::new().unwrap(); let package = rt.block_on(get_most_popular(reqs)).unwrap(); if package.is_none() { log::info!("No relevant popcon information found, not ranking by popcon"); return None; } let package = package.unwrap(); let winner = reqs .into_iter() .find(|req| req.package_names().contains(&package.to_string())); if winner.is_none() { log::info!("No relevant popcon information found, not ranking by popcon"); } winner.copied() } } ognibuild-0.2.6/src/debian/upstream_deps.rs000064400000000000000000000066221046102023000170450ustar 00000000000000use crate::buildsystem::{BuildSystem, DependencyCategory}; use crate::dependencies::debian::DebianDependency; use crate::installer::Error as InstallerError; use crate::session::Session; /// Get the project-wide dependencies for a project. /// /// This function will return a tuple of two vectors of `DebianDependency` objects. The first /// vector will contain the build dependencies, and the second vector will contain the test /// dependencies. /// /// # Arguments /// * `session` - The session to use for the operation. /// * `buildsystem` - The build system to use for the operation. pub fn get_project_wide_deps( session: &dyn Session, buildsystem: &dyn BuildSystem, ) -> (Vec, Vec) { let mut build_deps = vec![]; let mut test_deps = vec![]; let apt = crate::debian::apt::AptManager::new(session, None); let apt_installer = crate::debian::apt::AptInstaller::new(apt); let scope = crate::installer::InstallationScope::Global; let build_fixers = [ Box::new(crate::fixers::InstallFixer::new(&apt_installer, scope)) as Box>, ]; let apt = crate::debian::apt::AptManager::new(session, None); // Try to create build dependency tie breaker, but handle failure gracefully let mut tie_breakers = vec![]; match crate::debian::build_deps::BuildDependencyTieBreaker::try_from_session(session) { Ok(tie_breaker) => { tie_breakers .push(Box::new(tie_breaker) as Box); } Err(e) => { log::warn!( "Failed to create BuildDependencyTieBreaker: {}. Using basic dependency resolution.", e ); } } #[cfg(feature = "udd")] { tie_breakers.push(Box::new(crate::debian::udd::PopconTieBreaker) as Box); } match buildsystem.get_declared_dependencies( session, Some( build_fixers .iter() .map(|x| x.as_ref()) .collect::>() .as_slice(), ), ) { Err(e) => { log::error!("Unable to obtain declared dependencies: {}", e); } Ok(upstream_deps) => { for (kind, dep) in upstream_deps { let apt_dep = crate::debian::apt::dependency_to_deb_dependency( &apt, dep.as_ref(), tie_breakers.as_slice(), ) .unwrap(); if apt_dep.is_none() { log::warn!( "Unable to map upstream requirement {:?} (kind {}) to a Debian package", dep, kind, ); continue; } let apt_dep = apt_dep.unwrap(); log::debug!("Mapped {:?} (kind: {}) to {:?}", dep, kind, apt_dep); if [DependencyCategory::Universal, DependencyCategory::Build].contains(&kind) { build_deps.push(apt_dep.clone()); } if [DependencyCategory::Universal, DependencyCategory::Test].contains(&kind) { test_deps.push(apt_dep.clone()); } } } } (build_deps, test_deps) } ognibuild-0.2.6/src/dependencies/autoconf.rs000064400000000000000000000147341046102023000172170ustar 00000000000000use crate::dependency::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[cfg(feature = "debian")] use std::io::BufRead; /// Dependency on an Autoconf macro. /// /// This represents a dependency on a specific Autoconf macro that can be /// used in configure.ac files. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoconfMacroDependency { /// Name of the Autoconf macro macro_name: String, } impl AutoconfMacroDependency { /// Create a new AutoconfMacroDependency. /// /// # Arguments /// * `macro_name` - Name of the Autoconf macro /// /// # Returns /// A new AutoconfMacroDependency instance pub fn new(macro_name: &str) -> Self { Self { macro_name: macro_name.to_string(), } } } impl Dependency for AutoconfMacroDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "autoconf-macro" fn family(&self) -> &'static str { "autoconf-macro" } /// Checks if the Autoconf macro is present in the system. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn present(&self, _session: &dyn Session) -> bool { todo!() } /// Checks if the Autoconf macro is present in the project. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn project_present(&self, _session: &dyn Session) -> bool { todo!() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } /// Create a regular expression to find a macro definition in M4 files. /// /// This function generates a regex pattern that can match various ways a macro /// might be defined in M4 files (via AC_DEFUN, AU_ALIAS, or m4_copy). /// /// # Arguments /// * `macro` - Name of the Autoconf macro to search for /// /// # Returns /// Regular expression string pattern for finding the macro definition #[cfg(any(feature = "debian", test))] fn m4_macro_regex(r#macro: &str) -> String { let defun_prefix = regex::escape(format!("AC_DEFUN([{}],", r#macro).as_str()); let au_alias_prefix = regex::escape(format!("AU_ALIAS([{}],", r#macro).as_str()); let m4_copy = format!(r"m4_copy\(.*,\s*\[{}\]\)", regex::escape(r#macro)); [ "(", &defun_prefix, "|", &au_alias_prefix, "|", &m4_copy, ")", ] .concat() } #[cfg(feature = "debian")] /// Find a local M4 macro file that contains the definition of a given macro. /// /// Searches in `/usr/share/aclocal` for files containing the definition /// of the specified macro. /// /// # Arguments /// * `macro` - Name of the Autoconf macro to search for /// /// # Returns /// Path to the M4 file containing the macro definition, or None if not found fn find_local_m4_macro(r#macro: &str) -> Option { // TODO(jelmer): Query some external service that can search all binary packages? let p = regex::Regex::new(&m4_macro_regex(r#macro)).unwrap(); for entry in std::fs::read_dir("/usr/share/aclocal").unwrap() { let entry = entry.unwrap(); if !entry.metadata().unwrap().is_file() { continue; } let f = std::fs::File::open(entry.path()).unwrap(); let reader = std::io::BufReader::new(f); for line in reader.lines() { if p.find(line.unwrap().as_str()).is_some() { return Some(entry.path().to_str().unwrap().to_string()); } } } None } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for AutoconfMacroDependency { /// Convert this dependency to a list of Debian package dependencies. /// /// Attempts to find the Debian packages that provide the Autoconf macro by /// searching for M4 files in standard locations. /// /// # Arguments /// * `apt` - The APT package manager to use for queries /// /// # Returns /// A list of Debian package dependencies if found, or None if not found fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = find_local_m4_macro(&self.macro_name); if path.is_none() { log::info!("No local m4 file found defining {}", self.macro_name); return None; } Some( apt.get_packages_for_paths(vec![path.as_ref().unwrap()], false, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingAutoconfMacro { /// Convert a MissingAutoconfMacro problem to a Dependency. /// /// # Returns /// An AutoconfMacroDependency boxed as a Dependency trait object fn to_dependency(&self) -> Option> { Some(Box::new(AutoconfMacroDependency::new(&self.r#macro))) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_autoconf_macro_dependency_new() { let dependency = AutoconfMacroDependency::new("PKG_CHECK_MODULES"); assert_eq!(dependency.macro_name, "PKG_CHECK_MODULES"); } #[test] fn test_autoconf_macro_dependency_family() { let dependency = AutoconfMacroDependency::new("PKG_CHECK_MODULES"); assert_eq!(dependency.family(), "autoconf-macro"); } #[test] fn test_m4_macro_regex() { let regex = m4_macro_regex("PKG_CHECK_MODULES"); // Test AC_DEFUN matching assert!(regex::Regex::new(®ex) .unwrap() .is_match("AC_DEFUN([PKG_CHECK_MODULES],")); // Test AU_ALIAS matching assert!(regex::Regex::new(®ex) .unwrap() .is_match("AU_ALIAS([PKG_CHECK_MODULES],")); // Test m4_copy matching assert!(regex::Regex::new(®ex) .unwrap() .is_match("m4_copy([SOME_MACRO], [PKG_CHECK_MODULES])")); // Test negative case assert!(!regex::Regex::new(®ex) .unwrap() .is_match("PKG_CHECK_MODULES")); } } ognibuild-0.2.6/src/dependencies/debian.rs000064400000000000000000000340321046102023000166140ustar 00000000000000use crate::dependency::Dependency; use crate::session::Session; use debian_control::lossless::relations::{Entry, Relation, Relations}; use debian_control::relations::VersionConstraint; use debversion::Version; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashSet; use std::hash::Hash; /// Represents a Debian dependency. pub struct DebianDependency(Relations); impl std::fmt::Debug for DebianDependency { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("DebianDependency") .field(&self.0.to_string()) .finish() } } impl Clone for DebianDependency { fn clone(&self) -> Self { let rels = self.0.to_string().parse().unwrap(); DebianDependency(rels) } } impl Serialize for DebianDependency { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.0.to_string().serialize(serializer) } } impl<'a> Deserialize<'a> for DebianDependency { fn deserialize(deserializer: D) -> Result where D: Deserializer<'a>, { let s = String::deserialize(deserializer)?; Ok(DebianDependency(s.parse().unwrap())) } } impl PartialEq for DebianDependency { fn eq(&self, other: &Self) -> bool { self.0.to_string() == other.0.to_string() } } impl Eq for DebianDependency {} impl Hash for DebianDependency { fn hash(&self, state: &mut H) { self.0.to_string().hash(state); } } impl DebianDependency { /// Create a new dependency from a package name. pub fn new(name: &str) -> DebianDependency { DebianDependency( name.parse() .unwrap_or_else(|_| panic!("Failed to parse dependency: {}", name)), ) } /// Iterate over the entries in the dependency. pub fn iter(&self) -> impl Iterator + '_ { self.0.entries() } /// Get the relations of the dependency. pub fn relation_string(&self) -> String { self.0.to_string() } /// Create a new dependency from a package name with a specific version. pub fn simple(name: &str) -> DebianDependency { Self::new(name) } /// Check if the dependency is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Create a new dependency with a minimum version. pub fn new_with_min_version(name: &str, min_version: &Version) -> DebianDependency { DebianDependency( format!("{} (>= {})", name, min_version) .parse() .unwrap_or_else(|_| { panic!("Failed to parse dependency: {} (>= {})", name, min_version) }), ) } /// Check if the dependency touches a specific package. pub fn touches_package(&self, package: &str) -> bool { for entry in self.0.entries() { for relation in entry.relations() { if relation.name() == package { return true; } } } false } /// Get the package names from the dependency. pub fn package_names(&self) -> HashSet { let mut names = HashSet::new(); for entry in self.0.entries() { for relation in entry.relations() { names.insert(relation.name()); } } names } /// Check if the dependency is satisfied by the given versions. pub fn satisfied_by( &self, versions: &std::collections::HashMap, ) -> bool { let relation_satisfied = |relation: Relation| -> bool { let name = relation.name(); let version = if let Some(version) = versions.get(&name) { version } else { return false; }; match relation.version() { Some((VersionConstraint::Equal, v)) => version.cmp(&v) == std::cmp::Ordering::Equal, Some((VersionConstraint::GreaterThanEqual, v)) => version >= &v, Some((VersionConstraint::GreaterThan, v)) => version > &v, Some((VersionConstraint::LessThanEqual, v)) => version <= &v, Some((VersionConstraint::LessThan, v)) => version < &v, None => true, } }; self.0 .entries() .all(|entry| entry.relations().any(relation_satisfied)) } } /// Get the version of a package installed on the system. /// /// Returns `None` if the package is not installed. fn get_package_version(session: &dyn Session, package: &str) -> Option { let argv = vec!["dpkg-query", "-W", "-f=${Version}\n", package]; let output = session .command(argv) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() .unwrap(); match output.status.code() { Some(0) => { let output = String::from_utf8(output.stdout).unwrap(); if output.trim().is_empty() { return None; } Some(output.trim().parse().unwrap()) } Some(1) => None, _ => panic!("Failed to run dpkg-query"), } } impl Dependency for DebianDependency { fn family(&self) -> &'static str { "debian" } fn present(&self, session: &dyn Session) -> bool { use std::collections::HashMap; let mut versions = HashMap::new(); for name in self.package_names() { if let Some(version) = get_package_version(session, &name) { versions.insert(name, version); } else { // Package not found return false; } } let result = self.satisfied_by(&versions); if !result { log::debug!("Dependency not satisfied: {:?}", self); } else { log::debug!("Dependency satisfied: {:?}", self); } result } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } impl From for Relations { fn from(dep: DebianDependency) -> Self { dep.0 } } impl From for DebianDependency { fn from(rel: Relations) -> Self { DebianDependency(rel) } } /// Trait for breaking ties between multiple dependencies. pub trait TieBreaker { /// Break ties between multiple dependencies. fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency>; } /// Default tie breakers for Debian dependencies. pub fn default_tie_breakers(session: &dyn Session) -> Vec> { let mut tie_breakers: Vec> = Vec::new(); use crate::debian::build_deps::BuildDependencyTieBreaker; match BuildDependencyTieBreaker::try_from_session(session) { Ok(tie_breaker) => { tie_breakers.push(Box::new(tie_breaker)); } Err(e) => { log::warn!( "Failed to create BuildDependencyTieBreaker: {}. Continuing without it.", e ); } } #[cfg(feature = "udd")] { use crate::debian::udd::PopconTieBreaker; tie_breakers.push(Box::new(PopconTieBreaker)); } tie_breakers } /// Trait for converting a dependency into a DebianDependency. pub trait IntoDebianDependency: Dependency { /// Convert a dependency into a DebianDependency. fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option>; } impl IntoDebianDependency for DebianDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> Option> { Some(vec![self.clone()]) } } /// Trait for converting a DebianDependency into an upstream dependency. pub trait FromDebianDependency { /// Convert a DebianDependency into an upstream dependency. fn from_debian_dependency(dependency: &DebianDependency) -> Option>; } /// Extract an upstream dependency from a DebianDependency. pub fn extract_upstream_dependency(dep: &DebianDependency) -> Option> { crate::dependencies::RubyGemDependency::from_debian_dependency(dep) .or_else(|| { crate::dependencies::python::PythonPackageDependency::from_debian_dependency(dep) }) .or_else(|| crate::dependencies::RubyGemDependency::from_debian_dependency(dep)) .or_else(|| crate::dependencies::CargoCrateDependency::from_debian_dependency(dep)) .or_else(|| crate::dependencies::python::PythonDependency::from_debian_dependency(dep)) } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for DebianDependency { fn find_upstream(&self) -> Option { let upstream_dep = extract_upstream_dependency(self)?; crate::upstream::find_upstream(upstream_dep.as_ref()) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::debian::UnsatisfiedAptDependencies { fn to_dependency(&self) -> Option> { Some(Box::new(DebianDependency::new(&self.0))) } } /// Extract the package name and exact version from a dependency. pub fn extract_simple_exact_version( dep: &DebianDependency, ) -> Option<(String, Option)> { // Extract the package name and exact version from a dependency. Return None // if there are non-1 entries in the dependency, or non-1 relations in the entry or if the // version constraint is not Equal. let mut entries = dep.0.entries(); let first_entry = entries.next()?; if entries.next().is_some() { return None; } let mut relations = first_entry.relations(); let first_relation = relations.next()?; if relations.next().is_some() { return None; } let name = first_relation.name(); let version = match first_relation.version() { Some((VersionConstraint::Equal, v)) => Some(v), None => None, _ => return None, }; Some((name.to_string(), version)) } /// Extract the package name and minimum version from a dependency. pub fn extract_simple_min_version( dep: &DebianDependency, ) -> Option<(String, Option)> { // Extract the package name and minimum version from a dependency. Return None // if there are non-1 entries in the dependency, or non-1 relations in the entry or if the // version constraint is not GreaterThanEqual or absent. let mut entries = dep.0.entries(); let first_entry = entries.next()?; if entries.next().is_some() { return None; } let mut relations = first_entry.relations(); let first_relation = relations.next()?; if relations.next().is_some() { return None; } let name = first_relation.name(); let version = match first_relation.version() { Some((VersionConstraint::GreaterThanEqual, v)) => Some(v), None => None, _ => return None, }; Some((name.to_string(), version)) } /// Check if a string is a valid Debian package name. pub fn valid_debian_package_name(name: &str) -> bool { lazy_regex::regex_is_match!("[a-z0-9][a-z0-9+-\\.]+", name) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] /// Represents the category of a Debian dependency. pub enum DebianDependencyCategory { /// A runtime dependency. Runtime, /// A build dependency. Build, /// A runtime dependency that is also a build dependency. Install, /// A test dependency. Test(String), } #[cfg(test)] mod tests { use super::*; use maplit::hashset; #[test] fn test_valid_debian_package_name() { assert!(valid_debian_package_name("libssl-dev")); assert!(valid_debian_package_name("libssl1.1")); assert!(valid_debian_package_name("libssl1.1-dev")); assert!(valid_debian_package_name("libssl1.1-dev~foo")); } #[test] fn test_touches_package() { let dep = DebianDependency::new("libssl-dev"); assert!(dep.touches_package("libssl-dev")); assert!(!dep.touches_package("libssl1.1")); } #[test] fn test_package_names() { let dep = DebianDependency::new("libssl-dev"); assert_eq!(dep.package_names(), hashset! {"libssl-dev".to_string()}); } #[test] fn test_package_names_multiple() { let dep = DebianDependency::new("libssl-dev, libssl1.1"); assert_eq!( dep.package_names(), hashset! {"libssl-dev".to_string(), "libssl1.1".to_string()} ); } #[test] fn test_package_names_multiple_with_version() { let dep = DebianDependency::new("libssl-dev (>= 1.1), libssl1.1 (>= 1.1)"); assert_eq!( dep.package_names(), hashset! {"libssl-dev".to_string(), "libssl1.1".to_string()} ); } #[test] fn test_satisfied_by() { let dep = DebianDependency::new("libssl-dev (>= 1.1), libssl1.1 (>= 1.1)"); let mut versions = std::collections::HashMap::new(); versions.insert("libssl-dev".to_string(), "1.2".parse().unwrap()); versions.insert("libssl1.1".to_string(), "1.2".parse().unwrap()); assert!(dep.satisfied_by(&versions)); } #[test] fn test_satisfied_by_missing_package() { let dep = DebianDependency::new("libssl-dev (>= 1.1), libssl1.1 (>= 1.1)"); let mut versions = std::collections::HashMap::new(); versions.insert("libssl-dev".to_string(), "1.2".parse().unwrap()); assert!(!dep.satisfied_by(&versions)); } #[test] fn test_satisfied_by_missing_version() { let dep = DebianDependency::new("libssl-dev (>= 1.1), libssl1.1 (>= 1.1)"); let mut versions = std::collections::HashMap::new(); versions.insert("libssl-dev".to_string(), "1.2".parse().unwrap()); versions.insert("libssl1.1".to_string(), "1.0".parse().unwrap()); assert!(!dep.satisfied_by(&versions)); } } ognibuild-0.2.6/src/dependencies/go.rs000064400000000000000000000235211046102023000160000ustar 00000000000000use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// Represents a Go package dependency. pub struct GoPackageDependency { /// The name of the Go package. pub package: String, /// The version of the Go package, if specified. pub version: Option, } impl GoPackageDependency { /// Creates a new `GoPackageDependency` instance. pub fn new(package: &str, version: Option<&str>) -> Self { Self { package: package.to_string(), version: version.map(|s| s.to_string()), } } /// Creates a simple `GoPackageDependency` instance without a version. pub fn simple(package: &str) -> Self { Self { package: package.to_string(), version: None, } } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; use std::any::Any; #[test] fn test_go_package_dependency_new() { let dependency = GoPackageDependency::new("github.com/pkg/errors", Some("v0.9.1")); assert_eq!(dependency.package, "github.com/pkg/errors"); assert_eq!(dependency.version, Some("v0.9.1".to_string())); } #[test] fn test_go_package_dependency_simple() { let dependency = GoPackageDependency::simple("github.com/pkg/errors"); assert_eq!(dependency.package, "github.com/pkg/errors"); assert_eq!(dependency.version, None); } #[test] fn test_go_package_dependency_family() { let dependency = GoPackageDependency::simple("github.com/pkg/errors"); assert_eq!(dependency.family(), "go-package"); } #[test] fn test_go_package_dependency_as_any() { let dependency = GoPackageDependency::simple("github.com/pkg/errors"); let any_dep: &dyn Any = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_go_package_to_dependency() { let problem = buildlog_consultant::problems::common::MissingGoPackage { package: "github.com/pkg/errors".to_string(), }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "go-package"); let go_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(go_dep.package, "github.com/pkg/errors"); } #[test] fn test_go_dependency_new() { let dependency = GoDependency::new(Some("1.16")); assert_eq!(dependency.version, Some("1.16".to_string())); } #[test] fn test_go_dependency_family() { let dependency = GoDependency::new(None); assert_eq!(dependency.family(), "go"); } } impl Dependency for GoPackageDependency { fn family(&self) -> &'static str { "go-package" } fn present(&self, _session: &dyn Session) -> bool { unimplemented!() } fn project_present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["go".to_string(), "list".to_string(), "-f".to_string()]; if let Some(version) = &self.version { cmd.push(format!("{{.Version}} == {}", version)); } else { cmd.push("{{.Version}}".to_string()); } cmd.push(self.package.clone()); session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for GoPackageDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { use std::path::Path; let names = apt .get_packages_for_paths( vec![Path::new("/usr/share/gocode/src") .join(regex::escape(&self.package)) .join(".*") .to_str() .unwrap()], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|name| crate::dependencies::debian::DebianDependency::new(name)) .collect(), ) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::FromDebianDependency for GoPackageDependency { fn from_debian_dependency( dependency: &super::debian::DebianDependency, ) -> Option> { let (package, version) = crate::dependencies::debian::extract_simple_exact_version(&dependency)?; let (_, package) = lazy_regex::regex_captures!(r"golang-(.*)-dev", &package)?; let mut parts = package.split('-').collect::>(); if parts[0] == "github" { parts[1] = "github.com"; } if parts[0] == "gopkg" { parts[1] = "gopkg.in"; } Some(Box::new(GoPackageDependency::new( &parts.join("/"), version.map(|s| s.to_string()).as_deref(), ))) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingGoPackage { fn to_dependency(&self) -> Option> { Some(Box::new(GoPackageDependency::simple(&self.package))) } } #[derive(Debug, Clone, Serialize, Deserialize)] /// Represents a Go dependency. pub struct GoDependency { /// The version of the Go dependency, if specified. pub version: Option, } impl GoDependency { /// Creates a new `GoDependency` instance. pub fn new(version: Option<&str>) -> Self { Self { version: version.map(|s| s.to_string()), } } } impl Dependency for GoDependency { fn family(&self) -> &'static str { "go" } fn present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["go".to_string(), "version".to_string()]; if let Some(version) = &self.version { cmd.push(format!(">={}", version)); } session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { unimplemented!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for GoPackageDependency { fn find_upstream(&self) -> Option { upstream_ontologist::providers::go::remote_go_metadata(&self.package).ok() } } /// A resolver for Go package dependencies. pub struct GoResolver<'a> { session: &'a dyn Session, } impl<'a> GoResolver<'a> { /// Creates a new `GoResolver` instance. pub fn new(session: &'a dyn Session) -> Self { Self { session } } fn cmd(&self, reqs: &[&GoPackageDependency]) -> Vec { let mut cmd = vec!["go".to_string(), "get".to_string()]; for req in reqs { cmd.push(req.package.clone()); } cmd } } impl<'a> Installer for GoResolver<'a> { fn explain( &self, requirement: &dyn Dependency, _scope: InstallationScope, ) -> Result { let req = requirement .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; Ok(Explanation { message: format!("Install go package {}", req.package), command: Some(self.cmd(&[req])), }) } fn install(&self, requirement: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let req = requirement .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let cmd = self.cmd(&[req]); let (env, user) = match scope { InstallationScope::User => (std::collections::HashMap::new(), None), InstallationScope::Global => { // TODO(jelmer): Isn't this Debian-specific? ( std::collections::HashMap::from([( "GOPATH".to_string(), "/usr/share/gocode".to_string(), )]), Some("root"), ) } InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } }; let mut cmd = self .session .command(cmd.iter().map(|s| s.as_str()).collect()) .env(env); if let Some(user) = user { cmd = cmd.user(user); } cmd.run_detecting_problems()?; Ok(()) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for GoDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { if let Some(version) = &self.version { Some(vec![ crate::dependencies::debian::DebianDependency::new_with_min_version( "golang-go", &version.parse().unwrap(), ), ]) } else { Some(vec![crate::dependencies::debian::DebianDependency::new( "golang-go", )]) } } } ognibuild-0.2.6/src/dependencies/haskell.rs000064400000000000000000000174761046102023000170320ustar 00000000000000use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Haskell package pub struct HaskellPackageDependency { package: String, specs: Option>, } impl HaskellPackageDependency { /// Creates a new HaskellPackageDependency pub fn new(package: &str, specs: Option>) -> Self { Self { package: package.to_string(), specs: specs.map(|v| v.iter().map(|s| s.to_string()).collect()), } } /// Creates a new HaskellPackageDependency with no specs pub fn simple(package: &str) -> Self { Self { package: package.to_string(), specs: None, } } } impl std::str::FromStr for HaskellPackageDependency { type Err = String; fn from_str(s: &str) -> Result { let mut parts = s.splitn(2, ' '); let package = parts.next().ok_or("missing package name")?.to_string(); let specs = parts.next().map(|s| s.split(' ').collect()); Ok(Self::new(&package, specs)) } } fn ghc_pkg_list(session: &dyn Session) -> Vec<(String, String)> { let output = session .command(vec!["ghc-pkg", "list"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() .unwrap(); let output = String::from_utf8(output.stdout).unwrap(); output .lines() .filter_map(|line| { if let Some((name, version)) = line.strip_prefix(" ").and_then(|s| s.rsplit_once('-')) { Some((name.to_string(), version.to_string())) } else { None } }) .collect() } impl Dependency for HaskellPackageDependency { fn family(&self) -> &'static str { "haskell-package" } fn present(&self, session: &dyn Session) -> bool { // TODO: Check version ghc_pkg_list(session) .iter() .any(|(name, _version)| name == &self.package) } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for HaskellPackageDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { let path = format!( "/var/lib/ghc/package\\.conf\\.d/{}\\-.*\\.conf", regex::escape(&self.package) ); let names = apt .get_packages_for_paths(vec![path.as_str()], true, false) .unwrap(); if names.is_empty() { None } else { Some( names .into_iter() .map(|name| super::debian::DebianDependency::new(&name)) .collect(), ) } } } /// A resolver for Haskell packages using the `cabal` command pub struct HackageResolver<'a> { session: &'a dyn Session, } impl<'a> HackageResolver<'a> { /// Creates a new HackageResolver pub fn new(session: &'a dyn Session) -> Self { Self { session } } fn cmd( &self, reqs: &[&HaskellPackageDependency], scope: InstallationScope, ) -> Result, Error> { let mut cmd = vec!["cabal".to_string(), "install".to_string()]; match scope { InstallationScope::User => { cmd.push("--user".to_string()); } InstallationScope::Global => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } cmd.extend(reqs.iter().map(|req| req.package.clone())); Ok(cmd) } } impl<'a> Installer for HackageResolver<'a> { fn install(&self, requirement: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let requirement = requirement .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let user = if scope != InstallationScope::Global { None } else { Some("root") }; let cmd = self.cmd(&[requirement], scope)?; log::info!("Hackage: running {:?}", cmd); let mut cmd = self .session .command(cmd.iter().map(|x| x.as_str()).collect()); if let Some(user) = user { cmd = cmd.user(user); } cmd.run_detecting_problems()?; Ok(()) } fn explain( &self, requirement: &dyn Dependency, scope: InstallationScope, ) -> Result { if let Some(requirement) = requirement .as_any() .downcast_ref::() { let cmd = self.cmd(&[requirement], scope)?; Ok(Explanation { message: format!("Install Haskell package {}", requirement.package), command: Some(cmd), }) } else { Err(Error::UnknownDependencyFamily) } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingHaskellDependencies { fn to_dependency(&self) -> Option> { let d: HaskellPackageDependency = self.0[0].parse().unwrap(); Some(Box::new(d)) } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for HaskellPackageDependency { fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::haskell::remote_hackage_data(&self.package)) .ok() } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; use std::str::FromStr; #[test] fn test_haskell_package_dependency_new() { let dependency = HaskellPackageDependency::new("parsec", Some(vec![">=3.1.11"])); assert_eq!(dependency.package, "parsec"); assert_eq!(dependency.specs, Some(vec![">=3.1.11".to_string()])); } #[test] fn test_haskell_package_dependency_simple() { let dependency = HaskellPackageDependency::simple("parsec"); assert_eq!(dependency.package, "parsec"); assert_eq!(dependency.specs, None); } #[test] fn test_haskell_package_dependency_family() { let dependency = HaskellPackageDependency::simple("parsec"); assert_eq!(dependency.family(), "haskell-package"); } #[test] fn test_haskell_package_dependency_as_any() { let dependency = HaskellPackageDependency::simple("parsec"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_haskell_package_dependency_from_str() { let dependency = HaskellPackageDependency::from_str("parsec >=3.1.11").unwrap(); assert_eq!(dependency.package, "parsec"); assert_eq!(dependency.specs, Some(vec![">=3.1.11".to_string()])); } #[test] fn test_missing_haskell_dependencies_to_dependency() { let problem = buildlog_consultant::problems::common::MissingHaskellDependencies(vec![ "parsec".to_string(), ]); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "haskell-package"); let haskell_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(haskell_dep.package, "parsec"); } } ognibuild-0.2.6/src/dependencies/java.rs000064400000000000000000000265231046102023000163210ustar 00000000000000use crate::dependency::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency representing a Java class. pub struct JavaClassDependency { classname: String, } impl JavaClassDependency { /// Creates a new JavaClassDependency with the given class name. pub fn new(classname: &str) -> Self { Self { classname: classname.to_string(), } } } impl Dependency for JavaClassDependency { fn family(&self) -> &'static str { "java-class" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for JavaClassDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { apt.satisfy(vec![crate::debian::apt::SatisfyEntry::Required( "java-propose-classpath".to_string(), )]) .unwrap(); let output = String::from_utf8( apt.session() .command(vec![ "java-propose-classpath", &format!("-c{}", &self.classname), ]) .check_output() .unwrap(), ) .unwrap(); let classpath = output .trim_matches(':') .trim() .split(':') .collect::>(); if classpath.is_empty() { None } else { Some( classpath .iter() .map(|path| crate::dependencies::debian::DebianDependency::new(path)) .collect(), ) } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingJavaClass { fn to_dependency(&self) -> Option> { Some(Box::new(JavaClassDependency::new(&self.classname))) } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency representing the Java Development Kit (JDK). pub struct JDKDependency; impl Dependency for JDKDependency { fn family(&self) -> &'static str { "jdk" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["javac", "-version"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for JDKDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( "default-jdk", )]) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingJDK { fn to_dependency(&self) -> Option> { Some(Box::new(JDKDependency)) } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency representing the Java Runtime Environment (JRE). pub struct JREDependency; impl Dependency for JREDependency { fn family(&self) -> &'static str { "jre" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["java", "-version"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for JREDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( "default-jre", )]) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingJRE { fn to_dependency(&self) -> Option> { Some(Box::new(JREDependency)) } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency representing a specific file in the JDK. pub struct JDKFileDependency { jdk_path: std::path::PathBuf, filename: String, } impl JDKFileDependency { /// Creates a new JDKFileDependency with the given JDK path and filename. pub fn new(jdk_path: &str, filename: &str) -> Self { Self { jdk_path: std::path::PathBuf::from(jdk_path.to_string()), filename: filename.to_string(), } } /// Returns the full path to the JDK file. pub fn path(&self) -> std::path::PathBuf { self.jdk_path.join(&self.filename) } } impl Dependency for JDKFileDependency { fn family(&self) -> &'static str { "jdk-file" } fn present(&self, _session: &dyn Session) -> bool { self.path().exists() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for JDKFileDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = regex::escape(self.jdk_path.to_str().unwrap()) + ".*/" + ®ex::escape(self.filename.as_str()); let names = apt .get_packages_for_paths(vec![path.as_str()], true, false) .unwrap(); if names.is_empty() { None } else { Some( names .iter() .map(|name| crate::dependencies::debian::DebianDependency::simple(name)) .collect(), ) } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingJDKFile { fn to_dependency(&self) -> Option> { Some(Box::new(JDKFileDependency::new( &self.jdk_path, &self.filename, ))) } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; #[test] fn test_java_class_dependency_new() { let dependency = JavaClassDependency::new("org.apache.commons.lang3.StringUtils"); assert_eq!(dependency.classname, "org.apache.commons.lang3.StringUtils"); } #[test] fn test_java_class_dependency_family() { let dependency = JavaClassDependency::new("org.apache.commons.lang3.StringUtils"); assert_eq!(dependency.family(), "java-class"); } #[test] fn test_java_class_dependency_as_any() { let dependency = JavaClassDependency::new("org.apache.commons.lang3.StringUtils"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_java_class_to_dependency() { let problem = buildlog_consultant::problems::common::MissingJavaClass { classname: "org.apache.commons.lang3.StringUtils".to_string(), }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "java-class"); let java_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(java_dep.classname, "org.apache.commons.lang3.StringUtils"); } #[test] fn test_jdk_dependency_family() { let dependency = JDKDependency; assert_eq!(dependency.family(), "jdk"); } #[test] fn test_jdk_dependency_as_any() { let dependency = JDKDependency; let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_jdk_to_dependency() { let problem = buildlog_consultant::problems::common::MissingJDK { jdk_path: "/usr/lib/jvm/default-java".to_string(), }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "jdk"); assert!(dep.as_any().downcast_ref::().is_some()); } #[test] fn test_jre_dependency_family() { let dependency = JREDependency; assert_eq!(dependency.family(), "jre"); } #[test] fn test_jre_dependency_as_any() { let dependency = JREDependency; let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_jre_to_dependency() { let problem = buildlog_consultant::problems::common::MissingJRE; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "jre"); assert!(dep.as_any().downcast_ref::().is_some()); } #[test] fn test_jdk_file_dependency_new() { let dependency = JDKFileDependency::new("/usr/lib/jvm/default-java", "javac"); assert_eq!( dependency.jdk_path, std::path::PathBuf::from("/usr/lib/jvm/default-java") ); assert_eq!(dependency.filename, "javac"); } #[test] fn test_jdk_file_dependency_path() { let dependency = JDKFileDependency::new("/usr/lib/jvm/default-java", "javac"); assert_eq!( dependency.path(), std::path::PathBuf::from("/usr/lib/jvm/default-java/javac") ); } #[test] fn test_jdk_file_dependency_family() { let dependency = JDKFileDependency::new("/usr/lib/jvm/default-java", "javac"); assert_eq!(dependency.family(), "jdk-file"); } #[test] fn test_jdk_file_dependency_as_any() { let dependency = JDKFileDependency::new("/usr/lib/jvm/default-java", "javac"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_jdk_file_to_dependency() { let problem = buildlog_consultant::problems::common::MissingJDKFile { jdk_path: "/usr/lib/jvm/default-java".to_string(), filename: "javac".to_string(), }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "jdk-file"); let jdk_file_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!( jdk_file_dep.jdk_path, std::path::PathBuf::from("/usr/lib/jvm/default-java") ); assert_eq!(jdk_file_dep.filename, "javac"); } } ognibuild-0.2.6/src/dependencies/latex.rs000064400000000000000000000135261046102023000165140ustar 00000000000000use crate::analyze::AnalyzedError; use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a LaTeX package pub struct LatexPackageDependency { /// The name of the LaTeX package pub package: String, } impl LatexPackageDependency { /// Creates a new `LatexPackageDependency` instance pub fn new(package: &str) -> Self { Self { package: package.to_string(), } } } impl Dependency for LatexPackageDependency { fn family(&self) -> &'static str { "latex-package" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingLatexFile { fn to_dependency(&self) -> Option> { if let Some(filename) = self.0.strip_suffix(".sty") { Some(Box::new(LatexPackageDependency::new(filename))) } else { None } } } /// A resolver for LaTeX package dependencies using tlmgr pub struct TlmgrResolver<'a> { session: &'a dyn Session, repository: String, } impl<'a> TlmgrResolver<'a> { /// Creates a new `TlmgrResolver` instance pub fn new(session: &'a dyn Session, repository: &str) -> Self { Self { session, repository: repository.to_string(), } } fn cmd( &self, reqs: &[&LatexPackageDependency], scope: InstallationScope, ) -> Result, Error> { let mut ret = vec![ "tlmgr".to_string(), format!("--repository={}", self.repository), "install".to_string(), ]; match scope { InstallationScope::User => { ret.push("--usermode".to_string()); } InstallationScope::Global => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } ret.extend(reqs.iter().map(|req| req.package.clone())); Ok(ret) } } impl<'a> Installer for TlmgrResolver<'a> { fn explain( &self, dep: &dyn Dependency, scope: InstallationScope, ) -> Result { let dep = dep .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let cmd = self.cmd(&[dep], scope)?; Ok(Explanation { message: format!("Install the LaTeX package {}", dep.package), command: Some(cmd), }) } fn install(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let dep = dep .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let cmd = self.cmd(&[dep], scope)?; log::info!("tlmgr: running {:?}", cmd); match self .session .command(cmd.iter().map(|x| x.as_str()).collect()) .run_detecting_problems() { Ok(_) => Ok(()), Err(AnalyzedError::Unidentified { lines, retcode, secondary, }) => { if lines.contains( &"tlmgr: user mode not initialized, please read the documentation!".to_string(), ) { self.session .command(vec!["tlmgr", "init-usertree"]) .check_call()?; Ok(()) } else { Err(Error::AnalyzedError(AnalyzedError::Unidentified { retcode, lines, secondary, })) } } Err(e) => Err(e.into()), } } } /// Creates a new `TlmgrResolver` instance for the CTAN repository pub fn ctan<'a>(session: &'a dyn Session) -> TlmgrResolver<'a> { TlmgrResolver::new(session, "ctan") } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; use std::any::Any; #[test] fn test_latex_package_dependency_new() { let dependency = LatexPackageDependency::new("graphicx"); assert_eq!(dependency.package, "graphicx"); } #[test] fn test_latex_package_dependency_family() { let dependency = LatexPackageDependency::new("graphicx"); assert_eq!(dependency.family(), "latex-package"); } #[test] fn test_latex_package_dependency_as_any() { let dependency = LatexPackageDependency::new("graphicx"); let any_dep: &dyn Any = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_latex_file_to_dependency() { let problem = buildlog_consultant::problems::common::MissingLatexFile("graphicx.sty".to_string()); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "latex-package"); let latex_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(latex_dep.package, "graphicx"); } #[test] fn test_missing_latex_file_non_sty_to_dependency() { // Non .sty files should return None let problem = buildlog_consultant::problems::common::MissingLatexFile("graphicx.cls".to_string()); let dependency = problem.to_dependency(); assert!(dependency.is_none()); } } ognibuild-0.2.6/src/dependencies/mod.rs000064400000000000000000002275771046102023000161730ustar 00000000000000use crate::buildlog::ToDependency; use crate::dependency::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// Dependency handling for autoconf-based projects. pub mod autoconf; #[cfg(feature = "debian")] /// Dependency handling for Debian packages. pub mod debian; /// Dependency handling for Go projects. pub mod go; /// Dependency handling for Haskell projects. pub mod haskell; /// Dependency handling for Java projects. pub mod java; /// Dependency handling for LaTeX projects. pub mod latex; /// Dependency handling for Node.js projects. pub mod node; /// Dependency handling for GNU Octave projects. pub mod octave; /// Dependency handling for Perl projects. pub mod perl; /// Dependency handling for PHP projects. pub mod php; /// Dependency handling for pytest-specific dependencies. pub mod pytest; /// Dependency handling for Python projects. pub mod python; /// Dependency handling for R projects. pub mod r; /// Dependency handling for vague or generic dependencies. pub mod vague; /// Dependency handling for XML-related requirements. pub mod xml; /// Dependency on a system binary or executable file. /// /// This represents a dependency on an executable binary command that /// must be available in the PATH. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BinaryDependency { /// Name of the binary executable binary_name: String, } impl BinaryDependency { /// Create a new BinaryDependency. /// /// # Arguments /// * `binary_name` - Name of the binary executable /// /// # Returns /// A new BinaryDependency instance pub fn new(binary_name: &str) -> Self { Self { binary_name: binary_name.to_string(), } } } impl Dependency for BinaryDependency { fn family(&self) -> &'static str { "binary" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["which", &self.binary_name]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } impl ToDependency for buildlog_consultant::problems::common::MissingCommand { fn to_dependency(&self) -> Option> { Some(Box::new(BinaryDependency::new(&self.0))) } } impl ToDependency for buildlog_consultant::problems::common::MissingCommandOrBuildFile { fn to_dependency(&self) -> Option> { Some(Box::new(BinaryDependency::new(&self.filename))) } } #[cfg(feature = "debian")] const BIN_PATHS: &[&str] = &["/usr/bin", "/bin"]; #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for BinaryDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = if std::path::Path::new(&self.binary_name).is_absolute() { vec![self.binary_name.clone()] } else { BIN_PATHS .iter() .map(|p| format!("{}/{}", p, self.binary_name)) .collect() }; // TODO(jelmer): Check for binaries which use alternatives Some( apt.get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), false, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } /// Dependency for accessing VCS control directories. /// /// This represents a dependency on access to VCS control directories /// like .git, .svn, etc., which might be needed by certain build processes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VcsControlDirectoryAccessDependency { /// List of affected version control systems (e.g., "git", "svn") pub vcs: Vec, } impl VcsControlDirectoryAccessDependency { /// Create a new VcsControlDirectoryAccessDependency. /// /// # Arguments /// * `vcs` - List of version control systems /// /// # Returns /// A new VcsControlDirectoryAccessDependency instance pub fn new(vcs: Vec<&str>) -> Self { Self { vcs: vcs.iter().map(|s| s.to_string()).collect(), } } } impl Dependency for VcsControlDirectoryAccessDependency { fn family(&self) -> &'static str { "vcs-access" } fn project_present(&self, session: &dyn Session) -> bool { self.vcs.iter().all(|vcs| match vcs.as_str() { "git" => session .command(vec!["git", "rev-parse", "--is-inside-work-tree"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success(), _ => todo!(), }) } fn present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for VcsControlDirectoryAccessDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let pkgs = self .vcs .iter() .filter_map(|vcs| match vcs.as_str() { "git" => Some("git"), "hg" => Some("mercurial"), "svn" => Some("subversion"), "bzr" => Some("bzr"), _ => { log::warn!("Unknown VCS {}", vcs); None } }) .collect::>(); let rels: Vec = pkgs.iter().map(|p| p.parse().unwrap()).collect(); Some( rels.into_iter() .map(|p| crate::dependencies::debian::DebianDependency::from(p)) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::VcsControlDirectoryNeeded { fn to_dependency(&self) -> Option> { Some(Box::new(VcsControlDirectoryAccessDependency::new( self.vcs.iter().map(|s| s.as_str()).collect(), ))) } } /// Dependency on a Lua module. /// /// This represents a dependency on a Lua module that can be loaded /// with the require() function. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LuaModuleDependency { /// Name of the Lua module module: String, } impl LuaModuleDependency { /// Create a new LuaModuleDependency. /// /// # Arguments /// * `module` - Name of the Lua module /// /// # Returns /// A new LuaModuleDependency instance pub fn new(module: &str) -> Self { Self { module: module.to_string(), } } } impl Dependency for LuaModuleDependency { fn family(&self) -> &'static str { "lua-module" } fn present(&self, session: &dyn Session) -> bool { // lua -e 'package_name = "socket"; status, _ = pcall(require, package_name); if status then os.exit(0) else os.exit(1) end' session .command(vec![ "lua", "-e", &format!( r#"package_name = "{}"; status, _ = pcall(require, package_name); if status then os.exit(0) else os.exit(1) end"#, self.module ), ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } impl ToDependency for buildlog_consultant::problems::common::MissingLuaModule { fn to_dependency(&self) -> Option> { Some(Box::new(LuaModuleDependency::new(&self.0))) } } /// Dependency on a Rust crate from crates.io. /// /// This represents a dependency on a Rust crate that can be fetched /// from the crates.io registry. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CargoCrateDependency { /// Name of the crate pub name: String, /// Optional list of required features pub features: Option>, /// Optional API version requirement pub api_version: Option, /// Optional minimum version requirement pub minimum_version: Option, } impl CargoCrateDependency { /// Create a new CargoCrateDependency with features and API version. /// /// # Arguments /// * `name` - Name of the crate /// * `features` - Optional list of required features /// * `api_version` - Optional API version requirement /// /// # Returns /// A new CargoCrateDependency instance pub fn new(name: &str, features: Option>, api_version: Option<&str>) -> Self { Self { name: name.to_string(), features: features.map(|v| v.iter().map(|s| s.to_string()).collect()), api_version: api_version.map(|s| s.to_string()), minimum_version: None, } } /// Create a new CargoCrateDependency with just a name. /// /// # Arguments /// * `name` - Name of the crate /// /// # Returns /// A new CargoCrateDependency instance without features or version requirements pub fn simple(name: &str) -> Self { Self { name: name.to_string(), features: None, api_version: None, minimum_version: None, } } } impl Dependency for CargoCrateDependency { fn family(&self) -> &'static str { "cargo-crate" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["cargo".to_string(), "metadata".to_string()]; if let Some(api_version) = &self.api_version { cmd.push(format!("--version={}", api_version)); } let output = session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() .unwrap(); let metadata: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); let packages = metadata["packages"].as_array().unwrap(); packages.iter().any(|package| { package["name"] == self.name && (self.features.is_none() || package["features"].as_array().unwrap().iter().all(|f| { self.features .as_ref() .unwrap() .contains(&f.as_str().unwrap().to_string()) })) }) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for CargoCrateDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = format!( "/usr/share/cargo/registry/{}\\-[0-9]+.*/Cargo\\.toml", self.name ); Some( apt.get_packages_for_paths(vec![&path], true, false) .unwrap() .iter() .map(|p| { if self.api_version.is_some() { crate::dependencies::debian::DebianDependency::new_with_min_version( p.as_str(), &self.api_version.as_ref().unwrap().parse().unwrap(), ) } else { crate::dependencies::debian::DebianDependency::simple(p.as_str()) } }) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingCargoCrate { fn to_dependency(&self) -> Option> { Some(Box::new(CargoCrateDependency::new( &self.crate_name, None, None, ))) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::FromDebianDependency for CargoCrateDependency { fn from_debian_dependency( dependency: &crate::dependencies::debian::DebianDependency, ) -> Option> { use std::collections::HashSet; let (name, min_version) = crate::dependencies::debian::extract_simple_min_version(dependency)?; let (_, name, api_version, features) = lazy_regex::regex_captures!(r"librust-(.*)-([^-+]+)(\+.*?)-dev", &name)?; let features = if features.is_empty() { HashSet::new() } else { features[1..].split("-").collect::>() }; Some(Box::new(Self { name: name.to_string(), api_version: Some(api_version.to_string()), features: Some(features.into_iter().map(|t| t.to_string()).collect()), minimum_version: min_version.map(|v| v.upstream_version), })) } } impl ToDependency for buildlog_consultant::problems::common::MissingRustCompiler { fn to_dependency(&self) -> Option> { Some(Box::new(BinaryDependency::new("rustc"))) } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for CargoCrateDependency { fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::rust::remote_crate_data( &self.name, )) .ok() } } /// Dependency on a pkg-config module. /// /// This represents a dependency on a library that can be found /// and configured using the pkg-config system. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PkgConfigDependency { /// Name of the pkg-config module module: String, /// Optional minimum version requirement minimum_version: Option, } impl PkgConfigDependency { /// Create a new PkgConfigDependency with a version requirement. /// /// # Arguments /// * `module` - Name of the pkg-config module /// * `minimum_version` - Optional minimum version requirement /// /// # Returns /// A new PkgConfigDependency instance pub fn new(module: &str, minimum_version: Option<&str>) -> Self { Self { module: module.to_string(), minimum_version: minimum_version.map(|s| s.to_string()), } } /// Create a new PkgConfigDependency without a version requirement. /// /// # Arguments /// * `module` - Name of the pkg-config module /// /// # Returns /// A new PkgConfigDependency instance without a version requirement pub fn simple(module: &str) -> Self { Self { module: module.to_string(), minimum_version: None, } } } impl Dependency for PkgConfigDependency { fn family(&self) -> &'static str { "pkg-config" } fn present(&self, session: &dyn Session) -> bool { log::debug!("Checking for pkg-config module {}", self.module); let cmd = [ "pkg-config".to_string(), "--exists".to_string(), if let Some(minimum_version) = &self.minimum_version { format!("{} >= {}", self.module, minimum_version) } else { self.module.clone() }, ]; let result = session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success(); if !result { log::debug!("pkg-config module {} not found", self.module); } else { log::debug!("pkg-config module {} found", self.module); } result } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PkgConfigDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let mut names = apt .get_packages_for_paths( [format!( "/usr/lib/.*/pkgconfig/{}\\.pc", regex::escape(&self.module) )] .iter() .map(|s| s.as_str()) .collect(), true, false, ) .unwrap(); if names.is_empty() { names = apt .get_packages_for_paths( [ format!("/usr/lib/pkgconfig/{}\\.pc", regex::escape(&self.module)), format!("/usr/share/pkgconfig/{}\\.pc", regex::escape(&self.module)), ] .iter() .map(|s| s.as_str()) .collect(), true, false, ) .unwrap(); } if names.is_empty() { return None; } Some(if let Some(minimum_version) = &self.minimum_version { let minimum_version: debversion::Version = minimum_version.parse().unwrap(); names .into_iter() .map(|name| { crate::dependencies::debian::DebianDependency::new_with_min_version( &name, &minimum_version, ) }) .collect() } else { names .into_iter() .map(|name| crate::dependencies::debian::DebianDependency::simple(&name)) .collect() }) } } impl ToDependency for buildlog_consultant::problems::common::MissingPkgConfig { fn to_dependency(&self) -> Option> { Some(Box::new(PkgConfigDependency::new( &self.module, self.minimum_version.as_deref(), ))) } } /// Dependency on a file or directory at a specific path. /// /// This represents a dependency on a file or directory that must /// exist at a specific path in the filesystem. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PathDependency { /// Path to the required file or directory path: PathBuf, } impl From for PathDependency { fn from(path: PathBuf) -> Self { Self { path } } } impl PathDependency { /// Create a new PathDependency. /// /// # Arguments /// * `path` - Path to the required file or directory /// /// # Returns /// A new PathDependency instance pub fn new(path: &str) -> Self { Self { path: PathBuf::from(path), } } } impl Dependency for PathDependency { fn family(&self) -> &'static str { "path" } fn present(&self, _session: &dyn Session) -> bool { self.path.exists() } fn project_present(&self, _session: &dyn Session) -> bool { if self.path.is_absolute() { false } else { self.path.exists() } } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PathDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some( apt.get_packages_for_paths(vec![self.path.to_str().unwrap()], false, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingFile { fn to_dependency(&self) -> Option> { Some(Box::new(PathDependency { path: PathBuf::from(&self.path), })) } } /// Dependency on a C header file. /// /// This represents a dependency on a C header file that must /// be available in the system include paths. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CHeaderDependency { /// Name of the C header file header: String, } impl CHeaderDependency { /// Create a new CHeaderDependency. /// /// # Arguments /// * `header` - Name of the C header file /// /// # Returns /// A new CHeaderDependency instance pub fn new(header: &str) -> Self { Self { header: header.to_string(), } } } impl Dependency for CHeaderDependency { fn family(&self) -> &'static str { "c-header" } fn present(&self, session: &dyn Session) -> bool { session .command(vec![ "sh", "-c", &format!("echo \"#include <{}>\" | cc -E -", self.header), ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for CHeaderDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let mut deps = apt .get_packages_for_paths( vec![std::path::Path::new("/usr/include") .join(&self.header) .to_str() .unwrap()], false, false, ) .unwrap(); if deps.is_empty() { deps = apt .get_packages_for_paths( vec![std::path::Path::new("/usr/include") .join(".*") .join(&self.header) .to_str() .unwrap()], true, false, ) .unwrap(); } if deps.is_empty() { return None; } Some( deps.into_iter() .map(|name| crate::dependencies::debian::DebianDependency::simple(&name)) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingCHeader { fn to_dependency(&self) -> Option> { Some(Box::new(CHeaderDependency::new(&self.header))) } } /// Dependency on a JavaScript runtime environment. /// /// This represents a dependency on a JavaScript runtime environment /// like Node.js or a web browser. #[derive(Debug, Clone)] pub struct JavaScriptRuntimeDependency; impl Dependency for JavaScriptRuntimeDependency { fn family(&self) -> &'static str { "javascript-runtime" } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["node", "-e", "process.exit(0)"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for JavaScriptRuntimeDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = vec!["/usr/bin/node", "/usr/bin/duk"]; Some( apt.get_packages_for_paths(paths, false, false) .map(|p| { p.iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect() }) .unwrap(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingJavaScriptRuntime { fn to_dependency(&self) -> Option> { Some(Box::new(JavaScriptRuntimeDependency)) } } /// Dependency on a Vala package. /// /// This represents a dependency on a Vala package that can be located /// using pkg-config. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValaPackageDependency { /// Name of the Vala package package: String, } impl ValaPackageDependency { /// Create a new ValaPackageDependency. /// /// # Arguments /// * `package` - Name of the Vala package /// /// # Returns /// A new ValaPackageDependency instance pub fn new(package: &str) -> Self { Self { package: package.to_string(), } } } impl Dependency for ValaPackageDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "vala-package" fn family(&self) -> &'static str { "vala-package" } /// Checks if the dependency is present in the project context. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn project_present(&self, _session: &dyn Session) -> bool { todo!() } /// Checks if the Vala package is available in the system. /// /// Uses pkg-config to check if the package exists. /// /// # Arguments /// * `session` - The session in which to check /// /// # Returns /// `true` if the package exists, `false` otherwise fn present(&self, session: &dyn Session) -> bool { session .command(vec!["pkg-config", "--exists", &self.package]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for ValaPackageDependency { /// Convert this dependency to a list of Debian package dependencies. /// /// Attempts to find the Debian packages that provide the Vala package by /// searching for .vapi files in standard locations. /// /// # Arguments /// * `apt` - The APT package manager to use for queries /// /// # Returns /// A list of Debian package dependencies if found, or None if not found fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some( apt.get_packages_for_paths( vec![&format!( "/usr/share/vala-[.0-9]+/vapi/{}\\.vapi", regex::escape(&self.package) )], true, false, ) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingValaPackage { /// Convert a MissingValaPackage problem to a Dependency. /// /// # Returns /// A ValaPackageDependency boxed as a Dependency trait object fn to_dependency(&self) -> Option> { Some(Box::new(ValaPackageDependency::new(&self.0))) } } /// Dependency on a Ruby gem. /// /// This represents a dependency on a Ruby gem that can be installed /// via RubyGems or bundler. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RubyGemDependency { /// Name of the Ruby gem gem: String, /// Optional minimum version requirement minimum_version: Option, } impl RubyGemDependency { /// Create a new RubyGemDependency with optional version constraint. /// /// # Arguments /// * `gem` - Name of the Ruby gem /// * `minimum_version` - Optional minimum version requirement /// /// # Returns /// A new RubyGemDependency instance pub fn new(gem: &str, minimum_version: Option<&str>) -> Self { Self { gem: gem.to_string(), minimum_version: minimum_version.map(|s| s.to_string()), } } /// Create a new RubyGemDependency without version constraints. /// /// # Arguments /// * `gem` - Name of the Ruby gem /// /// # Returns /// A new RubyGemDependency instance without version constraints pub fn simple(gem: &str) -> Self { Self { gem: gem.to_string(), minimum_version: None, } } } impl Dependency for RubyGemDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "ruby-gem" fn family(&self) -> &'static str { "ruby-gem" } /// Checks if the gem is present in the project's bundle. /// /// Uses the `bundle list` command to check if the gem is available /// with the required version in the project's Gemfile. /// /// # Arguments /// * `session` - The session in which to check /// /// # Returns /// `true` if the gem exists in the project's bundle, `false` otherwise fn project_present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["bundle".to_string(), "list".to_string()]; if let Some(minimum_version) = &self.minimum_version { cmd.push(format!(">={}", minimum_version)); } cmd.push(self.gem.clone()); session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } /// Checks if the gem is installed in the system. /// /// Uses the `gem list --local` command to check if the gem is available /// with the required version. /// /// # Arguments /// * `session` - The session in which to check /// /// # Returns /// `true` if the gem exists in the system, `false` otherwise fn present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["gem".to_string(), "list".to_string(), "--local".to_string()]; if let Some(minimum_version) = &self.minimum_version { cmd.push(format!(">={}", minimum_version)); } session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for RubyGemDependency { /// Convert this dependency to a list of Debian package dependencies. /// /// Attempts to find the Debian packages that provide the Ruby gem by /// searching for .gemspec files in standard locations. /// /// # Arguments /// * `apt` - The APT package manager to use for queries /// /// # Returns /// A list of Debian package dependencies if found, or None if not found fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let names = apt .get_packages_for_paths( vec![ std::path::Path::new("/usr/share/rubygems-integration/all/specifications/") .join(format!("{}-.*\\.gemspec", regex::escape(&self.gem)).as_str()) .to_str() .unwrap(), ], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .into_iter() .map(|name| { if let Some(min_version) = self.minimum_version.as_ref() { crate::dependencies::debian::DebianDependency::new_with_min_version( &name, &min_version.parse().unwrap(), ) } else { crate::dependencies::debian::DebianDependency::simple(&name) } }) .collect(), ) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::FromDebianDependency for RubyGemDependency { /// Create a RubyGemDependency from a Debian dependency. /// /// Extracts the gem name and version from a Debian package name, /// assuming the package name follows the ruby-* naming convention. /// /// # Arguments /// * `dependency` - The Debian dependency to convert /// /// # Returns /// A RubyGemDependency boxed as a Dependency trait object if conversion is possible, /// None otherwise fn from_debian_dependency( dependency: &crate::dependencies::debian::DebianDependency, ) -> Option> { let (name, min_version) = crate::dependencies::debian::extract_simple_min_version(dependency)?; let (_, name) = lazy_regex::regex_captures!(r"ruby-(.*)", &name)?; Some(Box::new(Self { gem: name.to_string(), minimum_version: min_version.map(|v| v.upstream_version.to_string()), })) } } impl ToDependency for buildlog_consultant::problems::common::MissingRubyGem { /// Convert a MissingRubyGem problem to a Dependency. /// /// # Returns /// A RubyGemDependency boxed as a Dependency trait object fn to_dependency(&self) -> Option> { Some(Box::new(RubyGemDependency::new( &self.gem, self.version.as_deref(), ))) } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for RubyGemDependency { /// Find upstream metadata for this Ruby gem. /// /// Uses the upstream-ontologist crate to fetch metadata about the gem /// from RubyGems.org. /// /// # Returns /// Upstream metadata if available, None otherwise fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::ruby::remote_rubygem_metadata(&self.gem)) .ok() } } /// Dependency on a Debian debhelper addon. /// /// This represents a dependency on a debhelper addon that can be used /// in Debian packaging with the `dh` command. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DhAddonDependency { /// Name of the debhelper addon addon: String, } impl DhAddonDependency { /// Create a new DhAddonDependency. /// /// # Arguments /// * `addon` - Name of the debhelper addon /// /// # Returns /// A new DhAddonDependency instance pub fn new(addon: &str) -> Self { Self { addon: addon.to_string(), } } } impl Dependency for DhAddonDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "dh-addon" fn family(&self) -> &'static str { "dh-addon" } /// Checks if the dependency is present in the system. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn present(&self, _session: &dyn Session) -> bool { todo!() } /// Checks if the dependency is present in the project context. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn project_present(&self, _session: &dyn Session) -> bool { todo!() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for DhAddonDependency { /// Convert this dependency to a list of Debian package dependencies. /// /// Attempts to find the Debian packages that provide the debhelper addon by /// searching for addon Perl modules in standard locations. /// /// # Arguments /// * `apt` - The APT package manager to use for queries /// /// # Returns /// A list of Debian package dependencies if found, or None if not found fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some( apt.get_packages_for_paths( vec![&format!( "/usr/share/perl5/Debian/Debhelper/Sequence/{}.pm", regex::escape(&self.addon) )], true, false, ) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::DhAddonLoadFailure { /// Convert a DhAddonLoadFailure problem to a Dependency. /// /// # Returns /// A DhAddonDependency boxed as a Dependency trait object fn to_dependency(&self) -> Option> { Some(Box::new(DhAddonDependency::new(&self.path))) } } /// Dependency on a system library. /// /// This represents a dependency on a shared library that can be linked /// against using the -l flag with the linker. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LibraryDependency { /// Name of the library (without the lib prefix) library: String, } impl LibraryDependency { /// Create a new LibraryDependency. /// /// # Arguments /// * `library` - Name of the library (without the lib prefix) /// /// # Returns /// A new LibraryDependency instance pub fn new(library: &str) -> Self { Self { library: library.to_string(), } } } impl Dependency for LibraryDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "library" fn family(&self) -> &'static str { "library" } /// Checks if the library is present in the system. /// /// Uses the `ld` command to check if the library can be linked against. /// /// # Arguments /// * `session` - The session in which to check /// /// # Returns /// `true` if the library exists and can be linked against, `false` otherwise fn present(&self, session: &dyn Session) -> bool { session .command(vec!["ld", "-l", &self.library]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } /// Checks if the dependency is present in the project context. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn project_present(&self, _session: &dyn Session) -> bool { todo!() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for LibraryDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = vec![ format!("/usr/lib/lib{}.so", &self.library), format!("/usr/lib/.*/lib{}.so", regex::escape(&self.library)), format!("/usr/lib/lib{}.a", &self.library), format!("/usr/lib/.*/lib{}.a", regex::escape(&self.library)), ]; Some( apt.get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), true, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingLibrary { fn to_dependency(&self) -> Option> { Some(Box::new(LibraryDependency::new(&self.0))) } } /// Dependency on a static library. /// /// This represents a dependency on a static library (.a file) that /// can be linked against at build time. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StaticLibraryDependency { /// Name of the library (without the lib prefix) library: String, /// Filename of the library archive filename: String, } impl StaticLibraryDependency { /// Create a new StaticLibraryDependency. /// /// # Arguments /// * `library` - Name of the library (without the lib prefix) /// * `filename` - Filename of the library archive /// /// # Returns /// A new StaticLibraryDependency instance pub fn new(library: &str, filename: &str) -> Self { Self { library: library.to_string(), filename: filename.to_string(), } } } impl Dependency for StaticLibraryDependency { /// Returns the family name for this dependency type. /// /// # Returns /// The string "static-library" fn family(&self) -> &'static str { "static-library" } /// Checks if the static library is present in the system. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn present(&self, _session: &dyn Session) -> bool { todo!() } /// Checks if the dependency is present in the project context. /// /// # Arguments /// * `_session` - The session in which to check /// /// # Returns /// This method is not implemented yet and will panic if called fn project_present(&self, _session: &dyn Session) -> bool { todo!() } /// Returns this dependency as a trait object. /// /// # Returns /// Reference to this object as a trait object fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for StaticLibraryDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = vec![ format!("/usr/lib/lib{}.a", &self.library), format!("/usr/lib/.*/lib{}.a", regex::escape(&self.library)), ]; Some( apt.get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), true, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingStaticLibrary { fn to_dependency(&self) -> Option> { Some(Box::new(StaticLibraryDependency::new( &self.library, &self.filename, ))) } } /// Dependency on a Ruby source file. /// /// This represents a dependency on a Ruby source file that can be /// loaded with the require() function in Ruby. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RubyFileDependency { /// Name of the Ruby file (without .rb extension) filename: String, } impl RubyFileDependency { /// Create a new RubyFileDependency. /// /// # Arguments /// * `filename` - Name of the Ruby file (without .rb extension) /// /// # Returns /// A new RubyFileDependency instance pub fn new(filename: &str) -> Self { Self { filename: filename.to_string(), } } } impl Dependency for RubyFileDependency { fn family(&self) -> &'static str { "ruby-file" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["ruby", "-e", &format!("require '{}'", self.filename)]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for RubyFileDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = vec![format!( "/usr/lib/ruby/vendor_ruby/{}.rb", regex::escape(&self.filename) )]; let mut names = apt .get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), false, false) .unwrap(); if names.is_empty() { let paths = vec![format!( "/usr/share/rubygems\\-integration/all/gems/([^/]+)/lib/{}\\.rb", regex::escape(&self.filename) )]; names = apt .get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), true, false) .unwrap(); } if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingRubyFile { fn to_dependency(&self) -> Option> { Some(Box::new(RubyFileDependency::new(&self.filename))) } } /// Dependency on a file managed by Sprockets asset pipeline. /// /// This represents a dependency on a file that can be processed by the /// Sprockets asset pipeline in Ruby on Rails. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SprocketsFileDependency { /// MIME type of the file (e.g., "application/javascript") content_type: String, /// Name of the asset name: String, } impl SprocketsFileDependency { /// Create a new SprocketsFileDependency. /// /// # Arguments /// * `content_type` - MIME type of the file /// * `name` - Name of the asset /// /// # Returns /// A new SprocketsFileDependency instance pub fn new(content_type: &str, name: &str) -> Self { Self { content_type: content_type.to_string(), name: name.to_string(), } } } impl Dependency for SprocketsFileDependency { fn family(&self) -> &'static str { "sprockets-file" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["sprockets", "--check", &self.name]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for SprocketsFileDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = match self.content_type.as_str() { "application/javascript" => format!( "/usr/share/,*/app/assets/javascripts/{}\\.js", regex::escape(&self.name) ), _ => return None, }; Some( apt.get_packages_for_paths(vec![&path], true, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingSprocketsFile { fn to_dependency(&self) -> Option> { Some(Box::new(SprocketsFileDependency::new( &self.content_type, &self.name, ))) } } /// Dependency on a CMake module or config file. /// /// This represents a dependency on a CMake module or configuration file /// that can be found with find_package() in CMake. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CMakeFileDependency { /// Name of the CMake file filename: String, /// Optional version requirement version: Option, } impl CMakeFileDependency { /// Create a new CMakeFileDependency with optional version requirement. /// /// # Arguments /// * `filename` - Name of the CMake file /// * `version` - Optional version requirement /// /// # Returns /// A new CMakeFileDependency instance pub fn new(filename: &str, version: Option<&str>) -> Self { Self { filename: filename.to_string(), version: version.map(|s| s.to_string()), } } /// Create a new CMakeFileDependency without version requirement. /// /// # Arguments /// * `filename` - Name of the CMake file /// /// # Returns /// A new CMakeFileDependency instance without version requirement pub fn simple(filename: &str) -> Self { Self { filename: filename.to_string(), version: None, } } } impl Dependency for CMakeFileDependency { fn family(&self) -> &'static str { "cmakefile" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for CMakeFileDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let paths = vec![ format!("/usr/lib/.*/cmake/.*/{}", regex::escape(&self.filename)), format!("/usr/share/.*/cmake/{}", regex::escape(&self.filename)), ]; Some( apt.get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), true, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::CMakeFilesMissing { fn to_dependency(&self) -> Option> { Some(Box::new(CMakeFileDependency::new( &self.filenames[0], self.version.as_deref(), ))) } } /// Type of Maven artifact. /// /// Represents different kinds of Maven artifacts that can be dependencies. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub enum MavenArtifactKind { /// Java archive (JAR) file #[default] Jar, /// Project Object Model (POM) file Pom, } impl std::fmt::Display for MavenArtifactKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MavenArtifactKind::Jar => write!(f, "jar"), MavenArtifactKind::Pom => write!(f, "pom"), } } } impl std::str::FromStr for MavenArtifactKind { type Err = String; fn from_str(s: &str) -> Result { match s { "jar" => Ok(MavenArtifactKind::Jar), "pom" => Ok(MavenArtifactKind::Pom), _ => Err("Invalid Maven artifact kind".to_string()), } } } /// Dependency on a Maven artifact. /// /// This represents a dependency on a Maven artifact identified by /// group ID, artifact ID, and optionally version and kind. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MavenArtifactDependency { /// Maven group ID (e.g., "org.apache.maven") pub group_id: String, /// Maven artifact ID (e.g., "maven-core") pub artifact_id: String, /// Optional version requirement (e.g., "3.8.6") pub version: Option, /// Optional kind of artifact (JAR or POM) pub kind: Option, } impl MavenArtifactDependency { /// Create a new MavenArtifactDependency with all details. /// /// # Arguments /// * `group_id` - Maven group ID /// * `artifact_id` - Maven artifact ID /// * `version` - Optional version requirement /// * `kind` - Optional artifact kind string ("jar" or "pom") /// /// # Returns /// A new MavenArtifactDependency instance pub fn new( group_id: &str, artifact_id: &str, version: Option<&str>, kind: Option<&str>, ) -> Self { Self { group_id: group_id.to_string(), artifact_id: artifact_id.to_string(), version: version.map(|s| s.to_string()), kind: kind.map(|s| s.parse().unwrap()), } } /// Create a new MavenArtifactDependency with just group and artifact IDs. /// /// # Arguments /// * `group_id` - Maven group ID /// * `artifact_id` - Maven artifact ID /// /// # Returns /// A new MavenArtifactDependency instance without version or kind specified pub fn simple(group_id: &str, artifact_id: &str) -> Self { Self { group_id: group_id.to_string(), artifact_id: artifact_id.to_string(), version: None, kind: None, } } } impl From<(String, String)> for MavenArtifactDependency { fn from((group_id, artifact_id): (String, String)) -> Self { Self { group_id, artifact_id, version: None, kind: Some(MavenArtifactKind::Jar), } } } impl From<(String, String, String)> for MavenArtifactDependency { fn from((group_id, artifact_id, version): (String, String, String)) -> Self { Self { group_id, artifact_id, version: Some(version), kind: Some(MavenArtifactKind::Jar), } } } impl From<(String, String, String, String)> for MavenArtifactDependency { fn from((group_id, artifact_id, version, kind): (String, String, String, String)) -> Self { Self { group_id, artifact_id, version: Some(version), kind: Some(kind.parse().unwrap()), } } } impl Dependency for MavenArtifactDependency { fn family(&self) -> &'static str { "maven-artifact" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for MavenArtifactDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let group_id = self.group_id.replace(".", "/"); let kind = self.kind.clone().unwrap_or_default().to_string(); let (path, regex) = if let Some(version) = self.version.as_ref() { ( std::path::Path::new("/usr/share/maven-repo") .join(group_id) .join(&self.artifact_id) .join(version) .join(format!("{}-{}.{}", self.artifact_id, version, kind)), true, ) } else { ( std::path::Path::new("/usr/share/maven-repo") .join(regex::escape(&group_id)) .join(regex::escape(&self.artifact_id)) .join(".*") .join(format!( "{}-.*\\.{}", regex::escape(&self.artifact_id), kind )), false, ) }; let names = apt .get_packages_for_paths(vec![path.to_str().unwrap()], regex, false) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl std::str::FromStr for MavenArtifactDependency { type Err = String; fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split(':').collect(); match parts.len() { 2 => Ok(Self::from((parts[0].to_string(), parts[1].to_string()))), 3 => Ok(Self::from(( parts[0].to_string(), parts[1].to_string(), parts[2].to_string(), ))), 4 => Ok(Self::from(( parts[0].to_string(), parts[1].to_string(), parts[2].to_string(), parts[3].to_string(), ))), _ => Err("Invalid Maven artifact dependency".to_string()), } } } impl ToDependency for buildlog_consultant::problems::common::MissingMavenArtifacts { fn to_dependency(&self) -> Option> { let text = self.0[0].as_str(); let d: MavenArtifactDependency = text.parse().unwrap(); Some(Box::new(d)) } } /// Dependency on GNOME common build tools. /// /// This represents a dependency on the gnome-common package which provides /// common build infrastructure for GNOME projects. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GnomeCommonDependency; impl Dependency for GnomeCommonDependency { fn family(&self) -> &'static str { "gnome-common" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["gnome-autogen.sh", "--version"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for GnomeCommonDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( "gnome-common", )]) } } impl ToDependency for buildlog_consultant::problems::common::MissingGnomeCommonDependency { fn to_dependency(&self) -> Option> { Some(Box::new(GnomeCommonDependency)) } } /// Dependency on a Qt module. /// /// This represents a dependency on a Qt module like QtCore, QtGui, etc. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QtModuleDependency { /// Name of the Qt module module: String, } impl QtModuleDependency { /// Create a new QtModuleDependency. /// /// # Arguments /// * `module` - Name of the Qt module /// /// # Returns /// A new QtModuleDependency instance pub fn new(module: &str) -> Self { Self { module: module.to_string(), } } } impl Dependency for QtModuleDependency { fn family(&self) -> &'static str { "qt-module" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for QtModuleDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let names = apt .get_packages_for_paths( vec![&format!( "/usr/lib/.*/qt5/mkspecs/modules/qt_lib_{}\\.pri", regex::escape(&self.module) )], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingQtModules { fn to_dependency(&self) -> Option> { Some(Box::new(QtModuleDependency::new(&self.0[0]))) } } /// Dependency on the Qt toolkit. /// /// This represents a dependency on the Qt toolkit as a whole, /// specifically the qmake tool for building Qt projects. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QTDependency; impl Dependency for QTDependency { fn family(&self) -> &'static str { "qt" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for QTDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let names = apt .get_packages_for_paths(vec!["/usr/lib/.*/qt[0-9]+/bin/qmake"], true, false) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingQt { fn to_dependency(&self) -> Option> { Some(Box::new(QTDependency)) } } /// Dependency on the X11 window system. /// /// This represents a dependency on the X Window System (X11), /// which is needed for graphical applications. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct X11Dependency; impl Dependency for X11Dependency { fn family(&self) -> &'static str { "x11" } fn present(&self, session: &dyn Session) -> bool { // Does the X binary exist? crate::session::which(session, "X").is_some() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for X11Dependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( "libx11-dev", )]) } } impl ToDependency for buildlog_consultant::problems::common::MissingX11 { fn to_dependency(&self) -> Option> { Some(Box::new(X11Dependency)) } } /// Dependency on certificate authority certificates. /// /// This represents a dependency on CA certificates needed for /// secure HTTPS connections to specific URLs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateAuthorityDependency { /// URL that requires certificate verification url: String, } impl CertificateAuthorityDependency { /// Create a new CertificateAuthorityDependency. /// /// # Arguments /// * `url` - URL that requires certificate verification /// /// # Returns /// A new CertificateAuthorityDependency instance pub fn new(url: &str) -> Self { Self { url: url.to_string(), } } } impl Dependency for CertificateAuthorityDependency { fn family(&self) -> &'static str { "certificate-authority" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for CertificateAuthorityDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::simple( "ca-certificates", )]) } } impl ToDependency for buildlog_consultant::problems::common::UnknownCertificateAuthority { fn to_dependency(&self) -> Option> { Some(Box::new(CertificateAuthorityDependency::new(&self.0))) } } /// Dependency on the GNU Libtool. /// /// This represents a dependency on the GNU Libtool, which is used to /// create portable libraries in autotools-based projects. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LibtoolDependency; impl Dependency for LibtoolDependency { fn family(&self) -> &'static str { "libtool" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["libtoolize", "--version"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for LibtoolDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( "libtool", )]) } } impl ToDependency for buildlog_consultant::problems::common::MissingLibtool { fn to_dependency(&self) -> Option> { Some(Box::new(LibtoolDependency)) } } /// Dependency on a Boost library component. /// /// This represents a dependency on a specific component of the Boost C++ Libraries, /// such as boost_system, boost_filesystem, etc. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BoostComponentDependency { /// Name of the Boost component name: String, } impl BoostComponentDependency { /// Create a new BoostComponentDependency. /// /// # Arguments /// * `name` - Name of the Boost component /// /// # Returns /// A new BoostComponentDependency instance pub fn new(name: &str) -> Self { Self { name: name.to_string(), } } } impl Dependency for BoostComponentDependency { fn family(&self) -> &'static str { "boost-component" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for BoostComponentDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let names = apt .get_packages_for_paths( vec![&format!( "/usr/lib/.*/libboost_{}", regex::escape(&self.name) )], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } /// Dependency on a KDE Frameworks 5 component. /// /// This represents a dependency on a specific component of the /// KDE Frameworks 5, such as KF5Auth, KF5CoreAddons, etc. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KF5ComponentDependency { /// Name of the KF5 component name: String, } impl KF5ComponentDependency { /// Create a new KF5ComponentDependency. /// /// # Arguments /// * `name` - Name of the KF5 component /// /// # Returns /// A new KF5ComponentDependency instance pub fn new(name: &str) -> Self { Self { name: name.to_string(), } } } impl Dependency for KF5ComponentDependency { fn family(&self) -> &'static str { "kf5-component" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for KF5ComponentDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let names = apt .get_packages_for_paths( vec![&format!( "/usr/lib/.*/cmake/KF5{}/KF5{}Config\\.cmake", regex::escape(&self.name), regex::escape(&self.name) )], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingCMakeComponents { fn to_dependency(&self) -> Option> { match self.name.as_str() { "Boost" => Some(Box::new(BoostComponentDependency::new(&self.components[0]))), "KF5" => Some(Box::new(KF5ComponentDependency::new(&self.components[0]))), n => { log::warn!("Unknown CMake component: {}", n); None } } } } /// Dependency on a Gnulib directory. /// /// This represents a dependency on a specific directory containing /// Gnulib code, which is a collection of portable C functions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GnulibDirectoryDependency { /// Path to the Gnulib directory directory: PathBuf, } impl GnulibDirectoryDependency { /// Create a new GnulibDirectoryDependency. /// /// # Arguments /// * `directory` - Path to the Gnulib directory /// /// # Returns /// A new GnulibDirectoryDependency instance pub fn new(directory: &str) -> Self { Self { directory: PathBuf::from(directory), } } } impl Dependency for GnulibDirectoryDependency { fn family(&self) -> &'static str { "gnulib-directory" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } impl ToDependency for buildlog_consultant::problems::common::MissingGnulibDirectory { fn to_dependency(&self) -> Option> { Some(Box::new(GnulibDirectoryDependency { directory: self.0.clone(), })) } } /// Dependency on a GObject Introspection typelib. /// /// This represents a dependency on a GObject Introspection typelib file, /// which provides language bindings for GObject-based libraries. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IntrospectionTypelibDependency { /// Name of the library with introspection data library: String, } impl IntrospectionTypelibDependency { /// Create a new IntrospectionTypelibDependency. /// /// # Arguments /// * `library` - Name of the library with introspection data /// /// # Returns /// A new IntrospectionTypelibDependency instance pub fn new(library: &str) -> Self { Self { library: library.to_string(), } } } impl Dependency for IntrospectionTypelibDependency { fn family(&self) -> &'static str { "introspection-type-lib" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } impl ToDependency for buildlog_consultant::problems::common::MissingIntrospectionTypelib { fn to_dependency(&self) -> Option> { Some(Box::new(IntrospectionTypelibDependency::new(&self.0))) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for IntrospectionTypelibDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { let names = apt .get_packages_for_paths( vec![&format!( "/usr/lib/.*/girepository\\-.*/{}\\-.*.typelib", regex::escape(&self.library) )], true, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl ToDependency for buildlog_consultant::problems::common::MissingCSharpCompiler { fn to_dependency(&self) -> Option> { Some(Box::new(BinaryDependency::new("mcs"))) } } impl ToDependency for buildlog_consultant::problems::common::MissingXfceDependency { fn to_dependency(&self) -> Option> { match self.package.as_str() { "gtk-doc" => Some(Box::new(BinaryDependency::new("gtkdocize"))), n => { log::warn!("Unknown XFCE dependency: {}", n); None } } } } ognibuild-0.2.6/src/dependencies/node.rs000064400000000000000000000265201046102023000163220ustar 00000000000000use crate::dependencies::BinaryDependency; use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Node.js package. pub struct NodePackageDependency { package: String, } impl NodePackageDependency { /// Creates a new NodePackageDependency instance. pub fn new(package: &str) -> Self { Self { package: package.to_string(), } } } impl Dependency for NodePackageDependency { fn family(&self) -> &'static str { "npm-package" } fn present(&self, session: &dyn Session) -> bool { // npm list -g package-name --depth=0 >/dev/null 2>&1 session .command(vec!["npm", "list", "-g", &self.package, "--depth=0"]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for NodePackageDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { let paths = vec![ format!( "/usr/share/nodejs/.*/node_modules/{}/package\\.json", regex::escape(&self.package) ), format!( "/usr/lib/nodejs/{}/package\\.json", regex::escape(&self.package) ), format!( "/usr/share/nodejs/{}/package\\.json", regex::escape(&self.package) ), ]; let names = match apt.get_packages_for_paths( paths.iter().map(|p| p.as_str()).collect(), true, false, ) { Ok(names) => names, Err(e) => { log::warn!( "Failed to search for Node package {} in APT: {}", self.package, e ); return None; } }; if names.is_empty() { None } else { Some( names .into_iter() .map(|name| super::debian::DebianDependency::new(&name)) .collect(), ) } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingNodePackage { fn to_dependency(&self) -> Option> { Some(Box::new(NodePackageDependency::new(&self.0))) } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for NodePackageDependency { fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::node::remote_npm_metadata( &self.package, )) .ok() } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Node.js module. pub struct NodeModuleDependency { module: String, } impl NodeModuleDependency { /// Creates a new NodeModuleDependency instance. pub fn new(module: &str) -> Self { Self { module: module.to_string(), } } } impl Dependency for NodeModuleDependency { fn family(&self) -> &'static str { "node-module" } fn present(&self, session: &dyn Session) -> bool { // node -e 'try { require.resolve("express"); process.exit(0); } catch(e) { process.exit(1); }' session .command(vec![ "node", "-e", &format!( r#"try {{ require.resolve("{}"); process.exit(0); }} catch(e) {{ process.exit(1); }}"#, self.module ), ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for NodeModuleDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { let paths = vec![ format!( "/usr/share/nodejs/.*/node_modules/{}/package\\.json", regex::escape(&self.module) ), format!( "/usr/lib/nodejs/{}/package\\.json", regex::escape(&self.module) ), format!( "/usr/share/nodejs/{}/package\\.json", regex::escape(&self.module) ), ]; let names = match apt.get_packages_for_paths( paths.iter().map(|p| p.as_str()).collect(), true, false, ) { Ok(names) => names, Err(e) => { log::warn!( "Failed to search for Node module {} in APT: {}", self.module, e ); return None; } }; if names.is_empty() { None } else { Some( names .into_iter() .map(|name| super::debian::DebianDependency::new(&name)) .collect(), ) } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingNodeModule { fn to_dependency(&self) -> Option> { Some(Box::new(NodeModuleDependency::new(&self.0))) } } fn command_package(command: &str) -> Option<&str> { match command { "del-cli" => Some("del-cli"), "husky" => Some("husky"), "cross-env" => Some("cross-env"), "xo" => Some("xo"), "standard" => Some("standard"), "jshint" => Some("jshint"), "if-node-version" => Some("if-node-version"), "babel-cli" => Some("babel"), "c8" => Some("c8"), "prettier-standard" => Some("prettier-standard"), _ => None, } } /// A resolver for Node.js packages using npm. pub struct NpmResolver<'a> { session: &'a dyn Session, } impl<'a> NpmResolver<'a> { /// Creates a new NpmResolver instance. pub fn new(session: &'a dyn Session) -> Self { Self { session } } fn cmd( &self, reqs: &[&NodePackageDependency], scope: InstallationScope, ) -> Result, Error> { let mut cmd = vec!["npm".to_string(), "install".to_string()]; match scope { InstallationScope::Global => cmd.push("-g".to_string()), InstallationScope::User => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } cmd.extend(reqs.iter().map(|req| req.package.clone())); Ok(cmd) } } impl From for NodePackageDependency { fn from(dep: NodeModuleDependency) -> Self { let parts: Vec<&str> = dep.module.split('/').collect(); Self { // TODO: Is this legit? package: if parts[0].starts_with('@') { parts[..2].join("/") } else { parts[0].to_string() }, } } } fn to_node_package_req(requirement: &dyn Dependency) -> Option { if let Some(requirement) = requirement.as_any().downcast_ref::() { Some(requirement.clone().into()) } else if let Some(requirement) = requirement.as_any().downcast_ref::() { Some(requirement.clone()) } else if let Some(requirement) = requirement.as_any().downcast_ref::() { command_package(&requirement.binary_name).map(NodePackageDependency::new) } else { None } } #[cfg(test)] mod tests { #[test] #[cfg(feature = "debian")] fn test_get_project_wide_deps_no_hang() { use crate::buildsystem::detect_buildsystems; use crate::session::test_utils; use tempfile::TempDir; log::debug!("Starting test_get_project_wide_deps_no_hang"); let temp_dir = TempDir::new().unwrap(); let project_dir = temp_dir.path().join("test-nodejs"); std::fs::create_dir(&project_dir).unwrap(); std::fs::write( project_dir.join("package.json"), r#"{"name": "test", "version": "1.0.0", "dependencies": {"lodash": "^4.17.21"}}"#, ) .unwrap(); log::debug!("Created test project at {:?}", project_dir); let buildsystems = detect_buildsystems(&project_dir); assert!(!buildsystems.is_empty(), "Should detect node buildsystem"); let node_buildsystem = buildsystems .iter() .find(|bs| bs.name() == "node") .expect("Should find node buildsystem"); log::debug!("Detected buildsystem: {}", node_buildsystem.name()); // Use test session for better isolation when possible let session = test_utils::get_test_session().expect("Failed to create test session"); // This should NOT hang, even with network dependencies log::debug!("Calling get_project_wide_deps..."); let start = std::time::Instant::now(); let result = crate::debian::upstream_deps::get_project_wide_deps( session.as_ref(), node_buildsystem.as_ref(), ); let duration = start.elapsed(); // Should complete quickly with isolated session (no large APT downloads) assert!( duration.as_secs() < 30, "get_project_wide_deps took too long: {:?}", duration ); log::debug!("get_project_wide_deps completed in {:?}", duration); log::debug!("Build deps: {} items", result.0.len()); log::debug!("Test deps: {} items", result.1.len()); } } impl<'a> Installer for NpmResolver<'a> { fn explain( &self, requirement: &dyn Dependency, scope: InstallationScope, ) -> Result { let requirement = to_node_package_req(requirement).ok_or(Error::UnknownDependencyFamily)?; Ok(Explanation { message: format!("install node package {}", requirement.package), command: Some(self.cmd(&[&requirement], scope)?), }) } fn install(&self, requirement: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let requirement = to_node_package_req(requirement).ok_or(Error::UnknownDependencyFamily)?; let args = &self.cmd(&[&requirement], scope)?; let mut cmd = self .session .command(args.iter().map(|s| s.as_str()).collect()); match scope { InstallationScope::Global => { cmd = cmd.user("root"); } InstallationScope::User => {} InstallationScope::Vendor => {} } cmd.run_detecting_problems()?; Ok(()) } } ognibuild-0.2.6/src/dependencies/octave.rs000064400000000000000000000151451046102023000166570ustar 00000000000000use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// OctavePackageDependency represents a dependency on an Octave package. pub struct OctavePackageDependency { package: String, minimum_version: Option, } impl OctavePackageDependency { /// Creates a new OctavePackageDependency with a specified minimum version. pub fn new(package: &str, minimum_version: Option<&str>) -> Self { Self { package: package.to_string(), minimum_version: minimum_version.map(|s| s.to_string()), } } /// Creates a new OctavePackageDependency with no minimum version. pub fn simple(package: &str) -> Self { Self { package: package.to_string(), minimum_version: None, } } } impl std::str::FromStr for OctavePackageDependency { type Err = String; fn from_str(s: &str) -> Result { if let Some((_, name, min_version)) = lazy_regex::regex_captures!("(.*) \\(>= (.*)\\)", s) { Ok(Self::new(name, Some(min_version))) } else if !s.contains(" ") { Ok(Self::simple(s)) } else { Err(format!("Failed to parse Octave package dependency: {}", s)) } } } impl Dependency for OctavePackageDependency { fn family(&self) -> &'static str { "octave-package" } fn present(&self, session: &dyn Session) -> bool { session .command(vec![ "octave", "--eval", &format!("pkg load {}", self.package), ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } /// OctaveForgeResolver is an installer for Octave packages using the Octave Forge repository. pub struct OctaveForgeResolver<'a> { session: &'a dyn Session, } impl<'a> OctaveForgeResolver<'a> { /// Creates a new OctaveForgeResolver. pub fn new(session: &'a dyn Session) -> Self { Self { session } } fn cmd( &self, dependency: &OctavePackageDependency, scope: InstallationScope, ) -> Result, Error> { match scope { InstallationScope::Global => Ok(vec![ "octave-cli".to_string(), "--eval".to_string(), format!("pkg install -forge -global {}", dependency.package), ]), InstallationScope::User => Ok(vec![ "octave-cli".to_string(), "--eval".to_string(), format!("pkg install -forge -local {}", dependency.package), ]), InstallationScope::Vendor => Err(Error::UnsupportedScope(scope)), } } } impl<'a> Installer for OctaveForgeResolver<'a> { fn explain( &self, dependency: &dyn Dependency, scope: InstallationScope, ) -> Result { let dependency = dependency .as_any() .downcast_ref::() .unwrap(); let cmd = self.cmd(dependency, scope)?; Ok(Explanation { command: Some(cmd), message: format!("Install Octave package {}", dependency.package), }) } fn install(&self, dependency: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let dependency = dependency .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let cmd = self.cmd(dependency, scope)?; log::info!("Octave: installing {}", dependency.package); self.session .command(cmd.iter().map(|x| x.as_str()).collect()) .run_detecting_problems()?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::any::Any; #[test] fn test_octave_package_dependency_new() { let dependency = OctavePackageDependency::new("signal", Some("1.0.0")); assert_eq!(dependency.package, "signal"); assert_eq!(dependency.minimum_version, Some("1.0.0".to_string())); } #[test] fn test_octave_package_dependency_simple() { let dependency = OctavePackageDependency::simple("signal"); assert_eq!(dependency.package, "signal"); assert_eq!(dependency.minimum_version, None); } #[test] fn test_octave_package_dependency_family() { let dependency = OctavePackageDependency::simple("signal"); assert_eq!(dependency.family(), "octave-package"); } #[test] fn test_octave_package_dependency_as_any() { let dependency = OctavePackageDependency::simple("signal"); let any_dep: &dyn Any = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_octave_package_dependency_from_str_simple() { let dependency: OctavePackageDependency = "signal".parse().unwrap(); assert_eq!(dependency.package, "signal"); assert_eq!(dependency.minimum_version, None); } #[test] fn test_octave_package_dependency_from_str_with_version() { let dependency: OctavePackageDependency = "signal (>= 1.0.0)".parse().unwrap(); assert_eq!(dependency.package, "signal"); assert_eq!(dependency.minimum_version, Some("1.0.0".to_string())); } #[test] fn test_octave_package_dependency_from_str_invalid() { let result: Result = "signal with bad format".parse(); assert!(result.is_err()); } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for OctavePackageDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { if let Some(minimum_version) = &self.minimum_version { Some(vec![ crate::dependencies::debian::DebianDependency::new_with_min_version( &format!("octave-{}", &self.package), &minimum_version.parse().unwrap(), ), ]) } else { Some(vec![crate::dependencies::debian::DebianDependency::new( &format!("octave-{}", &self.package), )]) } } } ognibuild-0.2.6/src/dependencies/perl.rs000064400000000000000000000463311046102023000163410ustar 00000000000000//! Support for Perl dependencies. //! //! This module provides functionality for working with Perl dependencies, //! including Perl modules, pre-declared dependencies, and file dependencies. use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Perl module. /// /// This represents a dependency on a Perl module that needs to be installed /// for the code to function properly. pub struct PerlModuleDependency { /// The name of the Perl module. pub module: String, /// The optional filename that contains the module. pub filename: Option, /// Optional include paths for finding the module. pub inc: Option>, } impl PerlModuleDependency { /// Create a new Perl module dependency with filename and include paths. /// /// # Arguments /// * `module` - The name of the Perl module /// * `filename` - Optional filename that contains the module /// * `inc` - Optional include paths for finding the module /// /// # Returns /// A new PerlModuleDependency pub fn new(module: &str, filename: Option<&str>, inc: Option>) -> Self { Self { module: module.to_string(), filename: filename.map(|s| s.to_string()), inc: inc.map(|v| v.iter().map(|s| s.to_string()).collect()), } } /// Create a simple Perl module dependency with just a module name. /// /// # Arguments /// * `module` - The name of the Perl module /// /// # Returns /// A new PerlModuleDependency with no filename or include paths pub fn simple(module: &str) -> Self { Self { module: module.to_string(), filename: None, inc: None, } } } impl Dependency for PerlModuleDependency { fn family(&self) -> &'static str { "perl-module" } fn present(&self, session: &dyn Session) -> bool { let mut cmd = vec!["perl".to_string(), "-M".to_string(), self.module.clone()]; if let Some(filename) = &self.filename { cmd.push(filename.to_string()); } if let Some(inc) = &self.inc { cmd.push("-I".to_string()); cmd.push(inc.join(":")); } cmd.push("-e".to_string()); cmd.push("1".to_string()); session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for PerlModuleDependency { fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::perl::remote_cpan_data( &self.module, )) .ok() } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a predeclared Perl module. /// /// This represents a dependency on a Perl module that is known by name but /// may map to a different actual module name. pub struct PerlPreDeclaredDependency { name: String, } /// Map a predeclared module name to an actual module name. /// /// # Arguments /// * `name` - The predeclared module name /// /// # Returns /// The actual module name if known fn known_predeclared_module(name: &str) -> Option<&str> { // TODO(jelmer): Can we obtain this information elsewhere? match name { "auto_set_repository" => Some("Module::Install::Repository"), "author_tests" => Some("Module::Install::AuthorTests"), "recursive_author_tests" => Some("Module::Install::AuthorTests"), "author_requires" => Some("Module::Install::AuthorRequires"), "readme_from" => Some("Module::Install::ReadmeFromPod"), "catalyst" => Some("Module::Install::Catalyst"), "githubmeta" => Some("Module::Install::GithubMeta"), "use_ppport" => Some("Module::Install::XSUtil"), "pod_from" => Some("Module::Install::PodFromEuclid"), "write_doap_changes" => Some("Module::Install::DOAPChangeSets"), "use_test_base" => Some("Module::Install::TestBase"), "jsonmeta" => Some("Module::Install::JSONMETA"), "extra_tests" => Some("Module::Install::ExtraTests"), "auto_set_bugtracker" => Some("Module::Install::Bugtracker"), _ => None, } } impl PerlPreDeclaredDependency { /// Create a new predeclared Perl module dependency. /// /// # Arguments /// * `name` - The name of the predeclared module /// /// # Returns /// A new PerlPreDeclaredDependency pub fn new(name: &str) -> Self { Self { name: name.to_string(), } } } impl Dependency for PerlPreDeclaredDependency { fn family(&self) -> &'static str { "perl-predeclared" } fn present(&self, session: &dyn Session) -> bool { if let Some(module) = known_predeclared_module(&self.name) { PerlModuleDependency::simple(module).present(session) } else { todo!() } } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PerlPreDeclaredDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { if let Some(module) = known_predeclared_module(&self.name) { PerlModuleDependency::simple(module).try_into_debian_dependency(apt) } else { None } } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPerlPredeclared { fn to_dependency(&self) -> Option> { match known_predeclared_module(self.0.as_str()) { Some(_module) => Some(Box::new(PerlModuleDependency::simple(self.0.as_str()))), None => { log::warn!("Unknown predeclared function: {}", self.0); None } } } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Perl file. /// /// This represents a dependency on a specific Perl file rather than a module. pub struct PerlFileDependency { filename: String, } impl PerlFileDependency { /// Create a new Perl file dependency. /// /// # Arguments /// * `filename` - The path to the Perl file /// /// # Returns /// A new PerlFileDependency pub fn new(filename: &str) -> Self { Self { filename: filename.to_string(), } } } impl Dependency for PerlFileDependency { fn family(&self) -> &'static str { "perl-file" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["perl", "-e", &format!("require '{}'", self.filename)]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPerlFile { fn to_dependency(&self) -> Option> { Some(Box::new(PerlFileDependency { filename: self.filename.clone(), })) } } /// A resolver for Perl module dependencies using CPAN. /// /// This resolver installs Perl modules from CPAN using the cpan command. pub struct CPAN<'a> { session: &'a dyn Session, skip_tests: bool, } impl<'a> CPAN<'a> { /// Create a new CPAN resolver. /// /// # Arguments /// * `session` - The session to use for executing commands /// * `skip_tests` - Whether to skip tests when installing modules /// /// # Returns /// A new CPAN resolver pub fn new(session: &'a dyn Session, skip_tests: bool) -> Self { Self { session, skip_tests, } } fn cmd( &self, reqs: &[&PerlModuleDependency], _scope: InstallationScope, ) -> Result, Error> { let mut ret = vec!["cpan".to_string(), "-i".to_string()]; if self.skip_tests { ret.push("-T".to_string()); } ret.extend(reqs.iter().map(|req| req.module.clone())); Ok(ret) } } impl<'a> Installer for CPAN<'a> { fn explain( &self, dep: &dyn Dependency, scope: InstallationScope, ) -> Result { if let Some(dep) = dep.as_any().downcast_ref::() { let cmd = self.cmd(&[dep], scope)?; let explanation = Explanation { message: "Install the following Perl modules".to_string(), command: Some(cmd), }; Ok(explanation) } else { Err(Error::UnknownDependencyFamily) } } fn install(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let env = maplit::hashmap! { "PERL_MM_USE_DEFAULT".to_string() => "1".to_string(), "PERL_MM_OPT".to_string() => "".to_string(), "PERL_MB_OPT".to_string() => "".to_string(), }; let user = match scope { InstallationScope::User => None, InstallationScope::Global => Some("root"), InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } }; let dep = dep .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let cmd = self.cmd(&[dep], scope)?; log::info!("CPAN: running {:?}", cmd); let mut cmd = self .session .command(cmd.iter().map(|s| s.as_str()).collect()) .env(env); if let Some(user) = user { cmd = cmd.user(user); } cmd.run_detecting_problems()?; Ok(()) } fn explain_some( &self, deps: Vec>, scope: InstallationScope, ) -> Result<(Vec, Vec>), Error> { let mut explanations = Vec::new(); let mut failed = Vec::new(); for dep in deps { match self.explain(&*dep, scope) { Ok(explanation) => explanations.push(explanation), Err(Error::UnknownDependencyFamily) => failed.push(dep), Err(e) => { return Err(e); } } } Ok((explanations, failed)) } fn install_some( &self, deps: Vec>, scope: InstallationScope, ) -> Result<(Vec>, Vec>), Error> { let mut installed = Vec::new(); let mut failed = Vec::new(); for dep in deps { match self.install(&*dep, scope) { Ok(()) => installed.push(dep), Err(Error::UnknownDependencyFamily) => failed.push(dep), Err(e) => { return Err(e); } } } Ok((installed, failed)) } } /// Default paths where Perl modules can be installed. pub const DEFAULT_PERL_PATHS: &[&str] = &[ "/usr/share/perl5", "/usr/lib/.*/perl5/.*", "/usr/lib/.*/perl-base", "/usr/lib/.*/perl/[^/]+", "/usr/share/perl/[^/]+", ]; #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PerlModuleDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { use std::path::Path; let (regex, paths) = if let (Some(inc), Some(filename)) = (self.inc.as_ref(), self.filename.as_ref()) { ( false, inc.iter().map(|s| Path::new(s).join(filename)).collect(), ) } else if let Some(filename) = &self.filename { if !Path::new(filename).is_absolute() { ( true, DEFAULT_PERL_PATHS .iter() .map(|s| Path::new(s).join(filename)) .collect(), ) } else { (false, vec![Path::new(filename).to_path_buf()]) } } else { ( true, DEFAULT_PERL_PATHS .iter() .map(|s| Path::new(s).join(format!("{}.pm", &self.module.replace("::", "/")))) .collect(), ) }; let packages = apt .get_packages_for_paths( paths .iter() .map(|s| s.to_str().unwrap()) .collect::>(), regex, false, ) .unwrap(); Some( packages .into_iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(&p)) .collect(), ) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PerlFileDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let packages = apt .get_packages_for_paths(vec![&self.filename], false, false) .unwrap(); Some( packages .into_iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(&p)) .collect(), ) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPerlModule { fn to_dependency(&self) -> Option> { Some(Box::new(PerlModuleDependency { module: self.module.clone(), filename: self.filename.clone(), inc: self.inc.clone(), })) } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; #[test] fn test_perl_module_dependency_new() { let dependency = PerlModuleDependency::new( "Test::More", Some("Test/More.pm"), Some(vec!["/usr/share/perl5"]), ); assert_eq!(dependency.module, "Test::More"); assert_eq!(dependency.filename, Some("Test/More.pm".to_string())); assert_eq!(dependency.inc, Some(vec!["/usr/share/perl5".to_string()])); } #[test] fn test_perl_module_dependency_simple() { let dependency = PerlModuleDependency::simple("Test::More"); assert_eq!(dependency.module, "Test::More"); assert_eq!(dependency.filename, None); assert_eq!(dependency.inc, None); } #[test] fn test_perl_module_dependency_family() { let dependency = PerlModuleDependency::simple("Test::More"); assert_eq!(dependency.family(), "perl-module"); } #[test] fn test_perl_module_dependency_as_any() { let dependency = PerlModuleDependency::simple("Test::More"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_perl_module_to_dependency() { let problem = buildlog_consultant::problems::common::MissingPerlModule { module: "Test::More".to_string(), filename: Some("Test/More.pm".to_string()), inc: None, minimum_version: None, }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "perl-module"); let perl_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(perl_dep.module, "Test::More"); assert_eq!(perl_dep.filename, Some("Test/More.pm".to_string())); } #[test] fn test_perl_predeclared_dependency_new() { let dependency = PerlPreDeclaredDependency::new("auto_set_repository"); assert_eq!(dependency.name, "auto_set_repository"); } #[test] fn test_perl_predeclared_dependency_family() { let dependency = PerlPreDeclaredDependency::new("auto_set_repository"); assert_eq!(dependency.family(), "perl-predeclared"); } #[test] fn test_perl_predeclared_dependency_as_any() { let dependency = PerlPreDeclaredDependency::new("auto_set_repository"); let any_dep = dependency.as_any(); assert!(any_dep .downcast_ref::() .is_some()); } #[test] fn test_known_predeclared_module() { assert_eq!( known_predeclared_module("auto_set_repository"), Some("Module::Install::Repository") ); assert_eq!( known_predeclared_module("author_tests"), Some("Module::Install::AuthorTests") ); assert_eq!(known_predeclared_module("unknown_function"), None); } #[test] fn test_perl_file_dependency_new() { let dependency = PerlFileDependency::new("Test/More.pm"); assert_eq!(dependency.filename, "Test/More.pm"); } #[test] fn test_perl_file_dependency_family() { let dependency = PerlFileDependency::new("Test/More.pm"); assert_eq!(dependency.family(), "perl-file"); } #[test] fn test_perl_file_dependency_as_any() { let dependency = PerlFileDependency::new("Test/More.pm"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_perl_file_to_dependency() { let problem = buildlog_consultant::problems::common::MissingPerlFile { filename: "Test/More.pm".to_string(), inc: None, }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "perl-file"); let perl_file_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(perl_file_dep.filename, "Test/More.pm"); } #[test] fn test_cpan_new() { let session = crate::session::plain::PlainSession::new(); let cpan = CPAN::new(&session, true); assert!(cpan.skip_tests); } #[test] fn test_cpan_cmd() { let session = crate::session::plain::PlainSession::new(); let cpan = CPAN::new(&session, true); let dependency = PerlModuleDependency::simple("Test::More"); let cmd = cpan.cmd(&[&dependency], InstallationScope::User).unwrap(); assert_eq!(cmd, vec!["cpan", "-i", "-T", "Test::More"]); } } ognibuild-0.2.6/src/dependencies/php.rs000064400000000000000000000261021046102023000161600ustar 00000000000000//! Support for PHP dependencies. //! //! This module provides functionality for working with PHP dependencies, //! including PHP classes, packages via Composer, and PHP extensions. use crate::dependency::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a PHP class. /// /// This represents a dependency on a specific PHP class that needs to be available /// for the code to function properly. pub struct PhpClassDependency { php_class: String, } impl PhpClassDependency { /// Create a new PHP class dependency. /// /// # Arguments /// * `php_class` - The name of the PHP class /// /// # Returns /// A new PhpClassDependency pub fn new(php_class: &str) -> Self { Self { php_class: php_class.to_string(), } } } impl Dependency for PhpClassDependency { fn family(&self) -> &'static str { "php-class" } fn present(&self, session: &dyn Session) -> bool { session .command(vec!["php", "-r", &format!("new {}", self.php_class)]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; use std::any::Any; #[test] fn test_php_class_dependency_new() { let dependency = PhpClassDependency::new("SimplePie"); assert_eq!(dependency.php_class, "SimplePie"); } #[test] fn test_php_class_dependency_family() { let dependency = PhpClassDependency::new("SimplePie"); assert_eq!(dependency.family(), "php-class"); } #[test] fn test_php_class_dependency_as_any() { let dependency = PhpClassDependency::new("SimplePie"); let any_dep: &dyn Any = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_php_class_to_dependency() { let problem = buildlog_consultant::problems::common::MissingPhpClass { php_class: "SimplePie".to_string(), }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "php-class"); let php_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(php_dep.php_class, "SimplePie"); } #[test] fn test_php_package_dependency_simple() { let dependency = PhpPackageDependency::simple("symfony/console"); assert_eq!(dependency.package, "symfony/console"); assert_eq!(dependency.channel, None); assert_eq!(dependency.min_version, None); assert_eq!(dependency.max_version, None); } #[test] fn test_php_package_dependency_new() { let dependency = PhpPackageDependency::new( "symfony/console", Some("packagist"), Some("4.0.0"), Some("5.0.0"), ); assert_eq!(dependency.package, "symfony/console"); assert_eq!(dependency.channel, Some("packagist".to_string())); assert_eq!(dependency.min_version, Some("4.0.0".to_string())); assert_eq!(dependency.max_version, Some("5.0.0".to_string())); } #[test] fn test_php_package_dependency_family() { let dependency = PhpPackageDependency::simple("symfony/console"); assert_eq!(dependency.family(), "php-package"); } #[test] fn test_php_extension_dependency_new() { let dependency = PhpExtensionDependency::new("curl"); assert_eq!(dependency.extension, "curl"); } #[test] fn test_php_extension_dependency_family() { let dependency = PhpExtensionDependency::new("curl"); assert_eq!(dependency.family(), "php-extension"); } #[test] fn test_missing_php_extension_to_dependency() { let problem = buildlog_consultant::problems::common::MissingPHPExtension("curl".to_string()); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "php-extension"); let php_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(php_dep.extension, "curl"); } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PhpClassDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = format!("/usr/share/php/{}", self.php_class.replace("\\", "/")); let names = apt .get_packages_for_paths(vec![&path], false, false) .unwrap(); Some( names .into_iter() .map(|name| crate::dependencies::debian::DebianDependency::new(&name)) .collect(), ) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPhpClass { fn to_dependency(&self) -> Option> { Some(Box::new(PhpClassDependency::new(&self.php_class))) } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a PHP package installed via Composer. /// /// This represents a dependency on a PHP package that can be installed /// through Composer, with optional version constraints and channel specification. pub struct PhpPackageDependency { /// The name of the PHP package. pub package: String, /// The channel to install from (e.g., "pear"). pub channel: Option, /// The minimum version required. pub min_version: Option, /// The maximum version allowed. pub max_version: Option, } impl PhpPackageDependency { /// Create a new PHP package dependency with version constraints. /// /// # Arguments /// * `package` - The name of the PHP package /// * `channel` - Optional channel to install from /// * `min_version` - Optional minimum version constraint /// * `max_version` - Optional maximum version constraint /// /// # Returns /// A new PhpPackageDependency pub fn new( package: &str, channel: Option<&str>, min_version: Option<&str>, max_version: Option<&str>, ) -> Self { Self { package: package.to_string(), channel: channel.map(|s| s.to_string()), min_version: min_version.map(|s| s.to_string()), max_version: max_version.map(|s| s.to_string()), } } /// Create a simple PHP package dependency with no version constraints. /// /// # Arguments /// * `package` - The name of the PHP package /// /// # Returns /// A new PhpPackageDependency with no version constraints pub fn simple(package: &str) -> Self { Self { package: package.to_string(), channel: None, min_version: None, max_version: None, } } } impl Dependency for PhpPackageDependency { fn family(&self) -> &'static str { "php-package" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, session: &dyn Session) -> bool { // Run `composer show` and check the output let output = session .command(vec!["composer", "show", "--format=json"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() .unwrap(); let packages: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); let packages = packages["installed"].as_array().unwrap(); packages.iter().any(|package| { package["name"] == self.package && (self.min_version.is_none() || package["version"] .as_str() .unwrap() .parse::() .unwrap() >= self .min_version .as_ref() .unwrap() .parse::() .unwrap()) && (self.max_version.is_none() || package["version"] .as_str() .unwrap() .parse::() .unwrap() <= self .max_version .as_ref() .unwrap() .parse::() .unwrap()) }) } fn as_any(&self) -> &dyn std::any::Any { self } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a PHP extension. /// /// This represents a dependency on a PHP extension that needs to be installed /// and enabled for the code to function properly. pub struct PhpExtensionDependency { /// The name of the PHP extension. pub extension: String, } impl PhpExtensionDependency { /// Create a new PHP extension dependency. /// /// # Arguments /// * `extension` - The name of the PHP extension /// /// # Returns /// A new PhpExtensionDependency pub fn new(extension: &str) -> Self { Self { extension: extension.to_string(), } } } impl Dependency for PhpExtensionDependency { fn family(&self) -> &'static str { "php-extension" } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn present(&self, session: &dyn Session) -> bool { // Grep the output of php -m let output = session .command(vec!["php", "-m"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() .unwrap() .stdout; String::from_utf8(output) .unwrap() .lines() .any(|line| line == self.extension) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PhpExtensionDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::new( &format!("php-{}", &self.extension), )]) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPHPExtension { fn to_dependency(&self) -> Option> { Some(Box::new(PhpExtensionDependency::new(&self.0))) } } ognibuild-0.2.6/src/dependencies/pytest.rs000064400000000000000000000272471046102023000167340ustar 00000000000000//! Support for pytest plugin dependencies. //! //! This module provides functionality for working with pytest plugin dependencies, //! including detecting and resolving plugins from fixture names, config options, //! and command-line arguments. use crate::dependencies::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a pytest plugin. /// /// This represents a dependency on a pytest plugin that needs to be installed /// for tests to run correctly. pub struct PytestPluginDependency { /// The name of the pytest plugin. pub plugin: String, } impl PytestPluginDependency { /// Create a new pytest plugin dependency. /// /// # Arguments /// * `plugin` - The name of the pytest plugin /// /// # Returns /// A new PytestPluginDependency pub fn new(plugin: &str) -> Self { Self { plugin: plugin.to_string(), } } } /// Map pytest command-line arguments to a plugin name. /// /// # Arguments /// * `args` - The command-line arguments to pytest /// /// # Returns /// The name of the required plugin, if any fn map_pytest_arguments_to_plugin(args: &[&str]) -> Option<&'static str> { for arg in args { if arg.starts_with("--cov") { return Some("cov"); } } None } /// Map a pytest config option to a plugin name. /// /// # Arguments /// * `name` - The name of the config option /// /// # Returns /// The name of the required plugin, if any fn map_pytest_config_option_to_plugin(name: &str) -> Option<&'static str> { match name { "asyncio_mode" => Some("asyncio"), n => { log::warn!("Unknown pytest config option {}", n); None } } } // TODO(jelmer): populate this using an automated process /// Map a pytest fixture name to the plugin that provides it. /// /// # Arguments /// * `fixture` - The name of the pytest fixture /// /// # Returns /// The name of the plugin that provides the fixture, if known fn pytest_fixture_to_plugin(fixture: &str) -> Option<&str> { match fixture { "aiohttp_client" => Some("aiohttp"), "aiohttp_client_cls" => Some("aiohttp"), "aiohttp_server" => Some("aiohttp"), "aiohttp_raw_server" => Some("aiohttp"), "mock" => Some("mock"), "benchmark" => Some("benchmark"), "event_loop" => Some("asyncio"), "unused_tcp_port" => Some("asyncio"), "unused_udp_port" => Some("asyncio"), "unused_tcp_port_factory" => Some("asyncio"), "unused_udp_port_factory" => Some("asyncio"), _ => None, } } /// Get a list of installed pytest plugins from the pytest command. /// /// # Arguments /// * `session` - The session to run the command in /// /// # Returns /// A list of (plugin_name, version) pairs if available fn pytest_plugins(session: &dyn Session) -> Option> { let output = match session .command(vec!["pytest", "--version"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::null()) .output() { Ok(output) => output, Err(crate::session::Error::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => { // pytest is not installed return None; } Err(e) => { // Other errors should panic panic!("Failed to run pytest: {:?}", e); } }; for line in String::from_utf8(output.stdout).unwrap().lines() { if let Some(rest) = line.strip_prefix("plugins: ") { return Some( rest.split(',') .map(|s| { let mut parts = s.splitn(2, '='); ( parts.next().unwrap().to_string(), parts.next().unwrap_or("").to_string(), ) }) .collect(), ); } } None } impl Dependency for PytestPluginDependency { fn family(&self) -> &'static str { "pytest-plugin" } fn present(&self, session: &dyn Session) -> bool { if let Some(plugins) = pytest_plugins(session) { plugins.iter().any(|(name, _)| name == &self.plugin) } else { false } } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PytestPluginDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(vec![crate::dependencies::debian::DebianDependency::simple( &format!("python3-pytest-{}", self.plugin), )]) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPytestFixture { fn to_dependency(&self) -> Option> { pytest_fixture_to_plugin(&self.0) .map(|plugin| Box::new(PytestPluginDependency::new(plugin)) as Box) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::UnsupportedPytestArguments { fn to_dependency(&self) -> Option> { let args = self.0.iter().map(|x| x.as_str()).collect::>(); map_pytest_arguments_to_plugin(args.as_slice()) .map(|plugin| Box::new(PytestPluginDependency::new(plugin)) as Box) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::UnsupportedPytestConfigOption { fn to_dependency(&self) -> Option> { map_pytest_config_option_to_plugin(&self.0) .map(|plugin| Box::new(PytestPluginDependency::new(plugin)) as Box) } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; #[test] fn test_pytest_plugin_dependency_new() { let dependency = PytestPluginDependency::new("cov"); assert_eq!(dependency.plugin, "cov"); } #[test] fn test_pytest_plugin_dependency_family() { let dependency = PytestPluginDependency::new("cov"); assert_eq!(dependency.family(), "pytest-plugin"); } #[test] fn test_pytest_plugin_dependency_as_any() { let dependency = PytestPluginDependency::new("cov"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_map_pytest_arguments_to_plugin() { assert_eq!(map_pytest_arguments_to_plugin(&["--cov"]), Some("cov")); assert_eq!( map_pytest_arguments_to_plugin(&["--cov-report=html"]), Some("cov") ); assert_eq!(map_pytest_arguments_to_plugin(&["--xvs"]), None); } #[test] fn test_map_pytest_config_option_to_plugin() { assert_eq!( map_pytest_config_option_to_plugin("asyncio_mode"), Some("asyncio") ); assert_eq!(map_pytest_config_option_to_plugin("unknown_option"), None); } #[test] fn test_pytest_fixture_to_plugin() { assert_eq!(pytest_fixture_to_plugin("aiohttp_client"), Some("aiohttp")); assert_eq!(pytest_fixture_to_plugin("benchmark"), Some("benchmark")); assert_eq!(pytest_fixture_to_plugin("event_loop"), Some("asyncio")); assert_eq!(pytest_fixture_to_plugin("unknown_fixture"), None); } #[test] fn test_missing_pytest_fixture_to_dependency() { let problem = buildlog_consultant::problems::common::MissingPytestFixture("event_loop".to_string()); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "pytest-plugin"); let pytest_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(pytest_dep.plugin, "asyncio"); } #[test] fn test_missing_pytest_fixture_to_dependency_unknown() { let problem = buildlog_consultant::problems::common::MissingPytestFixture( "unknown_fixture".to_string(), ); let dependency = problem.to_dependency(); assert!(dependency.is_none()); } #[test] fn test_unsupported_pytest_arguments_to_dependency() { let problem = buildlog_consultant::problems::common::UnsupportedPytestArguments(vec![ "--cov".to_string(), "--cov-report=html".to_string(), ]); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "pytest-plugin"); let pytest_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(pytest_dep.plugin, "cov"); } #[test] fn test_unsupported_pytest_arguments_to_dependency_unknown() { let problem = buildlog_consultant::problems::common::UnsupportedPytestArguments(vec![ "--unknown".to_string(), ]); let dependency = problem.to_dependency(); assert!(dependency.is_none()); } #[test] fn test_unsupported_pytest_config_option_to_dependency() { let problem = buildlog_consultant::problems::common::UnsupportedPytestConfigOption( "asyncio_mode".to_string(), ); let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "pytest-plugin"); let pytest_dep = dep .as_any() .downcast_ref::() .unwrap(); assert_eq!(pytest_dep.plugin, "asyncio"); } #[test] fn test_unsupported_pytest_config_option_to_dependency_unknown() { let problem = buildlog_consultant::problems::common::UnsupportedPytestConfigOption( "unknown_option".to_string(), ); let dependency = problem.to_dependency(); assert!(dependency.is_none()); } #[test] fn test_pytest_plugins_no_pytest() { use crate::session::plain::PlainSession; use tempfile::TempDir; let _temp_dir = TempDir::new().unwrap(); let session = PlainSession::new(); // When pytest is not available, pytest_plugins should return None let result = pytest_plugins(&session); assert!(result.is_none()); } #[test] fn test_pytest_plugins_parsing() { // We can't easily test the actual pytest_plugins function without having pytest installed // But we can test the parsing logic by testing the components // Test plugin name parsing logic let test_line = "plugins: cov-2.10.1, xdist-2.3.0, mock-3.6.1"; if let Some(rest) = test_line.strip_prefix("plugins: ") { let parsed: Vec<(String, String)> = rest .split(',') .map(|s| { let s = s.trim(); let mut parts = s.splitn(2, '-'); ( parts.next().unwrap().to_string(), parts.next().unwrap_or("").to_string(), ) }) .collect(); assert_eq!(parsed.len(), 3); assert_eq!(parsed[0].0, "cov"); assert_eq!(parsed[0].1, "2.10.1"); assert_eq!(parsed[1].0, "xdist"); assert_eq!(parsed[1].1, "2.3.0"); assert_eq!(parsed[2].0, "mock"); assert_eq!(parsed[2].1, "3.6.1"); } } } ognibuild-0.2.6/src/dependencies/python.rs000064400000000000000000001070531046102023000167170ustar 00000000000000//! Support for Python package dependencies. //! //! This module provides functionality for working with Python package dependencies, //! including parsing and resolving PEP 508 package requirements, and integrating //! with package managers. #[cfg(feature = "debian")] use crate::debian::apt::AptManager; #[cfg(feature = "debian")] use crate::dependencies::debian::DebianDependency; use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; #[cfg(feature = "debian")] use debian_control::{ lossless::relations::{Relation, Relations}, relations::VersionConstraint, }; use pep508_rs::pep440_rs; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Python package. /// /// This represents a dependency on a Python package from PyPI or another package /// repository. It uses PEP 508 requirement syntax for expressing version constraints. pub struct PythonPackageDependency(pep508_rs::Requirement); impl From for PythonPackageDependency { fn from(requirement: pep508_rs::Requirement) -> Self { Self(requirement) } } impl TryFrom for pep508_rs::Requirement { type Error = pep508_rs::Pep508Error; fn try_from(value: PythonPackageDependency) -> Result { Ok(value.0) } } impl TryFrom for PythonPackageDependency { type Error = pep508_rs::Pep508Error; fn try_from(value: String) -> Result { Self::try_from(value.as_str()) } } impl TryFrom<&str> for PythonPackageDependency { type Error = pep508_rs::Pep508Error; fn try_from(value: &str) -> Result { use std::str::FromStr; let req = pep508_rs::Requirement::from_str(value)?; Ok(PythonPackageDependency(req)) } } impl PythonPackageDependency { /// Get the package name. /// /// # Returns /// The name of the Python package pub fn package(&self) -> String { self.0.name.to_string() } /// Create a new dependency with a minimum version requirement. /// /// # Arguments /// * `package` - The name of the Python package /// * `min_version` - The minimum version required /// /// # Returns /// A new PythonPackageDependency pub fn new_with_min_version(package: &str, min_version: &str) -> Self { Self(pep508_rs::Requirement { name: pep508_rs::PackageName::new(package.to_string()).unwrap(), version_or_url: Some(min_version_as_version_or_url(min_version)), extras: vec![], marker: pep508_rs::MarkerTree::TRUE, origin: None, }) } /// Create a simple dependency with no version constraints. /// /// # Arguments /// * `package` - The name of the Python package /// /// # Returns /// A new PythonPackageDependency pub fn simple(package: &str) -> Self { Self(pep508_rs::Requirement { name: pep508_rs::PackageName::new(package.to_string()).unwrap(), version_or_url: None, extras: vec![], marker: pep508_rs::MarkerTree::TRUE, origin: None, }) } } /// Convert a minimum version string to a PEP 508 VersionOrUrl. /// /// # Arguments /// * `min_version` - The minimum version string /// /// # Returns /// A PEP 508 VersionOrUrl with a >= constraint fn min_version_as_version_or_url(min_version: &str) -> pep508_rs::VersionOrUrl { use std::str::FromStr; let version_specifiers = std::iter::once( pep440_rs::VersionSpecifier::from_pattern( pep440_rs::Operator::GreaterThanEqual, pep440_rs::VersionPattern::verbatim(pep440_rs::Version::from_str(min_version).unwrap()), ) .unwrap(), ) .collect(); pep508_rs::VersionOrUrl::VersionSpecifier(version_specifiers) } /// Create a PEP 508 marker for a specific Python major version. /// /// # Arguments /// * `major_version` - The Python major version (e.g., 2 or 3) /// /// # Returns /// A PEP 508 MarkerTree that requires the specified Python version fn major_python_version_as_marker(major_version: u32) -> pep508_rs::MarkerTree { pep508_rs::MarkerTree::expression(pep508_rs::MarkerExpression::Version { key: pep508_rs::MarkerValueVersion::PythonVersion, specifier: pep440_rs::VersionSpecifier::equals_star_version(pep440_rs::Version::new([ major_version as u64, ])), }) } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPythonDistribution { fn to_dependency(&self) -> Option> { let version_or_url = self .minimum_version .as_ref() .map(|min_version| min_version_as_version_or_url(min_version)); let marker = self .python_version .as_ref() .map(|python_major_version| { major_python_version_as_marker(*python_major_version as u32) }) .unwrap_or(pep508_rs::MarkerTree::TRUE); let requirement = pep508_rs::Requirement { name: pep508_rs::PackageName::new(self.distribution.clone()).unwrap(), version_or_url, extras: vec![], marker, origin: None, }; Some(Box::new(PythonPackageDependency::from(requirement))) } } #[cfg(feature = "debian")] impl crate::dependencies::debian::FromDebianDependency for PythonPackageDependency { fn from_debian_dependency(dependency: &DebianDependency) -> Option> { // TODO: handle other things than min version let (name, min_version) = crate::dependencies::debian::extract_simple_min_version(dependency)?; let (_, python_version, name) = lazy_regex::regex_captures!("python([0-9.]*)-(.*)", &name)?; let major_python_version = if python_version.is_empty() { None } else { Some(python_version.parse::().unwrap()) }; Some(Box::new(PythonPackageDependency::from( pep508_rs::Requirement { name: pep508_rs::PackageName::new(name.to_owned()).unwrap(), version_or_url: min_version .map(|x| min_version_as_version_or_url(&x.upstream_version)), marker: major_python_version .map(major_python_version_as_marker) .unwrap_or(pep508_rs::MarkerTree::TRUE), extras: vec![], origin: None, }, ))) } } #[derive(Debug, Clone, Default, Copy, Serialize, Deserialize)] /// Supported Python implementations and versions. /// /// This enum represents the different Python implementations and versions /// that can be used to satisfy Python package dependencies. pub enum PythonVersion { /// CPython 2.x CPython2, /// CPython 3.x (default) #[default] CPython3, /// PyPy (Python 2 compatible) PyPy, /// PyPy (Python 3 compatible) PyPy3, } impl PythonVersion { /// Get the executable name for this Python version. /// /// # Returns /// The name of the Python executable pub fn executable(&self) -> &'static str { match self { PythonVersion::CPython2 => "python2", PythonVersion::CPython3 => "python3", PythonVersion::PyPy => "pypy", PythonVersion::PyPy3 => "pypy3", } } } impl Dependency for PythonPackageDependency { fn family(&self) -> &'static str { "python-package" } fn present(&self, session: &dyn Session) -> bool { let python_version = find_python_version(self.0.marker.to_dnf()).unwrap_or_default(); let cmd = python_version.executable(); session .command(vec![ cmd, "-c", &format!( r#"import pkg_resources; pkg_resources.require("""{}""")"#, self.0 ), ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { // TODO: check in the virtualenv, if any todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "upstream")] impl crate::upstream::FindUpstream for PythonPackageDependency { fn find_upstream(&self) -> Option { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(upstream_ontologist::providers::python::remote_pypi_metadata(&self.package())) .ok() } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on a Python module. /// /// This represents a dependency on a specific Python module (importable name) /// rather than a package. Used for checking if specific imports will work. pub struct PythonModuleDependency { module: String, minimum_version: Option, python_version: Option, } impl PythonModuleDependency { /// Create a new Python module dependency. /// /// # Arguments /// * `module` - The name of the module to import /// * `minimum_version` - Optional minimum version requirement /// * `python_version` - Optional specific Python version to use /// /// # Returns /// A new PythonModuleDependency pub fn new( module: &str, minimum_version: Option<&str>, python_version: Option, ) -> Self { Self { module: module.to_string(), minimum_version: minimum_version.map(|s| s.to_string()), python_version, } } /// Create a simple Python module dependency with no version constraints. /// /// # Arguments /// * `module` - The name of the module to import /// /// # Returns /// A new PythonModuleDependency with no version constraints pub fn simple(module: &str) -> Self { Self { module: module.to_string(), minimum_version: None, python_version: None, } } /// Get the Python executable to use for this dependency. /// /// # Returns /// The name of the Python executable fn python_executable(&self) -> &str { self.python_version.unwrap_or_default().executable() } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingPythonModule { fn to_dependency(&self) -> Option> { Some(Box::new(PythonModuleDependency::new( &self.module, self.minimum_version.as_deref(), match self.python_version { Some(2) => Some(PythonVersion::CPython2), Some(3) => Some(PythonVersion::CPython3), None => None, _ => unimplemented!(), }, ))) } } impl Dependency for PythonModuleDependency { fn family(&self) -> &'static str { "python-module" } fn present(&self, session: &dyn Session) -> bool { let cmd = [ self.python_executable().to_string(), "-c".to_string(), format!( r#"import pkgutil; exit(0 if pkgutil.find_loader("{}") else 1)"#, self.module ), ]; session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { false } fn as_any(&self) -> &dyn std::any::Any { self } } /// Resolver for Python packages using pip. /// /// This resolver installs Python packages from PyPI using pip. pub struct PypiResolver<'a> { session: &'a dyn Session, } impl<'a> PypiResolver<'a> { /// Create a new PypiResolver with the specified session. /// /// # Arguments /// * `session` - The session to use for executing commands pub fn new(session: &'a dyn Session) -> Self { Self { session } } /// Generate the pip command for installing the specified requirements. /// /// # Arguments /// * `reqs` - The Python package dependencies to install /// * `scope` - The installation scope (user or global) /// /// # Returns /// The pip command as a vector of strings pub fn cmd( &self, reqs: Vec<&PythonPackageDependency>, scope: InstallationScope, ) -> Result, Error> { let mut cmd = vec!["pip".to_string(), "install".to_string()]; match scope { InstallationScope::User => cmd.push("--user".to_string()), InstallationScope::Global => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } cmd.extend(reqs.iter().map(|req| req.package().to_string())); Ok(cmd) } } impl<'a> Installer for PypiResolver<'a> { fn install(&self, requirement: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let req = requirement .as_any() .downcast_ref::() .ok_or_else(|| Error::UnknownDependencyFamily)?; let args = self.cmd(vec![req], scope)?; let mut cmd = self .session .command(args.iter().map(|x| x.as_str()).collect()); match scope { InstallationScope::Global => { cmd = cmd.user("root"); } InstallationScope::User => {} InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } cmd.run_detecting_problems()?; Ok(()) } fn explain( &self, requirement: &dyn Dependency, scope: InstallationScope, ) -> Result { let req = requirement .as_any() .downcast_ref::() .ok_or_else(|| Error::UnknownDependencyFamily)?; let cmd = self.cmd(vec![req], scope)?; Ok(Explanation { message: format!("Install pip {}", req.0.name), command: Some(cmd), }) } } #[cfg(feature = "debian")] /// Convert Python version specifiers to Debian package version constraints. /// /// # Arguments /// * `pkg_name` - The name of the Debian package /// * `version_specifiers` - The Python version specifiers (PEP 440) /// /// # Returns /// Debian package relations with appropriate version constraints pub fn python_version_specifiers_to_debian( pkg_name: &str, version_specifiers: Option<&pep440_rs::VersionSpecifiers>, ) -> Relations { // TODO(jelmer): Dealing with epoch, etc? let mut rels: Vec = vec![]; if let Some(version_specifiers) = version_specifiers { for vs in version_specifiers.iter() { let v = vs.version().to_string(); match vs.operator() { pep440_rs::Operator::TildeEqual => { // PEP 440: For a given release identifier V.N , the compatible // release clause is approximately equivalent to the pair of // comparison clauses: >= V.N, == V.* let mut parts = v.split('.').map(|s| s.to_string()).collect::>(); parts.pop(); let last: isize = parts.pop().unwrap().parse().unwrap(); parts.push((last + 1).to_string()); let next_maj_deb_version: debversion::Version = parts.join(".").parse().unwrap(); let deb_version: debversion::Version = v.parse().unwrap(); rels.push(Relation::new( pkg_name, Some((VersionConstraint::GreaterThanEqual, deb_version)), )); rels.push(Relation::new( pkg_name, Some((VersionConstraint::LessThan, next_maj_deb_version)), )); } pep440_rs::Operator::NotEqual => { let deb_version: debversion::Version = v.parse().unwrap(); rels.push(Relation::new( pkg_name, Some((VersionConstraint::GreaterThan, deb_version.clone())), )); rels.push(Relation::new( pkg_name, Some((VersionConstraint::LessThan, deb_version)), )); } pep440_rs::Operator::Equal if v.ends_with(".*") => { let mut parts = v.split('.').map(|s| s.to_string()).collect::>(); parts.pop(); let last: isize = parts.pop().unwrap().parse().unwrap(); parts.push((last + 1).to_string()); let deb_version: debversion::Version = v.parse().unwrap(); let next_maj_deb_version: debversion::Version = parts.join(".").parse().unwrap(); rels.push(Relation::new( pkg_name, Some((VersionConstraint::GreaterThanEqual, deb_version)), )); rels.push(Relation::new( pkg_name, Some((VersionConstraint::LessThan, next_maj_deb_version)), )); } o => { let vc = match o { pep440_rs::Operator::GreaterThanEqual => { VersionConstraint::GreaterThanEqual } pep440_rs::Operator::GreaterThan => VersionConstraint::GreaterThan, pep440_rs::Operator::LessThanEqual => VersionConstraint::LessThanEqual, pep440_rs::Operator::LessThan => VersionConstraint::LessThan, pep440_rs::Operator::Equal => VersionConstraint::Equal, _ => unimplemented!(), }; let v: debversion::Version = v.parse().unwrap(); rels.push(Relation::new(pkg_name, Some((vc, v)))); } } } Relations::from(rels.into_iter().map(|r| r.into()).collect::>()) } else { Relations::from(vec![Relation::new(pkg_name, None).into()]) } } fn find_python_version(marker: Vec>) -> Option { let mut major_version = None; let mut implementation = None; for expr in marker.iter().flat_map(|x| x.iter()) { match expr { pep508_rs::MarkerExpression::Version { key: pep508_rs::MarkerValueVersion::PythonVersion, specifier, } => { let version = specifier.version(); major_version = Some(version.release()[0] as u32); } pep508_rs::MarkerExpression::String { key: pep508_rs::MarkerValueString::PlatformPythonImplementation, operator: pep508_rs::MarkerOperator::Equal, value, } => { if value.as_str() == "PyPy" { implementation = Some("PyPy"); } } _ => {} } } match (major_version, implementation) { (Some(2), None) => Some(PythonVersion::CPython2), (Some(3), None) | (None, None) => Some(PythonVersion::CPython3), (Some(3), Some("PyPy")) | (None, Some("PyPy")) => Some(PythonVersion::PyPy3), (Some(2), Some("PyPy")) => Some(PythonVersion::PyPy), _ => { log::warn!( "Unknown python implementation / version: {:?} {:?}", major_version, implementation ); None } } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PythonPackageDependency { fn try_into_debian_dependency( &self, apt_mgr: &crate::debian::apt::AptManager, ) -> Option> { let names = get_package_for_python_package( apt_mgr, &self.package(), find_python_version(self.0.marker.to_dnf()), self.0.version_or_url.as_ref(), ); Some(names) } } #[cfg(feature = "debian")] fn get_package_for_python_package( apt_mgr: &AptManager, package: &str, python_version: Option, version_or_url: Option<&pep508_rs::VersionOrUrl>, ) -> Vec { let pypy_regex = format!( "/usr/lib/pypy/dist\\-packages/{}-.*\\.(dist|egg)\\-info", regex::escape(&package.replace('-', "_")) ); let cpython2_regex = format!( "/usr/lib/python2\\.[0-9]/dist\\-packages/{}-.*\\.(dist|egg)\\-info", regex::escape(&package.replace('-', "_")) ); let cpython3_regex = format!( "/usr/lib/python3/dist\\-packages/{}-.*\\.(dist|egg)\\-info", regex::escape(&package.replace('-', "_")) ); let paths = match python_version { Some(PythonVersion::PyPy) => vec![pypy_regex], Some(PythonVersion::CPython2) => vec![cpython2_regex], Some(PythonVersion::CPython3) => vec![cpython3_regex], None => vec![cpython3_regex, cpython2_regex, pypy_regex], _ => unimplemented!(), }; let names = apt_mgr .get_packages_for_paths(paths.iter().map(|x| x.as_str()).collect(), true, true) .unwrap(); names .iter() .map(|name| { DebianDependency::from(python_version_specifiers_to_debian( name, if let Some(pep508_rs::VersionOrUrl::VersionSpecifier(specs)) = version_or_url { Some(specs) } else { None }, )) }) .collect() } #[cfg(any(feature = "debian", test))] fn get_possible_python3_paths_for_python_object(mut object_path: &str) -> Vec { let mut cpython3_regexes = vec![]; loop { cpython3_regexes.extend([ Path::new("/usr/lib/python3/dist\\-packages") .join(regex::escape(&object_path.replace('.', "/"))) .join("__init__\\.py"), Path::new("/usr/lib/python3/dist\\-packages").join(format!( "{}\\.py", regex::escape(&object_path.replace('.', "/")) )), Path::new("/usr/lib/python3\\.[0-9]+/lib\\-dynload").join(format!( "{}\\.cpython\\-.*\\.so", regex::escape(&object_path.replace('.', "/")) )), Path::new("/usr/lib/python3\\.[0-9]+/").join(format!( "{}\\.py", regex::escape(&object_path.replace('.', "/")) )), Path::new("/usr/lib/python3\\.[0-9]+/") .join(regex::escape(&object_path.replace('.', "/"))) .join("__init__\\.py"), ]); object_path = match object_path.rsplit_once('.') { Some((o, _)) => o, None => break, }; } cpython3_regexes } #[cfg(feature = "debian")] fn get_possible_pypy_paths_for_python_object(mut object_path: &str) -> Vec { let mut pypy_regexes = vec![]; loop { pypy_regexes.extend([ Path::new("/usr/lib/pypy/dist\\-packages") .join(regex::escape(&object_path.replace('.', "/"))) .join("__init__\\.py"), Path::new("/usr/lib/pypy/dist\\-packages").join(format!( "{}\\.py", regex::escape(&object_path.replace('.', "/")) )), Path::new("/usr/lib/pypy/dist\\-packages").join(format!( "{}\\.pypy-.*\\.so", regex::escape(&object_path.replace('.', "/")) )), ]); object_path = match object_path.rsplit_once('.') { Some((o, _)) => o, None => break, }; } pypy_regexes } #[cfg(feature = "debian")] fn get_possible_python2_paths_for_python_object(mut object_path: &str) -> Vec { let mut cpython2_regexes = vec![]; loop { cpython2_regexes.extend([ Path::new("/usr/lib/python2\\.[0-9]/dist\\-packages") .join(regex::escape(&object_path.replace('.', "/"))) .join("__init__\\.py"), Path::new("/usr/lib/python2\\.[0-9]/dist\\-packages").join(format!( "{}\\.py", regex::escape(&object_path.replace('.', "/")) )), Path::new("/usr/lib/python2.\\.[0-9]/lib\\-dynload").join(format!( "{}\\.so", regex::escape(&object_path.replace('.', "/")) )), ]); object_path = match object_path.rsplit_once('.') { Some((o, _)) => o, None => break, }; } cpython2_regexes } #[cfg(feature = "debian")] fn get_package_for_python_object_path( apt_mgr: &AptManager, object_path: &str, python_version: Option, version_specifiers: Option<&pep440_rs::VersionSpecifiers>, ) -> Vec { // Try to find the most specific file let paths = match python_version { Some(PythonVersion::CPython3) => get_possible_python3_paths_for_python_object(object_path), Some(PythonVersion::CPython2) => get_possible_python2_paths_for_python_object(object_path), Some(PythonVersion::PyPy) => get_possible_pypy_paths_for_python_object(object_path), None => get_possible_python3_paths_for_python_object(object_path) .into_iter() .chain(get_possible_python2_paths_for_python_object(object_path)) .chain(get_possible_pypy_paths_for_python_object(object_path)) .collect(), _ => unimplemented!(), }; let names = apt_mgr .get_packages_for_paths( paths.iter().map(|x| x.to_str().unwrap()).collect(), true, false, ) .unwrap(); names .into_iter() .map(|name| { DebianDependency::from(python_version_specifiers_to_debian( &name, version_specifiers, )) }) .collect() } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PythonModuleDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { use std::str::FromStr; let specs = self.minimum_version.as_ref().map(|min_version| { std::iter::once( pep440_rs::VersionSpecifier::from_pattern( pep440_rs::Operator::GreaterThanEqual, pep440_rs::VersionPattern::verbatim( pep440_rs::Version::from_str(min_version).unwrap(), ), ) .unwrap(), ) .collect() }); Some(get_package_for_python_object_path( apt, &self.module, self.python_version, specs.as_ref(), )) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingSetupPyCommand { fn to_dependency(&self) -> Option> { match self.0.as_str() { "test" => Some(Box::new(PythonPackageDependency::simple("setuptools"))), _ => None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on Python itself. /// /// This represents a dependency on the Python interpreter or development files. pub struct PythonDependency { /// The minimum Python version required, if any. pub min_version: Option, } impl PythonDependency { /// Create a new Python dependency with an optional minimum version. /// /// # Arguments /// * `min_version` - The minimum Python version required (e.g., "3.8") /// /// # Returns /// A new PythonDependency pub fn new(min_version: Option<&str>) -> Self { Self { min_version: min_version.map(|s| s.to_string()), } } /// Create a simple Python dependency with no version constraints. /// /// # Returns /// A new PythonDependency with no version constraints pub fn simple() -> Self { Self { min_version: None } } fn executable(&self) -> &str { match &self.min_version { Some(min_version) => { if min_version.starts_with("2") { "python" } else { "python3" } } None => "python3", } } } #[cfg(test)] mod python_dep_tests { use super::*; #[test] fn test_python_dependency_new() { let dependency = PythonDependency::new(Some("3.6")); assert_eq!(dependency.min_version, Some("3.6".to_string())); } #[test] fn test_python_dependency_simple() { let dependency = PythonDependency::simple(); assert_eq!(dependency.min_version, None); } #[test] fn test_python_dependency_family() { let dependency = PythonDependency::simple(); assert_eq!(dependency.family(), "python"); } #[test] fn test_python_dependency_executable_python3() { let dependency = PythonDependency::new(Some("3.6")); assert_eq!(dependency.executable(), "python3"); } #[test] fn test_python_dependency_executable_python2() { let dependency = PythonDependency::new(Some("2.7")); assert_eq!(dependency.executable(), "python"); } #[test] fn test_python_dependency_executable_default() { let dependency = PythonDependency::simple(); assert_eq!(dependency.executable(), "python3"); } #[test] fn test_python_dependency_from_specs() { use std::str::FromStr; let specs = pep440_rs::VersionSpecifiers::from_str(">=3.6").unwrap(); let dependency = PythonDependency::from(&specs); // The actual version might be "3.6" or "3.6.0" depending on the pep440_rs version, so we just check that it contains "3.6" assert!(dependency.min_version.is_some()); assert!(dependency.min_version.as_ref().unwrap().contains("3.6")); } #[test] fn test_python_dependency_from_specs_no_version() { use std::str::FromStr; let specs = pep440_rs::VersionSpecifiers::from_str("==3.6").unwrap(); let dependency = PythonDependency::from(&specs); assert_eq!(dependency.min_version, None); } } impl Dependency for PythonDependency { fn family(&self) -> &'static str { "python" } fn present(&self, session: &dyn Session) -> bool { let cmd = match self.min_version { Some(ref min_version) => vec![ self.executable().to_string(), "-c".to_string(), format!( "import sys; sys.exit(0 if sys.version_info >= ({}) else 1)", min_version.replace('.', ", ") ), ], None => vec![ PythonVersion::default().executable().to_string(), "-c".to_string(), "import sys; sys.exit(0 if sys.version_info >= (3, 0) else 1)".to_string(), ], }; session .command(cmd.iter().map(|s| s.as_str()).collect()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, session: &dyn Session) -> bool { // Check if a virtualenv is present session.exists(Path::new("bin/python")) } fn as_any(&self) -> &dyn std::any::Any { self } } impl From<&pep440_rs::VersionSpecifiers> for PythonDependency { fn from(specs: &pep440_rs::VersionSpecifiers) -> Self { for specifier in specs.iter() { if specifier.operator() == &pep440_rs::Operator::GreaterThanEqual { return Self { min_version: Some(specifier.version().to_string()), }; } } Self { min_version: None } } } #[cfg(feature = "debian")] impl crate::dependencies::debian::FromDebianDependency for PythonDependency { fn from_debian_dependency(dependency: &DebianDependency) -> Option> { let (name, min_version) = crate::dependencies::debian::extract_simple_min_version(dependency)?; if name == "python" || name == "python3" { Some(Box::new(PythonDependency { min_version: min_version.map(|x| x.upstream_version.clone()), })) } else { None } } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for PythonDependency { fn try_into_debian_dependency( &self, _apt: &crate::debian::apt::AptManager, ) -> Option> { let mut deps = vec![]; if let Some(min_version) = &self.min_version { if min_version.starts_with("2") { deps.push( crate::dependencies::debian::DebianDependency::new_with_min_version( "python", &min_version.parse::().unwrap(), ), ); } else { deps.push( crate::dependencies::debian::DebianDependency::new_with_min_version( "python3", &min_version.parse::().unwrap(), ), ); } } else { deps.push(crate::dependencies::debian::DebianDependency::simple( "python3", )); } Some(deps) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_paths() { use std::path::PathBuf; assert_eq!( vec![ PathBuf::from("/usr/lib/python3/dist\\-packages/dulwich/__init__\\.py"), PathBuf::from("/usr/lib/python3/dist\\-packages/dulwich\\.py"), PathBuf::from( "/usr/lib/python3\\.[0-9]+/lib\\-dynload/dulwich\\.cpython\\-.*\\.so" ), PathBuf::from("/usr/lib/python3\\.[0-9]+/dulwich\\.py"), PathBuf::from("/usr/lib/python3\\.[0-9]+/dulwich/__init__\\.py"), ], get_possible_python3_paths_for_python_object("dulwich"), ); assert_eq!( vec![ PathBuf::from("/usr/lib/python3/dist\\-packages/cleo/foo/__init__\\.py"), PathBuf::from("/usr/lib/python3/dist\\-packages/cleo/foo\\.py"), PathBuf::from( "/usr/lib/python3\\.[0-9]+/lib\\-dynload/cleo/foo\\.cpython\\-.*\\.so" ), PathBuf::from("/usr/lib/python3\\.[0-9]+/cleo/foo\\.py"), PathBuf::from("/usr/lib/python3\\.[0-9]+/cleo/foo/__init__\\.py"), PathBuf::from("/usr/lib/python3/dist\\-packages/cleo/__init__\\.py"), PathBuf::from("/usr/lib/python3/dist\\-packages/cleo\\.py"), PathBuf::from("/usr/lib/python3\\.[0-9]+/lib\\-dynload/cleo\\.cpython\\-.*\\.so"), PathBuf::from("/usr/lib/python3\\.[0-9]+/cleo\\.py"), PathBuf::from("/usr/lib/python3\\.[0-9]+/cleo/__init__\\.py"), ], get_possible_python3_paths_for_python_object("cleo.foo"), ); } } ognibuild-0.2.6/src/dependencies/r.rs000064400000000000000000000252661046102023000156440ustar 00000000000000//! Support for R package dependencies. //! //! This module provides functionality for working with R package dependencies, //! including parsing and resolving package requirements from CRAN and Bioconductor. use crate::dependency::Dependency; use crate::installer::{Error, Explanation, InstallationScope, Installer}; use crate::session::Session; use r_description::lossy::Relation; use r_description::VersionConstraint; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on an R package. /// /// This represents a dependency on an R package from CRAN, Bioconductor, or another repository. pub struct RPackageDependency(Relation); impl From for Relation { fn from(dep: RPackageDependency) -> Self { dep.0 } } impl From for RPackageDependency { fn from(rel: Relation) -> Self { Self(rel) } } impl From for RPackageDependency { fn from(rel: r_description::lossless::Relation) -> Self { Self(rel.into()) } } impl RPackageDependency { /// Create a new R package dependency with an optional minimum version. /// /// # Arguments /// * `package` - The name of the R package /// * `minimum_version` - Optional minimum version requirement /// /// # Returns /// A new RPackageDependency pub fn new(package: &str, minimum_version: Option<&str>) -> Self { if let Some(minimum_version) = minimum_version { Self(Relation { name: package.to_string(), version: Some(( VersionConstraint::GreaterThanEqual, minimum_version.parse().unwrap(), )), }) } else { Self(Relation { name: package.to_string(), version: None, }) } } /// Create a simple R package dependency with no version constraints. /// /// # Arguments /// * `package` - The name of the R package /// /// # Returns /// A new RPackageDependency with no version constraints pub fn simple(package: &str) -> Self { Self(Relation { name: package.to_string(), version: None, }) } /// Create an R package dependency from a string representation. /// /// # Arguments /// * `s` - String representation of the dependency (e.g., "dplyr (>= 1.0.0)") /// /// # Returns /// A new RPackageDependency parsed from the string pub fn from_str(s: &str) -> Self { if let Some((_, name, min_version)) = lazy_regex::regex_captures!("(.*) \\(>= (.*)\\)", s) { Self::new(name, Some(min_version)) } else if !s.contains(" ") { Self::simple(s) } else { panic!("Invalid R package dependency: {}", s); } } } impl Dependency for RPackageDependency { fn family(&self) -> &'static str { "r-package" } fn present(&self, _session: &dyn Session) -> bool { todo!() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for RPackageDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> Option> { let names = apt .get_packages_for_paths( vec![std::path::Path::new("/usr/lib/R/site-library") .join(&self.0.name) .join("DESCRIPTION") .to_str() .unwrap()], false, false, ) .unwrap(); if names.is_empty() { return None; } Some( names .into_iter() .map(|name| super::debian::DebianDependency::new(&name)) .collect(), ) } } /// A resolver for R package dependencies. /// /// This resolver installs R packages from repositories like CRAN and Bioconductor. pub struct RResolver<'a> { session: &'a dyn Session, repos: String, } impl<'a> RResolver<'a> { /// Create a new RResolver with the specified session and repository. /// /// # Arguments /// * `session` - The session to use for executing commands /// * `repos` - The R repository URL /// /// # Returns /// A new RResolver pub fn new(session: &'a dyn Session, repos: &str) -> Self { Self { session, repos: repos.to_string(), } } fn cmd(&self, req: &RPackageDependency) -> Vec { // R will install into the first directory in .libPaths() that is writeable. // TODO: explicitly set the library path to either the user's home directory or a system // directory. vec![ "R".to_string(), "-e".to_string(), format!("install.packages('{}', repos='{})'", req.0.name, self.repos), ] } } impl<'a> Installer for RResolver<'a> { /// Install the dependency into the session. fn install(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { let req = dep .as_any() .downcast_ref::() .ok_or(Error::UnknownDependencyFamily)?; let args = self.cmd(req); log::info!("RResolver({:?}): running {:?}", self.repos, args); let mut cmd = self .session .command(args.iter().map(|x| x.as_str()).collect()); match scope { InstallationScope::User => {} InstallationScope::Global => { cmd = cmd.user("root"); } InstallationScope::Vendor => { return Err(Error::UnsupportedScope(scope)); } } cmd.run_detecting_problems()?; Ok(()) } /// Explain how to install the dependency. fn explain( &self, dep: &dyn Dependency, _scope: InstallationScope, ) -> Result { if let Some(req) = dep.as_any().downcast_ref::() { Ok(Explanation { message: format!("Install R package {}", req.0.name), command: Some(self.cmd(req)), }) } else { Err(Error::UnknownDependencyFamily) } } } /// Create an RResolver for Bioconductor packages. /// /// # Arguments /// * `session` - The session to use for executing commands /// /// # Returns /// An RResolver configured for Bioconductor pub fn bioconductor(session: &dyn Session) -> RResolver<'_> { RResolver::new(session, "https://hedgehog.fhcrc.org/bioconductor") } /// Create an RResolver for CRAN packages. /// /// # Arguments /// * `session` - The session to use for executing commands /// /// # Returns /// An RResolver configured for CRAN pub fn cran(session: &dyn Session) -> RResolver<'_> { RResolver::new(session, "https://cran.r-project.org") } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingRPackage { fn to_dependency(&self) -> Option> { Some(Box::new(RPackageDependency::simple(&self.package))) } } #[cfg(test)] mod tests { use super::*; use crate::buildlog::ToDependency; #[test] fn test_r_package_dependency_new() { let dependency = RPackageDependency::new("dplyr", Some("1.0.0")); assert_eq!(dependency.0.name, "dplyr"); assert!(dependency.0.version.is_some()); let (constraint, version) = dependency.0.version.unwrap(); assert_eq!(constraint, VersionConstraint::GreaterThanEqual); assert_eq!(format!("{}", version), "1.0.0"); } #[test] fn test_r_package_dependency_simple() { let dependency = RPackageDependency::simple("dplyr"); assert_eq!(dependency.0.name, "dplyr"); assert!(dependency.0.version.is_none()); } #[test] fn test_r_package_dependency_from_str() { let dependency = RPackageDependency::from_str("dplyr (>= 1.0.0)"); assert_eq!(dependency.0.name, "dplyr"); assert!(dependency.0.version.is_some()); let (constraint, version) = dependency.0.version.unwrap(); assert_eq!(constraint, VersionConstraint::GreaterThanEqual); assert_eq!(format!("{}", version), "1.0.0"); let dependency = RPackageDependency::from_str("dplyr"); assert_eq!(dependency.0.name, "dplyr"); assert!(dependency.0.version.is_none()); } #[test] fn test_r_package_dependency_family() { let dependency = RPackageDependency::simple("dplyr"); assert_eq!(dependency.family(), "r-package"); } #[test] fn test_r_package_dependency_as_any() { let dependency = RPackageDependency::simple("dplyr"); let any_dep = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_missing_r_package_to_dependency() { let problem = buildlog_consultant::problems::common::MissingRPackage { package: "dplyr".to_string(), minimum_version: None, }; let dependency = problem.to_dependency(); assert!(dependency.is_some()); let dep = dependency.unwrap(); assert_eq!(dep.family(), "r-package"); let r_dep = dep.as_any().downcast_ref::().unwrap(); assert_eq!(r_dep.0.name, "dplyr"); } #[test] fn test_r_resolver_new() { let session = crate::session::plain::PlainSession::new(); let resolver = RResolver::new(&session, "https://cran.r-project.org"); assert_eq!(resolver.repos, "https://cran.r-project.org"); } #[test] fn test_r_resolver_cmd() { let session = crate::session::plain::PlainSession::new(); let resolver = RResolver::new(&session, "https://cran.r-project.org"); let dependency = RPackageDependency::simple("dplyr"); let cmd = resolver.cmd(&dependency); assert_eq!( cmd, vec![ "R", "-e", "install.packages('dplyr', repos='https://cran.r-project.org)'", ] ); } #[test] fn test_bioconductor() { let session = crate::session::plain::PlainSession::new(); let resolver = bioconductor(&session); assert_eq!(resolver.repos, "https://hedgehog.fhcrc.org/bioconductor"); } #[test] fn test_cran() { let session = crate::session::plain::PlainSession::new(); let resolver = cran(&session); assert_eq!(resolver.repos, "https://cran.r-project.org"); } } ognibuild-0.2.6/src/dependencies/vague.rs000064400000000000000000000275141046102023000165100ustar 00000000000000//! Support for vague dependencies that are not tied to a specific system. //! //! This module provides functionality for representing and resolving dependencies //! that are specified in a vague manner (e.g., "zlib" without specifying whether //! it's a binary, library, etc.). These dependencies are expanded into more //! specific dependencies when resolved. #[cfg(feature = "debian")] use crate::dependencies::debian::DebianDependency; #[cfg(feature = "debian")] use crate::dependencies::python::PythonPackageDependency; use crate::dependencies::BinaryDependency; use crate::dependencies::Dependency; use crate::dependencies::PkgConfigDependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency that is not tied to a specific system or package manager. /// /// This represents a dependency that could be satisfied in multiple ways. /// When resolved, it expands into multiple specific dependency types. pub struct VagueDependency { /// The name of the dependency. pub name: String, /// The minimum version required, if any. pub minimum_version: Option, } impl VagueDependency { /// Create a new VagueDependency with the given name and optional minimum version. /// /// # Arguments /// * `name` - The name of the dependency /// * `minimum_version` - Optional minimum version requirement pub fn new(name: &str, minimum_version: Option<&str>) -> Self { Self { name: name.to_string(), minimum_version: minimum_version.map(|s| s.trim().to_string()), } } /// Create a simple VagueDependency with just a name and no version requirement. /// /// # Arguments /// * `name` - The name of the dependency pub fn simple(name: &str) -> Self { Self { name: name.to_string(), minimum_version: None, } } /// Expand this vague dependency into more specific dependency types. /// /// This converts the vague dependency into specific dependency types such as /// binary dependencies, pkg-config dependencies, and Debian dependencies. /// /// # Returns /// A vector of specific dependency implementations pub fn expand(&self) -> Vec> { let mut ret: Vec> = vec![]; let lcname = self.name.to_lowercase(); if !self.name.contains(' ') { ret.push(Box::new(BinaryDependency::new(&self.name)) as Box); ret.push(Box::new(BinaryDependency::new(&self.name)) as Box); ret.push(Box::new(PkgConfigDependency::new( &self.name.clone(), self.minimum_version.clone().as_deref(), )) as Box); if lcname != self.name { ret.push(Box::new(BinaryDependency::new(&lcname)) as Box); ret.push(Box::new(BinaryDependency::new(&lcname)) as Box); ret.push(Box::new(PkgConfigDependency::new( &lcname, self.minimum_version.clone().as_deref(), )) as Box); } #[cfg(feature = "debian")] { ret.push(Box::new( if let Some(minimum_version) = &self.minimum_version { DebianDependency::new_with_min_version( &self.name, &minimum_version.parse().unwrap(), ) } else { DebianDependency::new(&self.name) }, )); let devname = if lcname.starts_with("lib") { format!("{}-dev", lcname) } else { format!("lib{}-dev", lcname) }; ret.push(if let Some(minimum_version) = &self.minimum_version { Box::new(DebianDependency::new_with_min_version( &devname, &minimum_version.parse().unwrap(), )) } else { Box::new(DebianDependency::new(&devname)) }); } } ret } } impl Dependency for VagueDependency { fn family(&self) -> &'static str { "vague" } fn present(&self, session: &dyn Session) -> bool { self.expand().iter().any(|d| d.present(session)) } fn project_present(&self, session: &dyn Session) -> bool { self.expand().iter().any(|d| d.project_present(session)) } fn as_any(&self) -> &dyn std::any::Any { self } } #[cfg(feature = "debian")] fn known_vague_dep_to_debian(name: &str) -> Option<&str> { match name { "the Gnu Scientific Library" => Some("libgsl-dev"), "the required FreeType library" => Some("libfreetype-dev"), "the Boost C++ libraries" => Some("libboost-dev"), "the sndfile library" => Some("libsndfile-dev"), // TODO(jelmer): Support resolving virtual packages "PythonLibs" => Some("libpython3-dev"), "PythonInterp" => Some("python3"), "ZLIB" => Some("libz3-dev"), "Osmium" => Some("libosmium2-dev"), "glib" => Some("libglib2.0-dev"), "OpenGL" => Some("libgl-dev"), // TODO(jelmer): For Python, check minimum_version and map to python 2 or python 3 "Python" => Some("libpython3-dev"), "Lua" => Some("liblua5.4-dev"), _ => None, } } #[cfg(feature = "debian")] fn resolve_vague_dep_req( apt_mgr: &crate::debian::apt::AptManager, req: VagueDependency, ) -> Vec { let name = req.name.as_str(); let mut options = vec![]; if name.contains(" or ") { for entry in name.split(" or ") { options.extend(resolve_vague_dep_req( apt_mgr, VagueDependency { name: entry.to_string(), minimum_version: req.minimum_version.clone(), }, )); } } if let Some(dep) = known_vague_dep_to_debian(name) { options.push( if let Some(minimum_version) = req.minimum_version.as_ref() { DebianDependency::new_with_min_version(dep, &minimum_version.parse().unwrap()) } else { DebianDependency::new(dep) }, ); } for x in req.expand() { options.extend(crate::debian::apt::dependency_to_possible_deb_dependencies( apt_mgr, x.as_ref(), )); } if let Some(rest) = name.strip_prefix("GNU ") { options.extend(resolve_vague_dep_req( apt_mgr, VagueDependency::simple(rest), )); } if name.starts_with("py") || name.ends_with("py") { // TODO(jelmer): Try harder to determine whether this is a python package let dep = if let Some(min_version) = req.minimum_version.as_ref() { PythonPackageDependency::new_with_min_version(name, min_version) } else { PythonPackageDependency::simple(name) }; options.extend(crate::debian::apt::dependency_to_possible_deb_dependencies( apt_mgr, &dep, )); } // Try even harder if options.is_empty() { use std::path::Path; let paths = [ Path::new("/usr/lib") .join(".*") .join("pkgconfig") .join(format!("{}-.*\\.pc", regex::escape(&req.name))), Path::new("/usr/lib/pkgconfig").join(format!("{}-.*\\.pc", regex::escape(&req.name))), ]; options.extend( apt_mgr .get_packages_for_paths( paths.iter().map(|x| x.to_str().unwrap()).collect(), true, true, ) .unwrap() .iter() .map(|x| DebianDependency::new(x)), ) } options } #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for VagueDependency { fn try_into_debian_dependency( &self, apt_mgr: &crate::debian::apt::AptManager, ) -> std::option::Option> { Some(resolve_vague_dep_req(apt_mgr, self.clone())) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingVagueDependency { fn to_dependency(&self) -> Option> { Some(Box::new(VagueDependency::new( &self.name, self.minimum_version.as_deref(), ))) } } #[cfg(test)] mod tests { use super::*; use std::any::Any; #[test] fn test_vague_dependency_new() { let dep = VagueDependency::new("zlib", Some("1.2.11")); assert_eq!(dep.name, "zlib"); assert_eq!(dep.minimum_version, Some("1.2.11".to_string())); } #[test] fn test_vague_dependency_new_trims_version() { let dep = VagueDependency::new("zlib", Some(" 1.2.11 ")); assert_eq!(dep.minimum_version, Some("1.2.11".to_string())); } #[test] fn test_vague_dependency_simple() { let dep = VagueDependency::simple("zlib"); assert_eq!(dep.name, "zlib"); assert_eq!(dep.minimum_version, None); } #[test] fn test_vague_dependency_family() { let dep = VagueDependency::simple("zlib"); assert_eq!(dep.family(), "vague"); } #[test] fn test_vague_dependency_as_any() { let dep = VagueDependency::simple("zlib"); let any_dep: &dyn Any = dep.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_vague_dependency_expand() { let dep = VagueDependency::simple("zlib"); let expanded = dep.expand(); // Should generate binary dependencies assert!(expanded.iter().any(|d| d.family() == "binary" && d.as_any() .downcast_ref::() .map(|bd| bd.binary_name == "zlib") .unwrap_or(false))); // Should generate pkg-config dependencies assert!(expanded.iter().any(|d| d.family() == "pkg-config" && d.as_any() .downcast_ref::() .map(|pd| pd.module == "zlib") .unwrap_or(false))); // Should also include lowercase versions assert!(expanded.iter().any(|d| d.family() == "binary" && d.as_any() .downcast_ref::() .map(|bd| bd.binary_name == "zlib") .unwrap_or(false))); } #[test] fn test_vague_dependency_expand_with_spaces() { let dep = VagueDependency::simple("zlib library"); let expanded = dep.expand(); // Should not expand dependencies with spaces assert!(expanded.is_empty()); } #[cfg(feature = "debian")] #[test] fn test_known_vague_dep_to_debian() { assert_eq!( known_vague_dep_to_debian("the Gnu Scientific Library"), Some("libgsl-dev") ); assert_eq!( known_vague_dep_to_debian("the required FreeType library"), Some("libfreetype-dev") ); assert_eq!( known_vague_dep_to_debian("the Boost C++ libraries"), Some("libboost-dev") ); assert_eq!( known_vague_dep_to_debian("PythonLibs"), Some("libpython3-dev") ); assert_eq!(known_vague_dep_to_debian("Python"), Some("libpython3-dev")); assert_eq!(known_vague_dep_to_debian("ZLIB"), Some("libz3-dev")); assert_eq!(known_vague_dep_to_debian("OpenGL"), Some("libgl-dev")); assert_eq!(known_vague_dep_to_debian("unknown_library"), None); assert_eq!(known_vague_dep_to_debian(""), None); } } ognibuild-0.2.6/src/dependencies/xml.rs000064400000000000000000000105561046102023000161770ustar 00000000000000//! Support for XML entity dependencies. //! //! This module provides functionality for working with XML entity dependencies, //! including checking if entities are defined in the local XML catalog and //! mapping between URLs and filesystem paths. use crate::dependencies::Dependency; use crate::session::Session; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] /// A dependency on an XML entity, such as a DocBook DTD. /// /// This represents a dependency on an XML entity, which is typically resolved /// through an XML catalog. pub struct XmlEntityDependency { url: String, } impl XmlEntityDependency { /// Create a new XML entity dependency with the specified URL. /// /// # Arguments /// * `url` - The URL of the XML entity /// /// # Returns /// A new XmlEntityDependency pub fn new(url: &str) -> Self { Self { url: url.to_string(), } } } impl Dependency for XmlEntityDependency { fn family(&self) -> &'static str { "xml-entity" } fn present(&self, session: &dyn Session) -> bool { // Check if the entity is defined in the local XML catalog session .command(vec!["xmlcatalog", "--noout", "--resolve", &self.url]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .run() .unwrap() .success() } fn project_present(&self, _session: &dyn Session) -> bool { todo!() } fn as_any(&self) -> &dyn std::any::Any { self } } /// Mapping between XML entity URLs and their filesystem locations. /// /// This constant maps from entity URLs to their corresponding filesystem paths, /// which is used to locate entities when resolving dependencies. pub const XML_ENTITY_URL_MAP: &[(&str, &str)] = &[( "http://www.oasis-open.org/docbook/xml/", "/usr/share/xml/docbook/schema/dtd/", )]; #[cfg(feature = "debian")] impl crate::dependencies::debian::IntoDebianDependency for XmlEntityDependency { fn try_into_debian_dependency( &self, apt: &crate::debian::apt::AptManager, ) -> std::option::Option> { let path = XML_ENTITY_URL_MAP.iter().find_map(|(url, path)| { self.url .strip_prefix(url) .map(|rest| format!("{}{}", path, rest)) }); path.as_ref()?; Some( apt.get_packages_for_paths(vec![path.as_ref().unwrap()], false, false) .unwrap() .iter() .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) .collect(), ) } } impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingXmlEntity { fn to_dependency(&self) -> Option> { Some(Box::new(XmlEntityDependency::new(&self.url))) } } #[cfg(test)] mod tests { use super::*; use std::any::Any; #[test] fn test_xml_entity_dependency_new() { let url = "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"; let dependency = XmlEntityDependency::new(url); assert_eq!(dependency.url, url); } #[test] fn test_xml_entity_dependency_family() { let dependency = XmlEntityDependency::new("http://www.example.com/entity"); assert_eq!(dependency.family(), "xml-entity"); } #[test] fn test_xml_entity_dependency_as_any() { let dependency = XmlEntityDependency::new("http://www.example.com/entity"); let any_dep: &dyn Any = dependency.as_any(); assert!(any_dep.downcast_ref::().is_some()); } #[test] fn test_xml_entity_url_map() { assert!(XML_ENTITY_URL_MAP .iter() .any(|(url, _)| *url == "http://www.oasis-open.org/docbook/xml/")); // Test that the URL map can be used to transform URLs let input_url = "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"; let expected_path = "/usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd"; let transformed = XML_ENTITY_URL_MAP.iter().find_map(|(url, path)| { input_url .strip_prefix(url) .map(|rest| format!("{}{}", path, rest)) }); assert_eq!(transformed, Some(expected_path.to_string())); } } ognibuild-0.2.6/src/dependency.rs000064400000000000000000000015361046102023000150650ustar 00000000000000use crate::session::Session; /// A dependency is a component that is required by a project to build or run. pub trait Dependency: std::fmt::Debug { /// Get the family of this dependency (e.g., "apt", "pip", etc.). /// /// # Returns /// A string identifying the dependency type family fn family(&self) -> &'static str; /// Check whether the dependency is present in the session. fn present(&self, session: &dyn Session) -> bool; /// Check whether the dependency is present in the project. fn project_present(&self, session: &dyn Session) -> bool; /// Convert this dependency to Any for dynamic casting. /// /// This method allows for conversion of the dependency to concrete types at runtime. /// /// # Returns /// A reference to this dependency as Any fn as_any(&self) -> &dyn std::any::Any; } ognibuild-0.2.6/src/dist.rs000064400000000000000000000133721046102023000137130ustar 00000000000000use crate::buildsystem::{detect_buildsystems, Error}; use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; use crate::fixers::*; use crate::installer::{ auto_installation_scope, auto_installer, Error as InstallerError, InstallationScope, }; use crate::logs::{wrap, LogManager}; use crate::session::Session; use std::ffi::OsString; use std::path::Path; /// Create a distribution package using the detected build system. /// /// # Arguments /// * `session` - The session to run commands in /// * `export_directory` - Directory to search for build systems /// * `reldir` - Relative directory to change to before building /// * `target_dir` - Directory to write distribution package to /// * `log_manager` - Log manager for capturing output /// * `version` - Optional version to use for the package /// * `quiet` - Whether to suppress output /// /// # Returns /// The filename of the created distribution package pub fn dist( session: &mut dyn Session, export_directory: &Path, reldir: &Path, target_dir: &Path, log_manager: &mut dyn LogManager, version: Option<&str>, quiet: bool, ) -> Result { session.chdir(reldir)?; if let Some(version) = version { // TODO(jelmer): Shouldn't include backend-specific code here std::env::set_var("SETUPTOOLS_SCM_PRETEND_VERSION", version); } // TODO(jelmer): use scan_buildsystems to also look in subdirectories let buildsystems = detect_buildsystems(export_directory); let scope = auto_installation_scope(session); let installer = auto_installer(session, scope, None); let mut fixers: Vec>> = vec![ Box::new(UnexpandedAutoconfMacroFixer::new( session, installer.as_ref(), )), Box::new(GnulibDirectoryFixer::new(session)), Box::new(MinimumAutoconfFixer::new(session)), Box::new(MissingGoSumEntryFixer::new(session)), Box::new(InstallFixer::new( installer.as_ref(), InstallationScope::User, )), ]; if session.is_temporary() { // Only muck about with temporary sessions fixers.extend([ Box::new(GitIdentityFixer::new(session)) as Box>, Box::new(SecretGpgKeyFixer::new(session)) as Box>, ]); } // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache session.create_home()?; if let Some(buildsystem) = buildsystems.into_iter().next() { return Ok(iterate_with_build_fixers( fixers .iter() .map(|x| x.as_ref()) .collect::>() .as_slice(), || -> Result<_, InterimError> { Ok(wrap(log_manager, || -> Result<_, Error> { buildsystem.dist(session, installer.as_ref(), target_dir, quiet) })?) }, None, )?); } Err(Error::NoBuildSystemDetected) } #[cfg(feature = "breezy")] // This is the function used by debianize() /// Create a dist tarball for a tree. /// /// # Arguments /// * `session` - session to run it /// * `tree` - Tree object to work in /// * `target_dir` - Directory to write tarball into /// * `include_controldir` - Whether to include the version control directory /// * `temp_subdir` - name of subdirectory in which to check out the source code; /// defaults to "package" pub fn create_dist( session: &mut dyn Session, tree: &T, target_dir: &Path, include_controldir: Option, log_manager: &mut dyn LogManager, version: Option<&str>, _subpath: &Path, temp_subdir: Option<&str>, ) -> Result { let temp_subdir = temp_subdir.unwrap_or("package"); let project = session.project_from_vcs(tree, include_controldir, Some(temp_subdir))?; dist( session, project.external_path(), project.internal_path(), target_dir, log_manager, version, false, ) } #[cfg(feature = "breezy")] #[cfg(target_os = "linux")] /// Create a dist tarball for a tree. /// /// # Arguments /// * `session` - session to run it /// * `tree` - Tree object to work in /// * `target_dir` - Directory to write tarball into /// * `include_controldir` - Whether to include the version control directory /// * `temp_subdir` - name of subdirectory in which to check out the source code; /// defaults to "package" pub fn create_dist_schroot( tree: &T, target_dir: &Path, chroot: &str, packaging_tree: Option<&dyn breezyshim::tree::Tree>, packaging_subpath: Option<&Path>, include_controldir: Option, subpath: &Path, log_manager: &mut dyn LogManager, version: Option<&str>, temp_subdir: Option<&str>, ) -> Result { // TODO(jelmer): pass in package name as part of session prefix let mut session = crate::session::schroot::SchrootSession::new(chroot, Some("ognibuild-dist"))?; #[cfg(feature = "debian")] if let (Some(packaging_tree), Some(packaging_subpath)) = (packaging_tree, packaging_subpath) { crate::debian::satisfy_build_deps(&session, packaging_tree, packaging_subpath) .map_err(|e| Error::Other(format!("Failed to satisfy build dependencies: {:?}", e)))?; } #[cfg(not(feature = "debian"))] if packaging_tree.is_some() || packaging_subpath.is_some() { log::warn!("Ignoring packaging tree and subpath as debian feature is not enabled"); } create_dist( &mut session, tree, target_dir, include_controldir, log_manager, version, subpath, temp_subdir, ) } ognibuild-0.2.6/src/dist_catcher.rs000064400000000000000000000122451046102023000154020ustar 00000000000000use std::collections::HashMap; use std::ffi::OsString; use std::path::{Path, PathBuf}; /// List of supported distribution file extensions. pub const SUPPORTED_DIST_EXTENSIONS: &[&str] = &[ ".tar.gz", ".tgz", ".tar.bz2", ".tar.xz", ".tar.lzma", ".tbz2", ".tar", ".zip", ]; /// Check if a file has a supported distribution extension. pub fn supported_dist_file(file: &Path) -> bool { SUPPORTED_DIST_EXTENSIONS .iter() .any(|&ext| file.ends_with(ext)) } /// Utility to detect and collect distribution files created by build systems. /// /// This monitors directories for new or updated distribution files that appear /// after a build process runs. pub struct DistCatcher { existing_files: Option>>, directories: Vec, files: std::sync::Mutex>, start_time: std::time::SystemTime, } impl DistCatcher { /// Create a new DistCatcher to monitor the specified directories. pub fn new(directories: Vec) -> Self { Self { directories: directories .iter() .map(|d| d.canonicalize().unwrap()) .collect(), files: std::sync::Mutex::new(Vec::new()), start_time: std::time::SystemTime::now(), existing_files: None, } } /// Create a DistCatcher with default directory locations. pub fn default(directory: &Path) -> Self { Self::new(vec![ directory.join("dist"), directory.to_path_buf(), directory.join(".."), ]) } /// Initialize the file monitoring process. /// /// Takes a snapshot of existing files to later detect new or modified files. pub fn start(&mut self) { self.existing_files = Some( self.directories .iter() .map(|d| { let mut map = HashMap::new(); for entry in d.read_dir().unwrap() { let entry = entry.unwrap(); map.insert(entry.path(), entry); } (d.clone(), map) }) .collect(), ); } /// Search for new or updated distribution files. /// /// Returns the path to a found file if any. pub fn find_files(&self) -> Option { let existing_files = self.existing_files.as_ref().unwrap(); let mut files = self.files.lock().unwrap(); for directory in &self.directories { let old_files = existing_files.get(directory).unwrap(); let mut possible_new = Vec::new(); let mut possible_updated = Vec::new(); if !directory.is_dir() { continue; } for entry in directory.read_dir().unwrap() { let entry = entry.unwrap(); if !entry.file_type().unwrap().is_file() || !supported_dist_file(&entry.path()) { continue; } let old_entry = old_files.get(&entry.path()); if old_entry.is_none() { possible_new.push(entry); continue; } if entry.metadata().unwrap().modified().unwrap() > self.start_time { possible_updated.push(entry); continue; } } if possible_new.len() == 1 { let entry = possible_new[0].path(); log::info!("Found new tarball {:?} in {:?}", entry, directory); files.push(entry.clone()); return Some(entry); } else if possible_new.len() > 1 { log::warn!( "Found multiple tarballs {:?} in {:?}", possible_new.iter().map(|e| e.path()).collect::>(), directory ); files.extend(possible_new.iter().map(|e| e.path())); return Some(possible_new[0].path()); } if possible_updated.len() == 1 { let entry = possible_updated[0].path(); log::info!("Found updated tarball {:?} in {:?}", entry, directory); files.push(entry.clone()); return Some(entry); } } None } /// Copy a single distribution file to the target directory. /// /// Returns the filename of the copied file if successful. pub fn copy_single(&self, target_dir: &Path) -> Result, std::io::Error> { for path in self.files.lock().unwrap().iter() { match std::fs::copy(path, target_dir.join(path.file_name().unwrap())) { Ok(_) => return Ok(Some(path.file_name().unwrap().into())), Err(e) => { if e.kind() == std::io::ErrorKind::AlreadyExists { continue; } return Err(e); } } } log::info!("No tarball created :("); Err(std::io::Error::new( std::io::ErrorKind::NotFound, "No tarball found", )) } } ognibuild-0.2.6/src/fix_build.rs000064400000000000000000000227171046102023000147200ustar 00000000000000use buildlog_consultant::{Match, Problem}; use log::{info, warn}; use std::fmt::{Debug, Display}; /// A fixer is a struct that can resolve a specific type of problem. pub trait BuildFixer: std::fmt::Debug + std::fmt::Display { /// Check if this fixer can potentially resolve the given problem. fn can_fix(&self, problem: &dyn Problem) -> bool; /// Attempt to resolve the given problem. fn fix(&self, problem: &dyn Problem) -> Result>; } #[derive(Debug)] /// Intermediate error type used during build fixing. /// /// This enum represents different kinds of errors that can occur during /// the build process, and which may be fixable by a BuildFixer. pub enum InterimError { /// A problem that was detected during the build, and that we can attempt to fix. Recognized(Box), /// An error that we could not identify. Unidentified { /// The return code of the failed command. retcode: i32, /// The output lines from the command. lines: Vec, /// Optional secondary information about the error. secondary: Option>, }, /// Another error raised specifically by the callback function that is not fixable. Other(O), } /// Error result from repeatedly running and attemptin to fix issues. #[derive(Debug)] pub enum IterateBuildError { /// The limit of fixing attempts was reached. FixerLimitReached(usize), /// A problem was detected that was recognized but could not be fixed. Persistent(Box), /// An error that we could not identify. Unidentified { /// The return code of the failed command. retcode: i32, /// The output lines from the command. lines: Vec, /// Optional secondary information about the error. secondary: Option>, }, /// Another error raised specifically by the callback function that is not fixable. Other(O), } impl Display for IterateBuildError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { IterateBuildError::FixerLimitReached(limit) => { write!(f, "Fixer limit reached: {}", limit) } IterateBuildError::Persistent(p) => { write!(f, "Persistent build problem: {}", p) } IterateBuildError::Unidentified { retcode, lines, secondary, } => write!( f, "Unidentified error: retcode: {}, lines: {:?}, secondary: {:?}", retcode, lines, secondary ), IterateBuildError::Other(e) => write!(f, "Other error: {}", e), } } } impl std::error::Error for IterateBuildError {} /// Call cb() until there are no more DetailedFailures we can fix. /// /// # Arguments /// * `fixers`: List of fixers to use to resolve issues /// * `cb`: Callable to run the build /// * `limit: Maximum number of fixing attempts before giving up pub fn iterate_with_build_fixers< T, // The error type that the fixers can return. I: std::error::Error, // The error type that the callback function can return, and the eventual return type. E: From + std::error::Error, >( fixers: &[&dyn BuildFixer], mut cb: impl FnMut() -> Result>, limit: Option, ) -> Result> { let mut attempts = 0; let mut fixed_errors: std::collections::HashSet> = std::collections::HashSet::new(); loop { let mut to_resolve: Vec> = vec![]; match cb() { Ok(v) => return Ok(v), Err(InterimError::Recognized(e)) => to_resolve.push(e), Err(InterimError::Unidentified { retcode, lines, secondary, }) => { return Err(IterateBuildError::Unidentified { retcode, lines, secondary, }); } Err(InterimError::Other(e)) => return Err(IterateBuildError::Other(e)), } while let Some(f) = to_resolve.pop() { info!("Identified error: {:?}", f); if fixed_errors.contains(&f) { warn!("Failed to resolve error {:?}, it persisted. Giving up.", f); return Err(IterateBuildError::Persistent(f)); } attempts += 1; if let Some(limit) = limit { if limit <= attempts { return Err(IterateBuildError::FixerLimitReached(limit)); } } match resolve_error(f.as_ref(), fixers) { Err(InterimError::Recognized(n)) => { info!("New error {:?} while resolving {:?}", &n, &f); if to_resolve.contains(&n) { return Err(IterateBuildError::Persistent(n)); } to_resolve.push(f); to_resolve.push(n); } Err(InterimError::Unidentified { retcode, lines, secondary, }) => { return Err(IterateBuildError::Unidentified { retcode, lines, secondary, }); } Err(InterimError::Other(e)) => return Err(IterateBuildError::Other(e.into())), Ok(resolved) if !resolved => { warn!("Failed to find resolution for error {:?}. Giving up.", f); return Err(IterateBuildError::Persistent(f)); } Ok(_) => { fixed_errors.insert(f); } } } } } /// Attempt to resolve a problem using available fixers. /// /// # Arguments /// * `problem` - The problem to resolve /// * `fixers` - List of fixers to try /// /// # Returns /// * `Ok(true)` - If the problem was fixed /// * `Ok(false)` - If no fixer could fix the problem /// * `Err(InterimError)` - If fixing the problem failed pub fn resolve_error( problem: &dyn Problem, fixers: &[&dyn BuildFixer], ) -> Result> { let relevant_fixers = fixers .iter() .filter(|fixer| fixer.can_fix(problem)) .collect::>(); if relevant_fixers.is_empty() { warn!("No fixer found for {:?}", problem); return Ok(false); } for fixer in relevant_fixers { info!("Attempting to use fixer {} to address {:?}", fixer, problem); let made_changes = fixer.fix(problem)?; if made_changes { return Ok(true); } } Ok(false) } /// Run a command repeatedly, attempting to fix any problems that occur. /// /// This function runs a command and applies fixes if it fails, /// potentially retrying multiple times with different fixers. /// /// # Arguments /// * `fixers` - List of fixers to try if the command fails /// * `limit` - Optional maximum number of fix attempts /// * `session` - The session to run the command in /// * `args` - The command and its arguments /// * `quiet` - Whether to suppress output /// * `cwd` - Optional current working directory /// * `user` - Optional user to run as /// * `env` - Optional environment variables /// /// # Returns /// * `Ok(Vec)` - The output lines if successful /// * `Err(IterateBuildError)` - If the command fails and can't be fixed pub fn run_fixing_problems< // The error type that the fixers can return. I: std::error::Error, // The error type that the callback function can return. E: From + std::error::Error + From, >( fixers: &[&dyn BuildFixer], limit: Option, session: &dyn crate::session::Session, args: &[&str], quiet: bool, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option<&std::collections::HashMap>, ) -> Result, IterateBuildError> { iterate_with_build_fixers::, I, E>( fixers, || { crate::analyze::run_detecting_problems( session, args.to_vec(), None, quiet, cwd, user, env, None, ) .map_err(|e| match e { crate::analyze::AnalyzedError::Detailed { retcode: _, error } => { InterimError::Recognized(error) } crate::analyze::AnalyzedError::Unidentified { retcode, lines, secondary, } => InterimError::Unidentified { retcode, lines, secondary, }, crate::analyze::AnalyzedError::MissingCommandError { command } => { InterimError::Recognized(Box::new( buildlog_consultant::problems::common::MissingCommand(command), )) } crate::analyze::AnalyzedError::IoError(e) => InterimError::Other(e.into()), }) }, limit, ) .map_err(|e| match e { IterateBuildError::Other(e) => IterateBuildError::Other(e), e => e, }) } ognibuild-0.2.6/src/fixers.rs000064400000000000000000000342321046102023000142460ustar 00000000000000use crate::fix_build::{BuildFixer, InterimError}; use crate::installer::{Error as InstallerError, InstallationScope, Installer}; use crate::session::Session; use buildlog_consultant::problems::common::{ MinimumAutoconfTooOld, MissingAutoconfMacro, MissingGitIdentity, MissingGnulibDirectory, MissingGoSumEntry, MissingSecretGpgKey, }; use buildlog_consultant::Problem; use std::io::{Seek, Write}; /// Fixer for missing gnulib directory. /// /// Runs the gnulib.sh script to fix the issue. pub struct GnulibDirectoryFixer<'a> { session: &'a dyn Session, } impl std::fmt::Debug for GnulibDirectoryFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GnulibDirectoryFixer").finish() } } impl std::fmt::Display for GnulibDirectoryFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "GnulibDirectoryFixer") } } impl<'a> GnulibDirectoryFixer<'a> { /// Create a new GnulibDirectoryFixer with the specified session. pub fn new(session: &'a dyn Session) -> Self { Self { session } } } impl<'a> BuildFixer for GnulibDirectoryFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, _problem: &dyn Problem) -> Result> { self.session .command(vec!["./gnulib.sh"]) .check_call() .unwrap(); Ok(true) } } /// Fixer for missing Git identity. /// /// Sets up Git configuration with user.email and user.name. pub struct GitIdentityFixer<'a> { session: &'a dyn Session, } impl std::fmt::Debug for GitIdentityFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GitIdentityFixer").finish() } } impl std::fmt::Display for GitIdentityFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "GitIdentityFixer") } } impl<'a> GitIdentityFixer<'a> { /// Create a new GitIdentityFixer with the specified session. pub fn new(session: &'a dyn Session) -> Self { Self { session } } } impl<'a> BuildFixer for GitIdentityFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, _problem: &dyn Problem) -> Result> { for name in ["user.email", "user.name"] { let output = std::process::Command::new("git") .arg("config") .arg("--global") .arg(name) .output() .unwrap(); let value = String::from_utf8(output.stdout).unwrap(); self.session .command(vec!["git", "config", "--global", name, &value]) .check_call() .unwrap(); } Ok(true) } } /// Fixer for missing secret GPG key. /// /// Generates a dummy GPG key for use in the build process. pub struct SecretGpgKeyFixer<'a> { session: &'a dyn Session, } impl std::fmt::Debug for SecretGpgKeyFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SecretGpgKeyFixer").finish() } } impl std::fmt::Display for SecretGpgKeyFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SecretGpgKey") } } impl<'a> SecretGpgKeyFixer<'a> { /// Create a new SecretGpgKeyFixer with the specified session. pub fn new(session: &'a dyn Session) -> Self { Self { session } } } impl<'a> BuildFixer for SecretGpgKeyFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, _problem: &dyn Problem) -> Result> { let mut td = tempfile::tempfile().unwrap(); let script = br#"""Key-Type: 1 Key-Length: 4096 Subkey-Type: 1 Subkey-Length: 4096 Name-Real: Dummy Key for ognibuild Name-Email: dummy@example.com Expire-Date: 0 Passphrase: "" """#; td.write_all(script).unwrap(); td.seek(std::io::SeekFrom::Start(0)).unwrap(); self.session .command(vec!["gpg", "--gen-key", "--batch", "/dev/stdin"]) .stdin(td.into()) .check_call() .unwrap(); Ok(true) } } /// Fixer for minimum Autoconf version requirements. /// /// Updates the AC_PREREQ macro in configure.ac or configure.in. pub struct MinimumAutoconfFixer<'a> { session: &'a dyn Session, } impl std::fmt::Debug for MinimumAutoconfFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MinimumAutoconfFixer").finish() } } impl std::fmt::Display for MinimumAutoconfFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "MinimumAutoconfFixer") } } impl<'a> MinimumAutoconfFixer<'a> { /// Create a new MinimumAutoconfFixer with the specified session. pub fn new(session: &'a dyn Session) -> Self { Self { session } } } impl<'a> BuildFixer for MinimumAutoconfFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, problem: &dyn Problem) -> Result> { let problem = problem .as_any() .downcast_ref::() .unwrap(); if let Some(name) = ["configure.ac", "configure.in"].into_iter().next() { let p = self.session.external_path(std::path::Path::new(name)); let f = std::fs::File::open(&p).unwrap(); let buf = std::io::BufReader::new(f); use std::io::BufRead; let mut lines = buf.lines().map(|l| l.unwrap()).collect::>(); let mut found = false; for line in lines.iter_mut() { let m = lazy_regex::regex_find!(r"AC_PREREQ\((.*)\)", &line); if m.is_none() { continue; } *line = format!("AC_PREREQ({})", problem.0); found = true; } if !found { lines.insert(0, format!("AC_PREREQ({})", problem.0)); } std::fs::write( self.session.external_path(std::path::Path::new(name)), lines.concat(), ) .unwrap(); return Ok(true); } Ok(false) } } /// Fixer for missing Go module sum entries. /// /// Downloads Go modules to update the go.sum file. pub struct MissingGoSumEntryFixer<'a> { session: &'a dyn Session, } impl std::fmt::Debug for MissingGoSumEntryFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MissingGoSumEntryFixer").finish() } } impl std::fmt::Display for MissingGoSumEntryFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "MissingGoSumEntryFixer") } } impl<'a> MissingGoSumEntryFixer<'a> { /// Create a new MissingGoSumEntryFixer with the specified session. pub fn new(session: &'a dyn Session) -> Self { Self { session } } } impl<'a> BuildFixer for MissingGoSumEntryFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, problem: &dyn Problem) -> Result> { let problem = problem .as_any() .downcast_ref::() .unwrap(); self.session .command(vec!["go", "mod", "download", &problem.package]) .check_call() .unwrap(); Ok(true) } } /// Fixer for unexpanded Autoconf macros. /// /// Installs missing autoconf macros and reruns autoconf. pub struct UnexpandedAutoconfMacroFixer<'a> { session: &'a dyn Session, installer: &'a dyn Installer, } impl std::fmt::Debug for UnexpandedAutoconfMacroFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UnexpandedAutoconfMacroFixer").finish() } } impl std::fmt::Display for UnexpandedAutoconfMacroFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "UnexpandedAutoconfMacroFixer") } } impl<'a> UnexpandedAutoconfMacroFixer<'a> { /// Create a new UnexpandedAutoconfMacroFixer with the specified session and installer. pub fn new(session: &'a dyn Session, installer: &'a dyn Installer) -> Self { Self { session, installer } } } impl<'a> BuildFixer for UnexpandedAutoconfMacroFixer<'a> { fn can_fix(&self, problem: &dyn Problem) -> bool { problem .as_any() .downcast_ref::() .is_some() } fn fix(&self, problem: &dyn Problem) -> Result> { let problem = problem .as_any() .downcast_ref::() .unwrap(); let dep = crate::dependencies::autoconf::AutoconfMacroDependency::new(&problem.r#macro); self.installer .install(&dep, InstallationScope::Global) .unwrap(); self.session .command(vec!["autoconf", "-f"]) .check_call() .unwrap(); Ok(true) } } /// Generic fixer for installing missing build dependencies. /// /// Detects and installs required dependencies from build logs. pub struct InstallFixer<'a> { installer: &'a dyn crate::installer::Installer, scope: crate::installer::InstallationScope, } impl std::fmt::Debug for InstallFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InstallFixer").finish() } } impl std::fmt::Display for InstallFixer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "upstream requirement fixer") } } impl<'a> InstallFixer<'a> { /// Create a new InstallFixer with the specified installer and installation scope. pub fn new( installer: &'a dyn crate::installer::Installer, scope: crate::installer::InstallationScope, ) -> Self { Self { installer, scope } } } impl<'a> BuildFixer for InstallFixer<'a> { fn can_fix(&self, error: &dyn Problem) -> bool { let req = crate::buildlog::problem_to_dependency(error); req.is_some() } fn fix(&self, error: &dyn Problem) -> Result> { let req = crate::buildlog::problem_to_dependency(error); if let Some(req) = req { match self.installer.install(req.as_ref(), self.scope) { Ok(()) => { log::debug!("Successfully installed dependency: {:?}", req); Ok(true) } Err(crate::installer::Error::UnknownDependencyFamily) => { log::warn!("Cannot install dependency from unknown family: {:?}", req); Ok(false) // Can't fix this problem, but that's okay } Err(e) => { log::error!("Failed to install dependency {:?}: {}", req, e); Err(InterimError::Other(e)) } } } else { Ok(false) } } } #[cfg(test)] mod tests { use super::*; use crate::installer::{InstallationScope, NullInstaller}; use buildlog_consultant::Problem; use std::borrow::Cow; // Mock problem that doesn't map to any dependency (to test the None case) struct UnknownProblem { description: String, } impl Problem for UnknownProblem { fn kind(&self) -> Cow<'_, str> { Cow::Borrowed("unknown") } fn json(&self) -> serde_json::Value { serde_json::json!({ "kind": "unknown", "description": self.description }) } fn as_any(&self) -> &(dyn std::any::Any + 'static) { self } } impl std::fmt::Display for UnknownProblem { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.description) } } impl std::fmt::Debug for UnknownProblem { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "UnknownProblem({})", self.description) } } #[test] fn test_install_fixer_handles_unknown_problem() { // Test with a problem that doesn't map to any dependency let installer = NullInstaller {}; let fixer = InstallFixer::new(&installer, InstallationScope::Global); let problem = UnknownProblem { description: "some unknown build problem".to_string(), }; // This should return Ok(false) without panicking let result = fixer.fix(&problem); assert!(result.is_ok()); assert_eq!(result.unwrap(), false); } #[test] fn test_install_fixer_handles_unknown_dependency_family() { use buildlog_consultant::problems::common::MissingCommand; // NullInstaller always returns UnknownDependencyFamily for any dependency let installer = NullInstaller {}; let fixer = InstallFixer::new(&installer, InstallationScope::Global); // Use MissingCommand which maps to a BinaryDependency let problem = MissingCommand("nonexistent-command".to_string()); // This should return Ok(false) without panicking, even though the dependency // family is unknown to the NullInstaller let result = fixer.fix(&problem); assert!(result.is_ok()); assert_eq!(result.unwrap(), false); } } ognibuild-0.2.6/src/installer.rs000064400000000000000000000406631046102023000147500ustar 00000000000000use crate::dependency::Dependency; use crate::session::Session; #[derive(Debug)] /// Errors that can occur during dependency installation. pub enum Error { /// Error indicating that the dependency family is unknown. UnknownDependencyFamily, /// Error indicating that the requested installation scope is not supported. UnsupportedScope(InstallationScope), /// Error indicating that the requested installation scopes are not supported. UnsupportedScopes(Vec), /// Error from analyzing a command execution. AnalyzedError(crate::analyze::AnalyzedError), /// Error from the session. SessionError(crate::session::Error), /// Other error with a message. Other(String), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::UnknownDependencyFamily => write!(f, "Unknown dependency family"), Error::UnsupportedScope(scope) => write!(f, "Unsupported scope: {:?}", scope), Error::UnsupportedScopes(scopes) => write!(f, "Unsupported scopes: {:?}", scopes), Error::AnalyzedError(e) => write!(f, "{}", e), Error::SessionError(e) => write!(f, "{}", e), Error::Other(s) => write!(f, "{}", s), } } } impl std::error::Error for Error {} impl From for Error { fn from(e: crate::analyze::AnalyzedError) -> Self { Error::AnalyzedError(e) } } impl From for Error { fn from(e: crate::session::Error) -> Self { Error::SessionError(e) } } /// An explanation is a human-readable description of what to do to install a dependency. pub struct Explanation { /// A human-readable message explaining how to install the dependency. pub message: String, /// An optional command that can be run to install the dependency. pub command: Option>, } impl Explanation { /// Create a new explanation. /// /// # Arguments /// * `message` - A human-readable message explaining how to install the dependency /// * `command` - An optional command that can be run to install the dependency /// /// # Returns /// A new Explanation instance pub fn new(message: String, command: Option>) -> Self { Explanation { message, command } } } impl std::fmt::Display for Explanation { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.message)?; if let Some(command) = &self.command { write!( f, "\n\nRun the following command to install the dependency:\n\n" )?; for arg in command { write!(f, "{} ", arg)?; } writeln!(f)?; } Ok(()) } } /// The scope of an installation. #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum InstallationScope { /// Under /usr in the system Global, /// In the current users' home directory User, /// Vendored in the projects' source directory Vendor, } impl std::str::FromStr for InstallationScope { type Err = Error; fn from_str(s: &str) -> Result { match s { "global" => Ok(InstallationScope::Global), "user" => Ok(InstallationScope::User), "vendor" => Ok(InstallationScope::Vendor), _ => Err(Error::Other(format!("Unknown installation scope: {}", s))), } } } /// An installer can take a dependency and install it into the session. pub trait Installer { /// Install the dependency into the session. fn install(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result<(), Error>; /// Explain how to install the dependency. fn explain(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result; /// Explain how to install multiple dependencies. /// /// # Arguments /// * `deps` - List of dependencies to explain /// * `scope` - Installation scope to use /// /// # Returns /// * `Ok((Vec, Vec>))` - Explanations for known dependencies and list of unknown dependencies /// * `Err(Error)` - If explaining any dependency fails with an error other than UnknownDependencyFamily fn explain_some( &self, deps: Vec>, scope: InstallationScope, ) -> Result<(Vec, Vec>), Error> { let mut explanations = Vec::new(); let mut failed = Vec::new(); for dep in deps { match self.explain(&*dep, scope) { Ok(explanation) => explanations.push(explanation), Err(Error::UnknownDependencyFamily) => failed.push(dep), Err(e) => { return Err(e); } } } Ok((explanations, failed)) } /// Install multiple dependencies. /// /// # Arguments /// * `deps` - List of dependencies to install /// * `scope` - Installation scope to use /// /// # Returns /// * `Ok((Vec>, Vec>))` - Successfully installed dependencies and unknown dependencies /// * `Err(Error)` - If installing any dependency fails with an error other than UnknownDependencyFamily fn install_some( &self, deps: Vec>, scope: InstallationScope, ) -> Result<(Vec>, Vec>), Error> { let mut installed = Vec::new(); let mut failed = Vec::new(); for dep in deps { match self.install(&*dep, scope) { Ok(()) => installed.push(dep), Err(Error::UnknownDependencyFamily) => failed.push(dep), Err(e) => { return Err(e); } } } Ok((installed, failed)) } } /// A null installer does nothing. pub struct NullInstaller; impl NullInstaller { /// Create a new NullInstaller. /// /// # Returns /// A new NullInstaller instance pub fn new() -> Self { NullInstaller } } impl Default for NullInstaller { fn default() -> Self { NullInstaller::new() } } impl Installer for NullInstaller { fn install(&self, _dep: &dyn Dependency, _scope: InstallationScope) -> Result<(), Error> { Err(Error::UnknownDependencyFamily) } fn explain( &self, _dep: &dyn Dependency, _scope: InstallationScope, ) -> Result { Err(Error::UnknownDependencyFamily) } } /// An installer that tries multiple installers in sequence. /// /// This installer tries each installer in order until one succeeds or all fail. pub struct StackedInstaller<'a>(pub Vec>); impl<'a> StackedInstaller<'a> { /// Create a new StackedInstaller. /// /// # Arguments /// * `resolvers` - List of installers to try in sequence /// /// # Returns /// A new StackedInstaller instance pub fn new(resolvers: Vec>) -> Self { Self(resolvers) } } impl<'a> Installer for StackedInstaller<'a> { fn install(&self, requirement: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { for sub in &self.0 { match sub.install(requirement, scope) { Ok(()) => { return Ok(()); } Err(Error::UnknownDependencyFamily) => {} Err(e) => { return Err(e); } } } Err(Error::UnknownDependencyFamily) } fn explain( &self, requirements: &dyn Dependency, scope: InstallationScope, ) -> Result { for sub in &self.0 { match sub.explain(requirements, scope) { Ok(e) => { return Ok(e); } Err(Error::UnknownDependencyFamily) => {} Err(e) => { return Err(e); } } } Err(Error::UnknownDependencyFamily) } } /// Create an installer by name. /// /// # Arguments /// * `session` - The session to use for installation /// * `name` - The name of the installer to create /// /// # Returns /// An installer that can install dependencies in the given session pub fn installer_by_name<'a>( session: &'a dyn crate::session::Session, name: &str, ) -> Option> { // TODO: Use more dynamic way to load installers match name { #[cfg(feature = "debian")] "apt" => Some( Box::new(crate::debian::apt::AptInstaller::from_session(session)) as Box, ), "cpan" => Some( Box::new(crate::dependencies::perl::CPAN::new(session, false)) as Box, ), "ctan" => Some(Box::new(crate::dependencies::latex::ctan(session)) as Box), "pypi" => Some( Box::new(crate::dependencies::python::PypiResolver::new(session)) as Box, ), "npm" => Some( Box::new(crate::dependencies::node::NpmResolver::new(session)) as Box, ), "go" => { Some(Box::new(crate::dependencies::go::GoResolver::new(session)) as Box) } "hackage" => Some( Box::new(crate::dependencies::haskell::HackageResolver::new(session)) as Box, ), "cran" => Some(Box::new(crate::dependencies::r::cran(session)) as Box), "bioconductor" => { Some(Box::new(crate::dependencies::r::bioconductor(session)) as Box) } "octave-forge" => Some( Box::new(crate::dependencies::octave::OctaveForgeResolver::new( session, )) as Box, ), "native" => { Some(Box::new(StackedInstaller::new(native_installers(session))) as Box) } _ => None, } } /// Create a list of all native installers for the current system. /// /// # Arguments /// * `session` - The session to use for installation /// /// # Returns /// A list of installers that can install dependencies on the current system pub fn native_installers<'a>( session: &'a dyn crate::session::Session, ) -> Vec> { // TODO: Use more dynamic way to load installers [ "ctan", "pypi", "npm", "go", "hackage", "cran", "bioconductor", "octave-forge", ] .iter() .map(|name| installer_by_name(session, name).unwrap()) .collect() } #[cfg(feature = "debian")] fn apt_installer<'a>( session: &'a dyn crate::session::Session, #[allow(unused_variables)] dep_server_url: Option<&url::Url>, ) -> Box { #[cfg(feature = "dep-server")] if let Some(dep_server_url) = dep_server_url { Box::new( crate::debian::dep_server::DepServerAptInstaller::from_session(session, dep_server_url), ) as Box } else { Box::new(crate::debian::apt::AptInstaller::from_session(session)) } #[cfg(not(feature = "dep-server"))] { Box::new(crate::debian::apt::AptInstaller::from_session(session)) } } /// Select installers by name. pub fn select_installers<'a>( session: &'a dyn crate::session::Session, names: &[&str], #[allow(unused_variables)] dep_server_url: Option<&url::Url>, ) -> Result, String> { let mut installers = Vec::new(); for name in names.iter() { if name == &"apt" { #[cfg(feature = "debian")] installers.push(apt_installer(session, dep_server_url)); #[cfg(not(feature = "debian"))] return Err("Apt installer not available".to_string()); } else if let Some(installer) = installer_by_name(session, name) { installers.push(installer); } else { return Err(format!("Unknown installer: {}", name)); } } Ok(Box::new(StackedInstaller(installers))) } /// Determine the default installation scope based on the session. /// /// # Arguments /// * `session` - The session to determine the scope for /// /// # Returns /// The default installation scope for the session pub fn auto_installation_scope(session: &dyn crate::session::Session) -> InstallationScope { let user = crate::session::get_user(session); // TODO(jelmer): Check VIRTUAL_ENV, and prioritize PypiResolver if // present? if user == "root" { log::info!("Running as root, so using global installation scope"); InstallationScope::Global } else if session.is_temporary() { log::info!("Running in a temporary session, so using global installation scope"); InstallationScope::Global } else { log::info!("Running as user, so using user installation scope"); InstallationScope::User } } /// Create an automatic installer that can install dependencies in the given session. /// /// # Arguments /// * `session` - The session to use for installation /// * `scope` - The installation scope to use /// * `dep_server_url` - Optional URL of a dependency server to use /// /// # Returns /// An installer that can install dependencies in the given session pub fn auto_installer<'a>( session: &'a dyn crate::session::Session, #[allow(unused_variables)] scope: InstallationScope, #[allow(unused_variables)] dep_server_url: Option<&url::Url>, ) -> Box { // if session is SchrootSession or if we're root, use apt let mut installers: Vec> = Vec::new(); #[cfg(feature = "debian")] if scope == InstallationScope::Global && crate::session::which(session, "apt-get").is_some() { log::info!( "Using global installation scope and apt-get is available, so using apt installer" ); installers.push(apt_installer(session, dep_server_url)); } installers.extend(native_installers(session)); Box::new(StackedInstaller::new(installers)) } /// Install missing dependencies. /// /// This function takes a list of dependencies and installs them if they are not already present. /// /// # Arguments /// * `session` - The session to install the dependencies into. /// * `installer` - The installer to use. pub fn install_missing_deps( session: &dyn Session, installer: &dyn Installer, scopes: &[InstallationScope], deps: &[&dyn Dependency], ) -> Result<(), Error> { if deps.is_empty() { return Ok(()); } let missing = deps .iter() .filter(|dep| !dep.present(session)) .collect::>(); if !missing.is_empty() { log::info!("Missing dependencies: {:?}", missing); for dep in missing.into_iter() { log::info!("Installing {:?}", dep); let mut installed = false; for scope in scopes { match installer.install(*dep, *scope) { Ok(()) => { log::info!("Installed {:?}", dep); installed = true; break; } Err(Error::UnsupportedScope(_)) => {} Err(e) => { return Err(e); } } } if !installed { return Err(Error::UnsupportedScopes(scopes.to_vec())); } } } Ok(()) } /// Explain missing dependencies. /// /// This function takes a list of dependencies and returns a list of explanations for how to /// install them. /// /// # Arguments /// * `session` - The session to install the dependencies into. /// * `installer` - The installer to use. pub fn explain_missing_deps( session: &dyn Session, installer: &dyn Installer, deps: &[&dyn Dependency], ) -> Result, Error> { if deps.is_empty() { return Ok(vec![]); } let mut missing = vec![]; for dep in deps.iter() { if !dep.present(session) { missing.push(*dep) } } if !missing.is_empty() { let mut explanations = vec![]; for dep in missing.into_iter() { log::info!("Explaining {:?}", dep); explanations.push(installer.explain(dep, InstallationScope::Global)?); } Ok(explanations) } else { Ok(vec![]) } } ognibuild-0.2.6/src/lib.rs000064400000000000000000000023461046102023000135150ustar 00000000000000#![deny(missing_docs)] //! Library for building packages from source code. /// Action implementations like build, clean, test, etc. pub mod actions; /// Analyze build errors and execution problems. pub mod analyze; /// Build log handling and parsing. pub mod buildlog; /// BuildSystem trait and related types. pub mod buildsystem; /// Implementations of different build systems. pub mod buildsystems; #[cfg(feature = "debian")] /// Debian-specific functionality. pub mod debian; /// Dependency resolution implementations. pub mod dependencies; /// Dependency trait and related types. pub mod dependency; /// Distribution package creation. pub mod dist; /// Utilities for catching distribution packages. pub mod dist_catcher; /// Build fixing utilities. pub mod fix_build; /// Implementations of different build fixers. pub mod fixers; /// Package installer functionality. pub mod installer; /// Logging utilities. pub mod logs; /// Output formatting and handling. pub mod output; /// Session handling for build environments. pub mod session; /// Shebang detection and processing. pub mod shebang; #[cfg(feature = "upstream")] /// Upstream package handling. pub mod upstream; #[cfg(feature = "breezy")] /// Version control system utilities. pub mod vcs; ognibuild-0.2.6/src/logs.rs000064400000000000000000000136751046102023000137220ustar 00000000000000use log::debug; use std::fs; use std::fs::File; use std::io::{self, Write}; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::{Path, PathBuf}; use std::process::Command; struct RedirectOutput { old_stdout: RawFd, old_stderr: RawFd, } impl RedirectOutput { fn new(to_file: &File) -> io::Result { let stdout = io::stdout(); let stderr = io::stderr(); stdout.lock().flush()?; stderr.lock().flush()?; let old_stdout = unsafe { libc::dup(libc::STDOUT_FILENO) }; let old_stderr = unsafe { libc::dup(libc::STDERR_FILENO) }; if old_stdout == -1 || old_stderr == -1 { return Err(io::Error::last_os_error()); } unsafe { libc::dup2(to_file.as_raw_fd(), libc::STDOUT_FILENO); libc::dup2(to_file.as_raw_fd(), libc::STDERR_FILENO); } Ok(RedirectOutput { old_stdout, old_stderr, }) } } impl Drop for RedirectOutput { fn drop(&mut self) { let stdout = io::stdout(); let stderr = io::stderr(); stdout.lock().flush().unwrap(); stderr.lock().flush().unwrap(); unsafe { libc::dup2(self.old_stdout, libc::STDOUT_FILENO); libc::dup2(self.old_stderr, libc::STDERR_FILENO); libc::close(self.old_stdout); libc::close(self.old_stderr); } } } struct CopyOutput { old_stdout: RawFd, old_stderr: RawFd, new_fd: Option, } impl CopyOutput { fn new(output_log: &std::path::Path, tee: bool) -> io::Result { let old_stdout = unsafe { libc::dup(libc::STDOUT_FILENO) }; let old_stderr = unsafe { libc::dup(libc::STDERR_FILENO) }; let new_fd = if tee { let process = Command::new("tee") .arg(output_log) .stdin(std::process::Stdio::piped()) .spawn()?; process.stdin.unwrap().as_raw_fd() } else { File::create(output_log)?.as_raw_fd() }; unsafe { libc::dup2(new_fd, libc::STDOUT_FILENO); libc::dup2(new_fd, libc::STDERR_FILENO); } Ok(CopyOutput { old_stdout, old_stderr, new_fd: Some(new_fd), }) } } impl Drop for CopyOutput { fn drop(&mut self) { if let Some(fd) = self.new_fd.take() { unsafe { libc::fsync(fd); libc::close(fd); } } unsafe { libc::dup2(self.old_stdout, libc::STDOUT_FILENO); libc::dup2(self.old_stderr, libc::STDERR_FILENO); libc::close(self.old_stdout); libc::close(self.old_stderr); } } } /// Rotate a log file, moving it to a new file with a timestamp. /// /// # Arguments /// * `source_path` - Path to the log file to rotate /// /// # Returns /// * `Ok(())` - If the log file was rotated successfully /// * `Err(Error)` - If rotating the log file failed pub fn rotate_logfile(source_path: &std::path::Path) -> std::io::Result<()> { if source_path.exists() { let directory_path = source_path.parent().unwrap_or_else(|| Path::new("")); let name = source_path.file_name().unwrap().to_str().unwrap(); let mut i = 1; while directory_path.join(format!("{}.{}", name, i)).exists() { i += 1; } let target_path: PathBuf = directory_path.join(format!("{}.{}", name, i)); fs::rename(source_path, &target_path)?; debug!("Storing previous build log at {}", target_path.display()); } Ok(()) } /// Mode for logging. pub enum LogMode { /// Copy output to the log file. Copy, /// Redirect output to the log file. Redirect, } /// Trait for managing log files for build operations. pub trait LogManager { /// Start logging to the log file. /// /// # Returns /// * `Ok(())` - If logging was started successfully /// * `Err(Error)` - If starting logging failed fn start(&mut self) -> std::io::Result<()>; /// Stop logging to the log file. fn stop(&mut self) {} } /// Run a function capturing its output to a log file. pub fn wrap(logs: &mut dyn LogManager, f: impl FnOnce() -> R) -> R { logs.start().unwrap(); let result = f(); std::io::stdout().flush().unwrap(); std::io::stderr().flush().unwrap(); logs.stop(); result } /// Log manager that logs to a file in a directory. pub struct DirectoryLogManager { path: PathBuf, mode: LogMode, copy_output: Option, redirect_output: Option, } impl DirectoryLogManager { /// Create a new DirectoryLogManager. /// /// # Arguments /// * `path` - Path to the log file /// * `mode` - Mode for logging /// /// # Returns /// A new DirectoryLogManager instance pub fn new(path: PathBuf, mode: LogMode) -> Self { Self { path, mode, copy_output: None, redirect_output: None, } } } impl LogManager for DirectoryLogManager { fn start(&mut self) -> std::io::Result<()> { rotate_logfile(&self.path)?; match self.mode { LogMode::Copy => { self.copy_output = Some(CopyOutput::new(&self.path, true)?); } LogMode::Redirect => { self.redirect_output = Some(RedirectOutput::new(&File::create(&self.path)?)?); } } Ok(()) } fn stop(&mut self) { self.copy_output = None; self.redirect_output = None; } } /// Log manager that does nothing. pub struct NoLogManager; impl NoLogManager { /// Create a new NoLogManager. /// /// # Returns /// A new NoLogManager instance pub fn new() -> Self { Self {} } } impl Default for NoLogManager { fn default() -> Self { Self::new() } } impl LogManager for NoLogManager { fn start(&mut self) -> std::io::Result<()> { Ok(()) } } ognibuild-0.2.6/src/output.rs000064400000000000000000000051301046102023000143010ustar 00000000000000/// Trait for build system outputs. /// /// This trait is implemented by types that represent outputs from a build system, /// such as binary packages, library packages, etc. pub trait Output: std::fmt::Debug { /// Get the family of this output (e.g., "binary", "python-package", etc.). /// /// # Returns /// A string identifying the output type family fn family(&self) -> &'static str; /// Get the dependencies declared by this output. /// /// # Returns /// A list of dependency names fn get_declared_dependencies(&self) -> Vec; } #[derive(Debug)] /// Output representing a binary executable. pub struct BinaryOutput(pub String); impl BinaryOutput { /// Create a new binary output. /// /// # Arguments /// * `name` - Name of the binary /// /// # Returns /// A new BinaryOutput instance pub fn new(name: &str) -> Self { BinaryOutput(name.to_owned()) } } impl Output for BinaryOutput { fn family(&self) -> &'static str { "binary" } fn get_declared_dependencies(&self) -> Vec { vec![] } } #[derive(Debug)] /// Output representing a Python package. pub struct PythonPackageOutput { /// Name of the Python package. pub name: String, /// Optional version of the Python package. pub version: Option, } impl PythonPackageOutput { /// Create a new Python package output. /// /// # Arguments /// * `name` - Name of the Python package /// * `version` - Optional version of the Python package /// /// # Returns /// A new PythonPackageOutput instance pub fn new(name: &str, version: Option<&str>) -> Self { PythonPackageOutput { name: name.to_owned(), version: version.map(|s| s.to_owned()), } } } impl Output for PythonPackageOutput { fn family(&self) -> &'static str { "python-package" } fn get_declared_dependencies(&self) -> Vec { vec![] } } #[derive(Debug)] /// Output representing an R package. pub struct RPackageOutput { /// Name of the R package. pub name: String, } impl RPackageOutput { /// Create a new R package output. /// /// # Arguments /// * `name` - Name of the R package /// /// # Returns /// A new RPackageOutput instance pub fn new(name: &str) -> Self { RPackageOutput { name: name.to_owned(), } } } impl Output for RPackageOutput { fn family(&self) -> &'static str { "r-package" } fn get_declared_dependencies(&self) -> Vec { vec![] } } ognibuild-0.2.6/src/session/mod.rs000064400000000000000000000666601046102023000152220ustar 00000000000000use std::collections::HashMap; use std::io::Write; use std::process::ExitStatus; /// Plain session implementation. pub mod plain; /// Schroot session implementation (Linux only). #[cfg(target_os = "linux")] pub mod schroot; /// Unshare session implementation (Linux only). #[cfg(target_os = "linux")] pub mod unshare; #[derive(Debug)] /// Errors related to image operations (downloading, caching, etc.) pub enum ImageError { /// Cached image specified was not found and downloading is not allowed CachedImageNotFound { /// Path where the image was expected to be cached path: std::path::PathBuf, }, /// There is no cached image NoCachedImage, /// Download is not available (missing feature or other reason) DownloadNotAvailable { /// Reason why download is not available reason: String, }, /// Architecture not supported for cloud images UnsupportedArchitecture { /// The unsupported architecture arch: String, }, /// Failed to download cloud image DownloadFailed { /// URL that failed to download url: String, /// Error message describing the failure error: String, }, } #[derive(Debug)] /// Errors that can occur in a session. pub enum Error { /// Error caused by a command that exited with a non-zero status code. CalledProcessError(ExitStatus), /// Error from an IO operation. IoError(std::io::Error), /// Error from setting up the session, with a message and detailed description. SetupFailure(String, String), /// Error from image operations (download, cache, etc.) ImageError(ImageError), } impl From for Error { fn from(e: std::io::Error) -> Self { Error::IoError(e) } } impl std::fmt::Display for ImageError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { ImageError::NoCachedImage => { write!(f, "No cached image available") } ImageError::CachedImageNotFound { path } => { write!( f, "Cached image not found at {} and downloading is not allowed", path.display() ) } ImageError::DownloadNotAvailable { reason } => { write!(f, "Download not available: {}", reason) } ImageError::UnsupportedArchitecture { arch } => { write!(f, "Architecture {} not supported for cloud images", arch) } ImageError::DownloadFailed { url, error } => { write!(f, "Failed to download from {}: {}", url, error) } } } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Error::CalledProcessError(code) => write!(f, "CalledProcessError({})", code), Error::IoError(e) => write!(f, "IoError({})", e), Error::SetupFailure(msg, _long_description) => write!(f, "SetupFailure({})", msg), Error::ImageError(e) => write!(f, "ImageError: {}", e), } } } impl std::error::Error for Error {} /// Session interface for running commands in different environments. /// /// This trait defines the interface for running commands in different environments, /// such as the local system, a chroot, or a container. pub trait Session { /// Change the current working directory in the session. fn chdir(&mut self, path: &std::path::Path) -> Result<(), crate::session::Error>; /// Get the current working directory in the session. /// /// # Returns /// The current working directory fn pwd(&self) -> &std::path::Path; /// Return the external path for a path inside the session. fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf; /// Return the location of the session. fn location(&self) -> std::path::PathBuf; /// Run a command and return its output. /// /// This method runs a command in the session and returns its output /// if the command exits successfully. /// /// # Arguments /// * `argv` - The command and its arguments /// * `cwd` - Optional current working directory /// * `user` - Optional user to run the command as /// * `env` - Optional environment variables /// /// # Returns /// * `Ok(Vec)` - The command output if successful /// * `Err(Error)` - If the command fails fn check_output( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result, Error>; /// Ensure that the current users' home directory exists. fn create_home(&self) -> Result<(), Error>; /// Run a command and check that it exits successfully. /// /// This method runs a command in the session and returns success /// if the command exits with a zero status code. /// /// # Arguments /// * `argv` - The command and its arguments /// * `cwd` - Optional current working directory /// * `user` - Optional user to run the command as /// * `env` - Optional environment variables /// /// # Returns /// * `Ok(())` - If the command exited successfully /// * `Err(Error)` - If the command fails fn check_call( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result<(), crate::session::Error>; /// Check if a file or directory exists. fn exists(&self, path: &std::path::Path) -> bool; /// Create a directory. fn mkdir(&self, path: &std::path::Path) -> Result<(), crate::session::Error>; /// Recursively remove a directory. fn rmtree(&self, path: &std::path::Path) -> Result<(), crate::session::Error>; /// Setup a project from an existing directory. /// /// # Arguments /// * `path` - The path to the directory to setup the session from. /// * `subdir` - The subdirectory to use as the session root. fn project_from_directory( &self, path: &std::path::Path, subdir: Option<&str>, ) -> Result; /// Create a new command builder for the session. /// /// # Arguments /// * `argv` - The command and its arguments /// /// # Returns /// A new CommandBuilder instance fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a>; /// Start a process in the session. /// /// # Arguments /// * `argv` - The command and its arguments /// * `cwd` - Optional current working directory /// * `user` - Optional user to run the command as /// * `stdout` - Optional stdout configuration /// * `stderr` - Optional stderr configuration /// * `stdin` - Optional stdin configuration /// * `env` - Optional environment variables /// /// # Returns /// * `Ok(Child)` - A handle to the running process /// * `Err(Error)` - If starting the process fails fn popen( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, stdout: Option, stderr: Option, stdin: Option, env: Option<&std::collections::HashMap>, ) -> Result; /// Check if the session is temporary. fn is_temporary(&self) -> bool; #[cfg(feature = "breezy")] /// Setup a project from a VCS tree. /// /// # Arguments /// * `tree` - The VCS tree to setup the session from. /// * `include_controldir` - Whether to include the control directory. /// * `subdir` - The subdirectory to use as the session root. /// /// # Returns /// A tuple containing the path to the tree in the session and /// the external path. fn project_from_vcs( &self, tree: &dyn crate::vcs::DupableTree, include_controldir: Option, subdir: Option<&str>, ) -> Result; /// Read the contents of a directory. /// /// # Arguments /// * `path` - Path to the directory to read /// /// # Returns /// * `Ok(Vec)` - The directory entries if successful /// * `Err(Error)` - If reading the directory fails fn read_dir(&self, path: &std::path::Path) -> Result, Error>; } /// Represents a project in a session, either as a temporary copy or a direct reference. pub enum Project { /// A project that does not need to be cleaned up. Noop(std::path::PathBuf), /// A temporary project that needs to be cleaned up. /// A temporary copy of a project, which exists only for the duration of the session. Temporary { /// The path to the project from the external environment. external_path: std::path::PathBuf, /// The path to the project inside the session. internal_path: std::path::PathBuf, /// The path to the temporary directory. td: std::path::PathBuf, }, } impl Drop for Project { fn drop(&mut self) { match self { Project::Noop(_) => {} Project::Temporary { external_path: _, internal_path: _, td, } => { log::info!("Removing temporary project {}", td.display()); std::fs::remove_dir_all(td).unwrap(); } } } } impl Project { /// Get the path to the project inside the session. /// /// # Returns /// The path to the project inside the session pub fn internal_path(&self) -> &std::path::Path { match self { Project::Noop(path) => path, Project::Temporary { internal_path, .. } => internal_path, } } /// Get the path to the project from the external environment. /// /// # Returns /// The path to the project from the external environment pub fn external_path(&self) -> &std::path::Path { match self { Project::Noop(path) => path, Project::Temporary { external_path, .. } => external_path, } } } impl From for Project { fn from(tempdir: tempfile::TempDir) -> Self { Project::Temporary { external_path: tempdir.path().to_path_buf(), internal_path: tempdir.path().to_path_buf(), td: tempdir.keep(), } } } /// Builder for creating and running commands in a session. /// /// This struct provides a fluent interface for configuring and executing /// commands within a session, handling options like working directory, /// environment variables, input/output redirection, and more. pub struct CommandBuilder<'a> { /// The session to run the command in session: &'a dyn Session, /// The command and its arguments argv: Vec<&'a str>, /// Optional current working directory cwd: Option<&'a std::path::Path>, /// Optional user to run the command as user: Option<&'a str>, /// Optional environment variables env: Option>, /// Optional stdin configuration stdin: Option, /// Optional stdout configuration stdout: Option, /// Optional stderr configuration stderr: Option, /// Whether to suppress output quiet: bool, } impl<'a> CommandBuilder<'a> { /// Create a new CommandBuilder. /// /// # Arguments /// * `session` - The session to run the command in /// * `argv` - The command and its arguments /// /// # Returns /// A new CommandBuilder instance pub fn new(session: &'a dyn Session, argv: Vec<&'a str>) -> Self { CommandBuilder { session, argv, cwd: None, user: None, env: None, stdin: None, stdout: None, stderr: None, quiet: false, } } /// Set whether the command should run quietly. /// /// # Arguments /// * `quiet` - Whether to suppress output /// /// # Returns /// Self for method chaining pub fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self } /// Set the current working directory for the command. pub fn cwd(mut self, cwd: &'a std::path::Path) -> Self { self.cwd = Some(cwd); self } /// Set the user to run the command as. pub fn user(mut self, user: &'a str) -> Self { self.user = Some(user); self } /// Set the environment for the command. pub fn env(mut self, env: std::collections::HashMap) -> Self { assert!(self.env.is_none()); self.env = Some(env); self } /// Add an environment variable to the command. pub fn setenv(mut self, key: String, value: String) -> Self { self.env = match self.env { Some(mut env) => { env.insert(key, value); Some(env) } None => Some(std::collections::HashMap::from([(key, value)])), }; self } /// Set the stdin for the command. /// /// # Arguments /// * `stdin` - The stdin configuration /// /// # Returns /// Self for method chaining pub fn stdin(mut self, stdin: std::process::Stdio) -> Self { self.stdin = Some(stdin); self } /// Set the stdout for the command. /// /// # Arguments /// * `stdout` - The stdout configuration /// /// # Returns /// Self for method chaining pub fn stdout(mut self, stdout: std::process::Stdio) -> Self { self.stdout = Some(stdout); self } /// Set the stderr for the command. /// /// # Arguments /// * `stderr` - The stderr configuration /// /// # Returns /// Self for method chaining pub fn stderr(mut self, stderr: std::process::Stdio) -> Self { self.stderr = Some(stderr); self } /// Run the command and capture its output, while also displaying it. /// /// This method executes the command and collects its output, while also /// displaying it in real time. /// /// # Returns /// * `Ok((ExitStatus, Vec))` - The exit status and output lines if successful /// * `Err(Error)` - If the command fails pub fn run_with_tee(self) -> Result<(ExitStatus, Vec), Error> { assert!(self.stdout.is_none()); assert!(self.stderr.is_none()); run_with_tee( self.session, self.argv, self.cwd, self.user, self.env.as_ref(), self.stdin, self.quiet, ) } /// Run the command and analyze the output for problems. /// /// This method executes the command and analyzes its output for common /// build problems, returning a more detailed error when issues are detected. /// /// # Returns /// * `Ok(Vec)` - The output lines if successful /// * `Err(AnalyzedError)` - A detailed error if the command fails pub fn run_detecting_problems(self) -> Result, crate::analyze::AnalyzedError> { assert!(self.stdout.is_none()); assert!(self.stderr.is_none()); crate::analyze::run_detecting_problems( self.session, self.argv, None, self.quiet, self.cwd, self.user, self.env.as_ref(), self.stdin, ) } /// Run the command and attempt to fix any problems that occur. /// /// This method executes the command and applies fixes if it fails, /// potentially retrying multiple times with different fixers. /// /// # Arguments /// * `fixers` - List of fixers to try if the command fails /// /// # Returns /// * `Ok(Vec)` - The command output if successful /// * `Err(IterateBuildError)` - If the command fails and can't be fixed pub fn run_fixing_problems< I: std::error::Error, E: From + std::error::Error + From, >( self, fixers: &[&dyn crate::fix_build::BuildFixer], ) -> Result, crate::fix_build::IterateBuildError> { assert!(self.stdin.is_none()); assert!(self.stdout.is_none()); assert!(self.stderr.is_none()); crate::fix_build::run_fixing_problems( fixers, None, self.session, self.argv.as_slice(), self.quiet, self.cwd, self.user, self.env.as_ref(), ) } /// Start the command and return a handle to the running process. /// /// # Returns /// * `Ok(Child)` - A handle to the running process /// * `Err(Error)` - If starting the process fails pub fn child(self) -> Result { self.session.popen( self.argv, self.cwd, self.user, self.stdout, self.stderr, self.stdin, self.env.as_ref(), ) } /// Run the command and return its exit status. /// /// # Returns /// * `Ok(ExitStatus)` - The exit status if successful /// * `Err(Error)` - If the command fails pub fn run(self) -> Result { let mut p = self.child()?; let status = p.wait()?; Ok(status) } /// Run the command and return its output. /// /// # Returns /// * `Ok(Output)` - The command output if successful /// * `Err(Error)` - If the command fails pub fn output(self) -> Result { let p = self.child()?; let output = p.wait_with_output()?; Ok(output) } /// Run the command and check that it exits successfully. /// /// # Returns /// * `Ok(())` - If the command exited successfully /// * `Err(Error)` - If the command fails pub fn check_call(self) -> Result<(), Error> { self.session .check_call(self.argv, self.cwd, self.user, self.env) } /// Run the command and return its output. /// /// # Returns /// * `Ok(Vec)` - The command output if successful /// * `Err(Error)` - If the command fails pub fn check_output(self) -> Result, Error> { self.session .check_output(self.argv, self.cwd, self.user, self.env) } } /// Find the path to an executable in the session's PATH. /// /// # Arguments /// * `session` - The session to search in /// * `name` - The name of the executable to find /// /// # Returns /// The full path to the executable if found, or None if not found pub fn which(session: &dyn Session, name: &str) -> Option { let ret = match session.check_output( vec!["which", name], Some(std::path::Path::new("/")), None, None, ) { Ok(ret) => ret, Err(Error::CalledProcessError(status)) if status.code() == Some(1) => return None, Err(e) => panic!("Unexpected error: {:?}", e), }; if ret.is_empty() { None } else { Some(String::from_utf8(ret).unwrap().trim().to_owned()) } } /// Get the current user in the session. /// /// # Arguments /// * `session` - The session to get the user from /// /// # Returns /// The username of the current user pub fn get_user(session: &dyn Session) -> String { String::from_utf8( session .check_output( vec!["sh", "-c", "echo $USER"], Some(std::path::Path::new("/")), None, None, ) .unwrap(), ) .unwrap() .trim() .to_owned() } /// A function to capture and forward stdout and stderr of a child process. fn capture_output( mut child: std::process::Child, forward: bool, ) -> Result<(std::process::ExitStatus, Vec), std::io::Error> { use std::io::{BufRead, BufReader}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::thread; let mut output_log = Vec::::new(); // Channels to handle communication from threads let (tx, rx): (Sender>, Receiver>) = channel(); // Function to handle the stdout of the child process let stdout_tx = tx.clone(); let stdout = child.stdout.take().expect("Failed to capture stdout"); let stdout_handle = thread::spawn(move || -> Result<(), std::io::Error> { let reader = BufReader::new(stdout); for line in reader.lines() { let line = line?; if forward { std::io::stdout().write_all(line.as_bytes())?; std::io::stdout().write_all(b"\n")?; } stdout_tx .send(Some(line)) .expect("Failed to send stdout through channel"); } stdout_tx .send(None) .expect("Failed to send None through channel"); Ok(()) }); // Function to handle the stderr of the child process let stderr_tx = tx.clone(); let stderr = child.stderr.take().expect("Failed to capture stderr"); let stderr_handle = thread::spawn(move || -> Result<(), std::io::Error> { let reader = BufReader::new(stderr); for line in reader.lines() { let line = line?; if forward { std::io::stderr().write_all(line.as_bytes())?; std::io::stderr().write_all(b"\n")?; } stderr_tx .send(Some(line)) .expect("Failed to send stderr through channel"); } stderr_tx .send(None) .expect("Failed to send None through channel"); Ok(()) }); // Wait for the child process to exit let status = child.wait().expect("Child process wasn't running"); stderr_handle .join() .expect("Failed to join stderr thread")?; stdout_handle .join() .expect("Failed to join stdout thread")?; let mut terminated = 0; // Collect all output from both stdout and stderr while let Ok(line) = rx.recv() { if let Some(line) = line { output_log.push(line); } else { terminated += 1; if terminated == 2 { break; } } } Ok((status, output_log)) } /// Run a command and capture its output, while also displaying it. /// /// This function executes a command in the given session and collects /// its output, while also displaying it in real time. /// /// # Arguments /// * `session` - The session to run the command in /// * `args` - The command and its arguments /// * `cwd` - Optional current working directory /// * `user` - Optional user to run the command as /// * `env` - Optional environment variables /// * `stdin` - Optional stdin configuration /// * `quiet` - Whether to suppress output /// /// # Returns /// * `Ok((ExitStatus, Vec))` - The exit status and output lines if successful /// * `Err(Error)` - If the command fails pub fn run_with_tee( session: &dyn Session, args: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option<&std::collections::HashMap>, stdin: Option, quiet: bool, ) -> Result<(ExitStatus, Vec), Error> { if let (Some(cwd), Some(user)) = (cwd, user) { log::debug!("Running command: {:?} in {:?} as user {}", args, cwd, user); } else if let Some(cwd) = cwd { log::debug!("Running command: {:?} in {:?}", args, cwd); } else if let Some(user) = user { log::debug!("Running command: {:?} as user {}", args, user); } else { log::debug!("Running command: {:?}", args); } let p = session.popen( args, cwd, user, Some(std::process::Stdio::piped()), Some(std::process::Stdio::piped()), Some(stdin.unwrap_or(std::process::Stdio::null())), env, )?; // While the process is running, read its output and write it to stdout // *and* to the contents variable. Ok(capture_output(p, !quiet)?) } /// Create the user's home directory in the session. /// /// This function creates the user's home directory in the session, /// which is needed for some commands that write to the home directory. /// /// # Arguments /// * `session` - The session to create the home directory in /// /// # Returns /// * `Ok(())` if the home directory was created successfully /// * `Err(Error)` if creating the home directory fails pub fn create_home(session: &impl Session) -> Result<(), Error> { let cwd = std::path::Path::new("/"); let home = String::from_utf8(session.check_output( vec!["sh", "-c", "echo $HOME"], Some(cwd), None, None, )?) .unwrap() .trim_end_matches('\n') .to_string(); let user = String::from_utf8(session.check_output( vec!["sh", "-c", "echo $LOGNAME"], Some(cwd), None, None, )?) .unwrap() .trim_end_matches('\n') .to_string(); log::info!("Creating directory {} in schroot session.", home); session.check_call(vec!["mkdir", "-p", &home], Some(cwd), Some("root"), None)?; session.check_call(vec!["chown", &user, &home], Some(cwd), Some("root"), None)?; Ok(()) } #[cfg(test)] mod tests { #[test] fn test_get_user() { let session = super::plain::PlainSession::new(); let user = super::get_user(&session); assert!(!user.is_empty()); } #[test] fn test_which() { let session = super::plain::PlainSession::new(); let which = super::which(&session, "ls"); assert!(which.unwrap().ends_with("/ls")); } #[test] fn test_capture_and_forward_output() { let p = std::process::Command::new("echo") .arg("Hello, world!") .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(); let (status, output) = super::capture_output(p, false).unwrap(); assert!(status.success()); assert_eq!(output, vec!["Hello, world!"]); } } /// Test utilities for sessions #[cfg(test)] pub mod test_utils { /// Get a test session for use in tests. /// /// This returns an isolated session that's suitable for testing. /// On Linux, it tries to create an UnshareSession for better isolation. /// If that fails (e.g., no unshare permissions), it falls back to PlainSession. /// The session is isolated from the host system when possible. /// /// Returns None only if no session can be created at all. #[cfg(target_os = "linux")] pub fn get_test_session() -> Option> { // In CI environments like GitHub Actions, skip UnshareSession due to permission restrictions if std::env::var("GITHUB_ACTIONS").is_ok() { return Some(Box::new(super::plain::PlainSession::new())); } // Try to create an UnshareSession for isolation if let Ok(session) = super::unshare::UnshareSession::bootstrap() { return Some(Box::new(session)); } // Fall back to PlainSession if unshare isn't available Some(Box::new(super::plain::PlainSession::new())) } /// Get a test session for use in tests (non-Linux fallback). /// /// On non-Linux systems, returns a PlainSession for testing. #[cfg(not(target_os = "linux"))] pub fn get_test_session() -> Option> { Some(Box::new(super::plain::PlainSession::new())) } } ognibuild-0.2.6/src/session/plain.rs000064400000000000000000000325551046102023000155420ustar 00000000000000use crate::session::{CommandBuilder, Error, Project, Session}; /// A session implementation that runs commands on the local system. /// /// This is the simplest session implementation, which just runs commands /// directly on the host system without any isolation. pub struct PlainSession(std::path::PathBuf); impl Default for PlainSession { fn default() -> Self { Self::new() } } impl PlainSession { /// Create a new PlainSession. /// /// This creates a new session with the root directory (/) as the /// current working directory. /// /// # Returns /// A new PlainSession instance pub fn new() -> Self { PlainSession(std::path::PathBuf::from("/")) } fn prepend_user<'a>(&'a self, user: Option<&'a str>, mut args: Vec<&'a str>) -> Vec<&'a str> { if let Some(user) = user { if user != whoami::username() { args = vec!["sudo", "-u", user].into_iter().chain(args).collect(); } } args } } impl Session for PlainSession { fn location(&self) -> std::path::PathBuf { std::path::PathBuf::from("/") } fn exists(&self, path: &std::path::Path) -> bool { self.0.join(path).exists() } fn mkdir(&self, path: &std::path::Path) -> Result<(), Error> { std::fs::create_dir_all(self.0.join(path)).map_err(Error::IoError) } fn chdir(&mut self, path: &std::path::Path) -> Result<(), Error> { self.0 = self.0.join(path).canonicalize().unwrap(); Ok(()) } fn pwd(&self) -> &std::path::Path { &self.0 } fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf { self.0.join(path).canonicalize().unwrap() } fn check_output( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result, Error> { let argv = self.prepend_user(user, argv); let mut binding = std::process::Command::new(argv[0]); let mut cmd = binding.args(&argv[1..]); cmd = cmd.current_dir(cwd.unwrap_or(self.0.as_path())); if let Some(env) = env { cmd = cmd.envs(env); } let output = cmd.output(); match output { Ok(output) => { if output.status.success() { Ok(output.stdout) } else { Err(Error::CalledProcessError(output.status)) } } Err(e) => Err(Error::IoError(e)), } } fn rmtree(&self, path: &std::path::Path) -> Result<(), Error> { std::fs::remove_dir_all(path).map_err(Error::IoError) } fn check_call( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result<(), Error> { let argv = self.prepend_user(user, argv); let mut binding = std::process::Command::new(argv[0]); let mut cmd = binding.args(&argv[1..]); cmd = cmd.current_dir(cwd.unwrap_or(self.0.as_path())); if let Some(env) = env { cmd = cmd.envs(env); } let status = cmd.status(); match status { Ok(status) => { if status.success() { Ok(()) } else { Err(Error::CalledProcessError(status)) } } Err(e) => Err(Error::IoError(e)), } } fn create_home(&self) -> Result<(), Error> { Ok(()) } fn project_from_directory( &self, path: &std::path::Path, _subdir: Option<&str>, ) -> Result { Ok(Project::Noop(path.to_path_buf())) } fn popen( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, stdout: Option, stderr: Option, stdin: Option, env: Option<&std::collections::HashMap>, ) -> Result { let argv = self.prepend_user(user, argv); let mut binding = std::process::Command::new(argv[0]); let mut cmd = binding .args(&argv[1..]) .stdin(stdin.unwrap_or(std::process::Stdio::inherit())) .stdout(stdout.unwrap_or(std::process::Stdio::inherit())) .stderr(stderr.unwrap_or(std::process::Stdio::inherit())); let cwd = cwd.map_or_else(|| self.0.clone(), |p| self.0.join(p)); cmd = cmd.current_dir(cwd); if let Some(env) = env { cmd = cmd.envs(env); } Ok(cmd.spawn()?) } fn is_temporary(&self) -> bool { false } #[cfg(feature = "breezy")] fn project_from_vcs( &self, tree: &dyn crate::vcs::DupableTree, include_controldir: Option, subdir: Option<&str>, ) -> Result { use crate::vcs::dupe_vcs_tree; if include_controldir.unwrap_or(true) && tree.basedir().is_some() { // Optimization: just use the directory as-is, don't copy anything Ok(Project::Noop(tree.basedir().unwrap())) } else if !include_controldir.unwrap_or(false) { let td = tempfile::tempdir().unwrap(); let p = if let Some(subdir) = subdir { td.path().join(subdir) } else { td.path().to_path_buf() }; tree.export_to(&p, None).unwrap(); Ok(Project::Temporary { internal_path: p.clone(), external_path: p, td: td.keep(), }) } else { let td = tempfile::tempdir().unwrap(); let p = if let Some(subdir) = subdir { td.path().join(subdir) } else { td.path().to_path_buf() }; dupe_vcs_tree(tree, &p).unwrap(); Ok(Project::Temporary { internal_path: p.clone(), external_path: p, td: td.keep(), }) } } fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a> { CommandBuilder::new(self, argv) } fn read_dir(&self, path: &std::path::Path) -> Result, Error> { std::fs::read_dir(path) .map_err(Error::IoError)? .collect::, _>>() .map_err(Error::IoError) } } #[cfg(test)] mod tests { use super::*; #[cfg(any(feature = "breezy", feature = "debian"))] use breezyshim::WorkingTree; #[test] fn test_prepend_user() { let session = PlainSession::new(); let args = vec!["ls"]; let current_user = whoami::username(); let args = session.prepend_user(Some("root"), args); // If we're already root, don't prepend sudo if current_user == "root" { assert_eq!(args, vec!["ls"]); } else { assert_eq!(args, vec!["sudo", "-u", "root", "ls"]); } } #[test] fn test_prepend_user_no_user() { let session = PlainSession::new(); let args = vec!["ls"]; let args = session.prepend_user(None, args); assert_eq!(args, vec!["ls"]); } #[test] fn test_prepend_user_current_user() { let session = PlainSession::new(); let args = vec!["ls"]; let username = whoami::username(); let args = session.prepend_user(Some(username.as_str()), args); assert_eq!(args, vec!["ls"]); } #[test] fn test_location() { let session = PlainSession::new(); assert_eq!(session.location(), std::path::PathBuf::from("/")); } #[test] fn test_is_temporary() { let session = PlainSession::new(); assert!(!session.is_temporary()); } #[test] fn test_exists() { let session = PlainSession::new(); assert!(session.exists(std::path::Path::new("/"))); let td = tempfile::tempdir().unwrap(); assert!(session.exists(td.path())); let path = td.path().join("test"); assert!(!session.exists(&path)); } #[test] fn test_mkdir() { let session = PlainSession::new(); let td = tempfile::tempdir().unwrap(); let path = td.path().join("test"); session.mkdir(&path).unwrap(); assert!(session.exists(&path)); session.rmtree(&path).unwrap(); assert!(!session.exists(&path)); } #[test] fn test_chdir() { let mut session = PlainSession::new(); let td = tempfile::tempdir().unwrap(); let path = td.path().join("test"); session.mkdir(&path).unwrap(); session.chdir(&path).unwrap(); let pwd_bytes = session.check_output(vec!["pwd"], None, None, None).unwrap(); let reported = std::str::from_utf8(pwd_bytes.as_slice().strip_suffix(b"\n").unwrap()).unwrap(); assert_eq!(reported, path.canonicalize().unwrap().to_str().unwrap()); } #[test] fn test_pwd() { let mut session = PlainSession::new(); let pwd = session.pwd(); assert_eq!(pwd, std::path::Path::new("/")); let td = tempfile::tempdir().unwrap(); session.chdir(td.path()).unwrap(); let pwd = session.pwd(); assert_eq!(pwd, td.path().canonicalize().unwrap()); } #[test] fn test_external_path() { let session = PlainSession::new(); let td = tempfile::tempdir().unwrap(); let path = td.path().join("test"); session.mkdir(&path).unwrap(); assert_eq!(session.external_path(&path), path.canonicalize().unwrap()); } #[test] fn test_check_output() { let session = PlainSession::new(); let output = session .check_output(vec!["echo", "hello"], None, None, None) .unwrap(); assert_eq!(output, b"hello\n"); } #[test] fn test_check_call() { let session = PlainSession::new(); session.check_call(vec!["true"], None, None, None).unwrap(); } #[test] fn test_create_home() { let session = PlainSession::new(); session.create_home().unwrap(); } #[test] fn test_project_from_directory() { let session = PlainSession::new(); let td = tempfile::tempdir().unwrap(); let path = td.path().join("test"); session.mkdir(&path).unwrap(); let project = session.project_from_directory(&path, None).unwrap(); assert_eq!(project.external_path(), path); assert_eq!(project.internal_path(), path); } #[test] fn test_popen() { let session = PlainSession::new(); let child = session .popen( vec!["echo", "hello"], None, None, Some(std::process::Stdio::piped()), Some(std::process::Stdio::piped()), Some(std::process::Stdio::piped()), None, ) .unwrap(); let output = child.wait_with_output().unwrap(); assert_eq!(output.stdout, b"hello\n"); } #[cfg(feature = "breezy")] #[test] fn test_project_from_vcs() { // Trigger loading of test session before we mess up HOME #[cfg(target_os = "linux")] crate::session::unshare::test_session(); use breezyshim::tree::MutableTree; let env = breezyshim::testing::TestEnv::new(); let session = PlainSession::new(); let td = tempfile::tempdir().unwrap(); let tree = breezyshim::controldir::create_standalone_workingtree( td.path(), &breezyshim::controldir::ControlDirFormat::default(), ) .unwrap(); let path = td.path(); tree.put_file_bytes_non_atomic(std::path::Path::new("test"), b"hello") .unwrap(); tree.add(&[std::path::Path::new("test")]).unwrap(); tree.build_commit().message("test").commit().unwrap(); let project = session .project_from_vcs(&tree as &dyn crate::vcs::DupableTree, None, None) .unwrap(); assert_eq!(project.external_path(), path.canonicalize().unwrap()); assert_eq!(project.internal_path(), path.canonicalize().unwrap()); assert!(project.external_path().join(".bzr").exists()); let project = session .project_from_vcs(&tree as &dyn crate::vcs::DupableTree, Some(true), None) .unwrap(); assert_eq!(project.external_path(), path.canonicalize().unwrap()); assert_eq!(project.internal_path(), path.canonicalize().unwrap()); assert!(project.external_path().join(".bzr").exists()); let project = session .project_from_vcs(&tree as &dyn crate::vcs::DupableTree, Some(false), None) .unwrap(); assert_ne!(project.external_path(), path.canonicalize().unwrap()); assert_ne!(project.internal_path(), path.canonicalize().unwrap()); assert!(!project.external_path().join(".bzr").exists()); std::mem::drop(env); } #[test] fn test_output() { let session = PlainSession::new(); let output = session .command(vec!["echo", "hello"]) .stdout(std::process::Stdio::piped()) .output() .unwrap() .stdout; assert_eq!(output, b"hello\n"); } } ognibuild-0.2.6/src/session/schroot.rs000064400000000000000000000304701046102023000161120ustar 00000000000000use crate::session::{CommandBuilder, Error, Project, Session}; use std::io::{BufRead, Read}; extern crate rand; use rand::Rng; use std::iter; /// Sanitize the session name pub fn sanitize_session_name(name: &str) -> String { name.chars() .filter(|&c| c.is_alphanumeric() || "_-.".contains(c)) .collect() } /// Generate a session pub fn generate_session_id(prefix: &str) -> String { let suffix: String = String::from_utf8( iter::repeat(()) .map(|()| rand::rng().sample(rand::distr::Alphanumeric)) .take(8) .collect(), ) .unwrap(); format!("{}-{}", sanitize_session_name(prefix), suffix) } /// A schroot-based session pub struct SchrootSession { cwd: std::path::PathBuf, session_id: String, location: std::path::PathBuf, } impl SchrootSession { /// Create a schroot session pub fn new(chroot: &str, session_prefix: Option<&str>) -> Result { let mut stderr = tempfile::tempfile().unwrap(); let mut extra_args = vec![]; if let Some(session_prefix) = session_prefix { let sanitized_session_name = generate_session_id(session_prefix); extra_args.extend(["-n".to_string(), sanitized_session_name]); } let cmd = std::process::Command::new("schroot") .arg("-c") .arg(chroot) .arg("-b") .args(extra_args) .stderr(std::process::Stdio::from(stderr.try_clone().unwrap())) .output() .unwrap(); let session_id = match cmd.status.code() { Some(0) => String::from_utf8(cmd.stdout).unwrap(), Some(_) => { let mut errlines = String::new(); stderr.read_to_string(&mut errlines).unwrap(); if errlines.len() == 1 { return Err(Error::SetupFailure( errlines.lines().next().unwrap().to_string(), errlines, )); } else if errlines.is_empty() { return Err(Error::SetupFailure( "No output from schroot".to_string(), errlines, )); } else { return Err(Error::SetupFailure( errlines.lines().last().unwrap().to_string(), errlines, )); } } None => panic!("schroot exited by signal"), }; log::info!("Opened schroot session {} (from {})", session_id, chroot); let output = std::process::Command::new("schroot") .arg("-c") .arg(format!("session:{}", session_id)) .arg("--location") .output() .unwrap(); let location = std::path::PathBuf::from( String::from_utf8(output.stdout) .unwrap() .trim_end_matches('\n'), ); Ok(Self { cwd: std::path::PathBuf::from("/"), session_id, location, }) } fn run_argv( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option<&std::collections::HashMap>, ) -> Vec { let mut argv = argv.iter().map(|x| x.to_string()).collect::>(); let mut base_argv = vec![ "schroot".to_string(), "-r".to_string(), "-c".to_string(), format!("session:{}", self.session_id), ]; let cwd = cwd.unwrap_or(self.pwd()); base_argv.extend([ "-d".to_string(), cwd.to_path_buf().to_string_lossy().to_string(), ]); if let Some(user) = user { base_argv.extend(["-u".to_string(), user.to_string()]); } if let Some(env) = env { argv = vec![ "sh".to_string(), "-c".to_string(), env.iter() .map(|(key, value)| format!("{}={} ", key, shlex::try_quote(value).unwrap())) .chain( argv.iter() .map(|x| shlex::try_quote(x).unwrap().to_string()), ) .collect::>() .join(" "), ]; } [base_argv, vec!["--".to_string()], argv].concat() } fn build_tempdir(&self) -> std::path::PathBuf { let build_dir = "/build"; String::from_utf8( self.check_output( vec!["mktemp", "-d", "-p", build_dir], Some(std::path::Path::new("/")), None, None, ) .unwrap(), ) .unwrap() .trim_end_matches('\n') .to_string() .into() } } impl Drop for SchrootSession { fn drop(&mut self) { let stderr = tempfile::tempfile().unwrap(); match std::process::Command::new("schroot") .arg("-c") .arg(format!("session:{}", self.session_id)) .arg("-e") .stderr(std::process::Stdio::from(stderr.try_clone().unwrap())) .output() { Err(_) => { for line in std::io::BufReader::new(&stderr).lines() { let line = line.unwrap(); if let Some(rest) = line.strip_prefix("E: ") { log::error!("{}", rest); } } log::error!( "Failed to close schroot session {}, leaving stray.", self.session_id ); } Ok(_) => { log::debug!("Closed schroot session {}", self.session_id); } } } } impl Session for SchrootSession { fn rmtree(&self, path: &std::path::Path) -> Result<(), Error> { let fullpath = self.external_path(path); std::fs::remove_dir_all(fullpath).map_err(Error::IoError) } fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf { let path = path.to_string_lossy(); if let Some(rest) = path.strip_prefix('/') { return self.location().join(rest); } self.location() .join( self.cwd .to_string_lossy() .to_string() .trim_start_matches('/'), ) .join(path.as_ref()) } fn location(&self) -> std::path::PathBuf { self.location.clone() } fn exists(&self, path: &std::path::Path) -> bool { let fullpath = self.external_path(path); fullpath.exists() } fn chdir(&mut self, path: &std::path::Path) -> Result<(), Error> { self.cwd = self.cwd.join(path); Ok(()) } fn pwd(&self) -> &std::path::Path { &self.cwd } fn mkdir(&self, path: &std::path::Path) -> Result<(), Error> { let fullpath = self.external_path(path); std::fs::create_dir_all(fullpath).map_err(Error::IoError) } fn check_output( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result, Error> { let argv = self.run_argv(argv, cwd, user, env.as_ref()); let output = std::process::Command::new(&argv[0]) .args(&argv[1..]) .stderr(std::process::Stdio::inherit()) .output(); match output { Ok(output) => { if output.status.success() { Ok(output.stdout) } else { Err(Error::CalledProcessError(output.status)) } } Err(e) => Err(Error::IoError(e)), } } fn check_call( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result<(), Error> { let argv = self.run_argv(argv, cwd, user, env.as_ref()); let status = std::process::Command::new(&argv[0]) .args(&argv[1..]) .status(); match status { Ok(status) => { if status.success() { Ok(()) } else { Err(Error::CalledProcessError(status)) } } Err(e) => Err(Error::IoError(e)), } } fn create_home(&self) -> Result<(), Error> { crate::session::create_home(self) } fn project_from_directory( &self, path: &std::path::Path, subdir: Option<&str>, ) -> Result { let subdir = subdir.unwrap_or("package"); let reldir = self.build_tempdir(); let export_directory = self.external_path(&reldir).join(subdir); // Copy tree from path to export_directory let mut options = fs_extra::dir::CopyOptions::new(); options.copy_inside = true; // Copy contents inside the source directory options.content_only = false; // Copy the entire directory options.skip_exist = false; // Skip if file already exists in the destination options.overwrite = true; // Overwrite files if they already exist options.buffer_size = 64000; // Buffer size in bytes options.depth = 0; // Recursion depth (0 for unlimited depth) // Perform the copy operation fs_extra::dir::copy(path, &export_directory, &options).unwrap(); Ok(Project::Temporary { external_path: export_directory, internal_path: reldir.join(subdir), td: self.external_path(&reldir), }) } fn popen( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, stdout: Option, stderr: Option, stdin: Option, env: Option<&std::collections::HashMap>, ) -> Result { let argv = self.run_argv(argv, cwd, user, env); Ok(std::process::Command::new(&argv[0]) .args(&argv[1..]) .stdin(stdin.unwrap_or(std::process::Stdio::inherit())) .stdout(stdout.unwrap_or(std::process::Stdio::inherit())) .stderr(stderr.unwrap_or(std::process::Stdio::inherit())) .spawn()?) } fn is_temporary(&self) -> bool { true } #[cfg(feature = "breezy")] fn project_from_vcs( &self, tree: &dyn crate::vcs::DupableTree, include_controldir: Option, subdir: Option<&str>, ) -> Result { let reldir = self.build_tempdir(); let subdir = subdir.unwrap_or("package"); let export_directory = self.external_path(&reldir).join(subdir); if !include_controldir.unwrap_or(false) { tree.export_to(&export_directory, None).unwrap(); } else { crate::vcs::dupe_vcs_tree(tree, &export_directory).unwrap(); } Ok(Project::Temporary { external_path: export_directory, internal_path: reldir.join(subdir), td: self.external_path(&reldir), }) } fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a> { CommandBuilder::new(self, argv) } fn read_dir(&self, path: &std::path::Path) -> Result, Error> { std::fs::read_dir(self.external_path(path)) .map_err(Error::IoError)? .collect::, _>>() .map_err(Error::IoError) } } #[cfg(test)] mod tests { #[test] fn test_sanitize_session_name() { assert_eq!(super::sanitize_session_name("foo"), "foo"); assert_eq!(super::sanitize_session_name("foo-bar"), "foo-bar"); assert_eq!(super::sanitize_session_name("foo_bar"), "foo_bar"); assert_eq!(super::sanitize_session_name("foo.bar"), "foo.bar"); assert_eq!(super::sanitize_session_name("foo!bar"), "foobar"); assert_eq!(super::sanitize_session_name("foo@bar"), "foobar"); } #[test] fn test_generate_session_id() { let id = super::generate_session_id("foo"); assert_eq!(id.len(), 12); assert_eq!(&id[..4], "foo-"); } } ognibuild-0.2.6/src/session/unshare.rs000064400000000000000000001022721046102023000160760ustar 00000000000000use crate::session::{CommandBuilder, Error, ImageError, Project, Session}; use std::path::{Path, PathBuf}; /// An unshare based session pub struct UnshareSession { root: PathBuf, _tempdir: Option, cwd: PathBuf, } fn compression_flag(path: &Path) -> Result, crate::session::Error> { match path.extension().unwrap().to_str().unwrap() { "tar" => Ok(None), "gz" => Ok(Some("-z")), "bz2" => Ok(Some("-j")), "xz" => Ok(Some("-J")), "zst" => Ok(Some("--zstd")), e => Err(crate::session::Error::SetupFailure( "unknown extension".to_string(), format!("unknown extension: {}", e), )), } } /// Get the path to a cached Debian tarball if it exists /// /// # Arguments /// * `suite` - The Debian suite to use (e.g., "sid", "bookworm") /// /// # Returns /// * `Option` - Path to the cached tarball if it exists pub fn cached_debian_tarball_path(suite: &str) -> Result { let arch = std::env::consts::ARCH; let arch_name = match arch { "x86_64" => "amd64", "aarch64" => "arm64", other => other, }; // Use ~/.cache/ognibuild/images/ for caching let base_cache_dir = dirs::cache_dir() .ok_or_else(|| crate::session::Error::ImageError(ImageError::NoCachedImage))?; let cache_dir = base_cache_dir.join("ognibuild").join("images"); let tarball_name = format!("debian-{}-{}.tar.gz", suite, arch_name); Ok(cache_dir.join(&tarball_name)) } impl UnshareSession { /// Create a cached Debian session from a cloud image /// /// Looks for a cached tarball in ~/.cache/ognibuild/images/debian-{suite}-{arch}.tar.xz /// # Arguments /// * `suite` - The Debian suite to use (e.g., "sid", "bookworm") pub fn cached_debian_session(suite: &str) -> Result { let tarball_path = cached_debian_tarball_path(suite)?; if !tarball_path.exists() { Err(Error::ImageError(ImageError::NoCachedImage)) } else { log::info!( "Using cached Debian {} image from: {}", suite, tarball_path.display() ); Self::from_tarball(&tarball_path) } } /// Create a session from a tarball pub fn from_tarball(path: &Path) -> Result { let td = tempfile::tempdir().map_err(|e| { crate::session::Error::SetupFailure("tempdir failed".to_string(), e.to_string()) })?; // Run tar within unshare to extract the tarball. This is necessary because // the tarball may contain files that are owned by a different user. // // However, the tar executable is not available within the unshare environment. // Therefore, we need to extract the tarball to a temporary directory and then // move it to the final location. let root = td.path(); let f = std::fs::File::open(path).map_err(|e| { crate::session::Error::SetupFailure("open failed".to_string(), e.to_string()) })?; // Create necessary directories for mounting before extraction // These might not exist in cloud images for dir in &["proc", "sys", "dev"] { std::fs::create_dir_all(root.join(dir)).map_err(|e| { crate::session::Error::SetupFailure( format!("Failed to create {} directory", dir), e.to_string(), ) })?; } let output = std::process::Command::new("unshare") .arg("--map-users=auto") .arg("--map-groups=auto") .arg("--fork") .arg("--pid") .arg("--mount-proc") .arg("--net") .arg("--uts") .arg("--ipc") .arg("--wd") .arg(root) .arg("--") .arg("tar") .arg("x") .arg(compression_flag(path)?.unwrap_or("--")) .stdin(std::process::Stdio::from(f)) .stderr(std::process::Stdio::piped()) .output()?; if !output.status.success() { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(crate::session::Error::SetupFailure( "tar failed".to_string(), stderr, )); } let s = Self { root: root.to_path_buf(), _tempdir: Some(td), cwd: std::path::PathBuf::from("/"), }; s.ensure_current_user()?; Ok(s) } /// Save the session to a tarball pub fn save_to_tarball(&self, path: &Path) -> Result<(), crate::session::Error> { // Create the tarball from within the session, dumping it to stdout let mut child = self.popen( vec![ "tar", "c", "--absolute-names", "--exclude", "/dev/*", "--exclude", "/proc/*", "--exclude", "/sys/*", compression_flag(path)?.unwrap_or("--"), "/", ], Some(std::path::Path::new("/")), Some("root"), Some(std::process::Stdio::piped()), None, None, None, )?; let f = std::fs::File::create(path).map_err(|e| { crate::session::Error::SetupFailure("create failed".to_string(), e.to_string()) })?; let mut writer = std::io::BufWriter::new(f); std::io::copy(child.stdout.as_mut().unwrap(), &mut writer).map_err(|e| { crate::session::Error::SetupFailure("copy failed".to_string(), e.to_string()) })?; if child.wait()?.success() { Ok(()) } else { Err(crate::session::Error::SetupFailure( "tar failed".to_string(), "tar failed".to_string(), )) } } /// Bootstrap the session environment with Debian sid pub fn bootstrap() -> Result { bootstrap_debian_tarball("sid", true) } /// Verify that the current user has an account in the session pub fn ensure_current_user(&self) -> Result<(), crate::session::Error> { // Ensure that the current user has an entry in /etc/passwd let user = whoami::username(); let uid = nix::unistd::getuid().to_string(); let gid = nix::unistd::getgid().to_string(); match self.check_call( vec![ "/usr/sbin/groupadd", "--force", "--non-unique", "--gid", &gid, user.as_str(), ], Some(std::path::Path::new("/")), Some("root"), None, ) { Ok(_) => {} Err(e) => panic!("Error: {:?}", e), } let child = self.popen( vec![ "/usr/sbin/useradd", "--uid", &uid, "--gid", &gid, user.as_str(), ], Some(std::path::Path::new("/")), Some("root"), None, Some(std::process::Stdio::piped()), None, None, )?; match child.wait_with_output() { Ok(output) => { match output.status.code() { // User created Some(0) => Ok(()), // Ignore if user already exists Some(9) => Ok(()), Some(4) => Ok(()), _ => panic!( "Error: {:?}: {}", output.status, String::from_utf8(output.stdout).unwrap() ), } } Err(e) => panic!("Error: {:?}", e), } } /// Run a command in the session pub fn run_argv<'a>( &'a self, argv: Vec<&'a str>, cwd: Option<&'a std::path::Path>, user: Option<&'a str>, ) -> std::vec::Vec<&'a str> { let mut ret = vec![ "unshare", "--map-users=auto", "--map-groups=auto", "--fork", "--pid", "--mount-proc", "--net", "--uts", "--ipc", "--root", self.root.to_str().unwrap(), "--wd", cwd.unwrap_or(&self.cwd).to_str().unwrap(), ]; if let Some(user) = user { if user == "root" { ret.push("--map-root-user") } else { ret.push("--map-user"); ret.push(user); } } else { ret.push("--map-current-user") } ret.push("--"); ret.extend(argv); ret } fn build_tempdir(&self, user: Option<&str>) -> std::path::PathBuf { let build_dir = "/build"; // Ensure that the build directory exists self.check_call(vec!["mkdir", "-p", build_dir], None, user, None) .unwrap(); String::from_utf8( self.check_output( vec!["mktemp", "-d", format!("--tmpdir={}", build_dir).as_str()], Some(std::path::Path::new("/")), user, None, ) .unwrap(), ) .unwrap() .trim_end_matches('\n') .to_string() .into() } } /// Create a Debian UnshareSession for testing, with fallback options /// /// This function tries the following in order: /// 1. If OGNIBUILD_DEBIAN_TEST_TARBALL is set, use that tarball /// 2. If a cached image exists, use it /// 3. Otherwise, bootstrap from network using mmdebstrap /// /// # Arguments /// * `suite` - The Debian suite to use (e.g., "sid", "unstable", "bookworm", "stable") pub fn create_debian_session_for_testing( suite: &str, allow_network: bool, ) -> Result { // Check if a custom tarball path is provided for testing if let Ok(tarball_path) = std::env::var("OGNIBUILD_DEBIAN_TEST_TARBALL") { let path = Path::new(&tarball_path); if path.exists() { log::info!( "Using Debian test tarball from OGNIBUILD_DEBIAN_TEST_TARBALL: {}", tarball_path ); return UnshareSession::from_tarball(path); } else { return Err(Error::SetupFailure( "Tarball not found".to_string(), format!( "OGNIBUILD_DEBIAN_TEST_TARBALL points to non-existent file: {}", tarball_path ), )); } } // Try to use cached session first (without downloading if not present) match UnshareSession::cached_debian_session(suite) { Ok(session) => { log::info!("Using cached Debian {} image", suite); return Ok(session); } Err(Error::ImageError(ImageError::NoCachedImage)) => { log::debug!("No cached image available for Debian {}", suite); // Continue to next option: bootstrap from network } Err(Error::ImageError(ImageError::CachedImageNotFound { path })) => { log::debug!("Cached image not found at {}", path.display()); // Continue to next option: bootstrap from network } Err(e) => return Err(e), // Other errors should propagate } if !allow_network { return Err(Error::ImageError(ImageError::NoCachedImage)); } // Default: bootstrap from network log::info!( "No cached image found, bootstrapping Debian {} test session from network using mmdebstrap", suite ); bootstrap_debian_tarball(suite, true) } /// Bootstrap a Debian system using mmdebstrap and create a tarball /// /// # Arguments /// * `suite` - The Debian suite to use (e.g., "sid", "unstable", "bookworm", "stable") /// * `setup_apt_file` - Whether to install and configure apt-file during bootstrap (requires network) pub fn bootstrap_debian_tarball( suite: &str, setup_apt_file: bool, ) -> Result { let td = tempfile::tempdir().map_err(|e| { crate::session::Error::SetupFailure("tempdir failed".to_string(), e.to_string()) })?; let root = td.path(); // Build mmdebstrap command let mut cmd = std::process::Command::new("mmdebstrap"); cmd.current_dir(root) .arg("--mode=unshare") .arg("--variant=minbase"); // Conditionally add apt-file setup if requested if setup_apt_file { log::info!("Including apt-file in bootstrap (this requires network access)"); cmd.arg("--include=apt-file") // Install apt-file package during bootstrap .arg("--customize-hook=chroot \"$1\" apt-file update") // Download Contents files .arg("--skip=cleanup/apt/lists"); // Preserve apt lists (Contents files) for apt-file } cmd.arg("--quiet") .arg(suite) .arg(root) .arg("http://deb.debian.org/debian/"); let status = cmd.status().map_err(|e| { crate::session::Error::SetupFailure( "mmdebstrap command not found or failed to execute".to_string(), format!("Failed to run mmdebstrap (ensure it's installed): {}", e), ) })?; if !status.success() { return Err(crate::session::Error::SetupFailure( "mmdebstrap failed".to_string(), format!("mmdebstrap exited with status: {}. This likely requires network access to http://deb.debian.org/debian/", status), )); } let s = UnshareSession { root: root.to_path_buf(), _tempdir: Some(td), cwd: std::path::PathBuf::from("/"), }; s.ensure_current_user()?; Ok(s) } impl Session for UnshareSession { fn chdir(&mut self, path: &std::path::Path) -> Result<(), crate::session::Error> { self.cwd = self.cwd.join(path); Ok(()) } fn pwd(&self) -> &std::path::Path { &self.cwd } fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf { if let Ok(rest) = path.strip_prefix("/") { return self.location().join(rest); } self.location() .join( self.cwd .to_string_lossy() .to_string() .trim_start_matches('/'), ) .join(path) } fn location(&self) -> std::path::PathBuf { self.root.clone() } fn check_output( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result, super::Error> { let argv = self.run_argv(argv, cwd, user); let output = std::process::Command::new(argv[0]) .args(&argv[1..]) .stderr(std::process::Stdio::inherit()) .envs(env.unwrap_or_default()) .output(); match output { Ok(output) => { if output.status.success() { Ok(output.stdout) } else { Err(Error::CalledProcessError(output.status)) } } Err(e) => Err(Error::IoError(e)), } } fn create_home(&self) -> Result<(), super::Error> { crate::session::create_home(self) } fn check_call( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, env: Option>, ) -> Result<(), crate::session::Error> { let argv = self.run_argv(argv, cwd, user); let status = std::process::Command::new(argv[0]) .args(&argv[1..]) .envs(env.unwrap_or_default()) .status(); match status { Ok(status) => { if status.success() { Ok(()) } else { Err(Error::CalledProcessError(status)) } } Err(e) => Err(Error::IoError(e)), } } fn exists(&self, path: &std::path::Path) -> bool { let args = vec!["test", "-e", path.to_str().unwrap()]; self.check_call(args, None, None, None).is_ok() } fn mkdir(&self, path: &std::path::Path) -> Result<(), crate::session::Error> { let args = vec!["mkdir", path.to_str().unwrap()]; self.check_call(args, None, None, None) } fn rmtree(&self, path: &std::path::Path) -> Result<(), crate::session::Error> { let args = vec!["rm", "-rf", path.to_str().unwrap()]; self.check_call(args, None, None, None) } fn project_from_directory( &self, path: &std::path::Path, subdir: Option<&str>, ) -> Result { let subdir = subdir.unwrap_or("package"); let reldir = self.build_tempdir(Some("root")); let export_directory = self.external_path(&reldir).join(subdir); // Copy tree from path to export_directory let mut options = fs_extra::dir::CopyOptions::new(); options.copy_inside = true; // Copy contents inside the source directory options.content_only = false; // Copy the entire directory options.skip_exist = false; // Skip if file already exists in the destination options.overwrite = true; // Overwrite files if they already exist options.buffer_size = 64000; // Buffer size in bytes options.depth = 0; // Recursion depth (0 for unlimited depth) // Perform the copy operation fs_extra::dir::copy(path, &export_directory, &options).unwrap(); Ok(Project::Temporary { external_path: export_directory, internal_path: reldir.join(subdir), td: self.external_path(&reldir), }) } fn popen( &self, argv: Vec<&str>, cwd: Option<&std::path::Path>, user: Option<&str>, stdout: Option, stderr: Option, stdin: Option, env: Option<&std::collections::HashMap>, ) -> Result { let argv = self.run_argv(argv, cwd, user); let mut binding = std::process::Command::new(argv[0]); let mut cmd = binding.args(&argv[1..]); if let Some(env) = env { cmd = cmd.envs(env); } if let Some(stdin) = stdin { cmd = cmd.stdin(stdin); } if let Some(stdout) = stdout { cmd = cmd.stdout(stdout); } if let Some(stderr) = stderr { cmd = cmd.stderr(stderr); } Ok(cmd.spawn()?) } fn is_temporary(&self) -> bool { true } #[cfg(feature = "breezy")] fn project_from_vcs( &self, tree: &dyn crate::vcs::DupableTree, include_controldir: Option, subdir: Option<&str>, ) -> Result { let reldir = self.build_tempdir(None); let subdir = subdir.unwrap_or("package"); let export_directory = self.external_path(&reldir).join(subdir); if !include_controldir.unwrap_or(false) { tree.export_to(&export_directory, None).unwrap(); } else { crate::vcs::dupe_vcs_tree(tree, &export_directory).unwrap(); } Ok(Project::Temporary { external_path: export_directory, internal_path: reldir.join(subdir), td: self.external_path(&reldir), }) } fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a> { CommandBuilder::new(self, argv) } fn read_dir(&self, path: &std::path::Path) -> Result, Error> { std::fs::read_dir(self.external_path(path)) .map_err(Error::IoError)? .collect::, _>>() .map_err(Error::IoError) } } #[cfg(test)] lazy_static::lazy_static! { static ref TEST_SESSION: std::sync::Mutex = std::sync::Mutex::new( create_debian_session_for_testing("sid", false) .expect("Failed to create test session. This requires network access.\nYou can avoid this by:\n - Pre-caching with: ogni cache-env sid\n - Setting: OGNIBUILD_DEBIAN_TEST_TARBALL=/path/to/tarball.tar.xz") ); } #[cfg(test)] pub(crate) fn test_session() -> Option> { // Don't run tests if we're in github actions (CI environment restrictions) if std::env::var("GITHUB_ACTIONS").is_ok() { return None; } // Handle poisoned mutex: if a previous test panicked while holding the lock, // we recover the guard to allow tests to continue match TEST_SESSION.lock() { Ok(guard) => Some(guard), Err(poisoned) => { // Recover from poisoned mutex - this is safe because UnshareSession // doesn't have invalid states that could cause issues after a panic Some(poisoned.into_inner()) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_temporary() { let session = if let Some(session) = test_session() { session } else { return; }; assert!(session.is_temporary()); } #[test] fn test_chdir() { let mut session = if let Some(session) = test_session() { session } else { return; }; session.chdir(std::path::Path::new("/")).unwrap(); } #[test] fn test_check_output() { let session = if let Some(session) = test_session() { session } else { return; }; let output = String::from_utf8( session .check_output(vec!["ls"], Some(std::path::Path::new("/")), None, None) .unwrap(), ) .unwrap(); let dirs = output.split_whitespace().collect::>(); assert!(dirs.contains(&"bin")); assert!(dirs.contains(&"dev")); assert!(dirs.contains(&"etc")); assert!(dirs.contains(&"home")); assert!(dirs.contains(&"lib")); assert!(dirs.contains(&"usr")); assert!(dirs.contains(&"proc")); assert_eq!( "root", String::from_utf8( session .check_output(vec!["whoami"], None, Some("root"), None) .unwrap() ) .unwrap() .trim_end() ); assert_eq!( // Get current process uid String::from_utf8( session .check_output(vec!["id", "-u"], None, None, None) .unwrap() ) .unwrap() .trim_end(), String::from_utf8( session .check_output(vec!["id", "-u"], None, None, None) .unwrap() ) .unwrap() .trim_end() ); assert_eq!( "nobody", String::from_utf8( session .check_output(vec!["whoami"], None, Some("nobody"), None) .unwrap() ) .unwrap() .trim_end() ); } #[test] fn test_check_call() { let session = if let Some(session) = test_session() { session } else { return; }; session .check_call(vec!["true"], Some(std::path::Path::new("/")), None, None) .unwrap(); } #[test] fn test_create_home() { let session = if let Some(session) = test_session() { session } else { return; }; session.create_home().unwrap(); } fn save_and_reuse(name: &str) { let session = if let Some(session) = test_session() { session } else { return; }; let tempdir = tempfile::tempdir().unwrap(); let path = tempdir.path().join(name); session.save_to_tarball(&path).unwrap(); std::mem::drop(session); let session = UnshareSession::from_tarball(&path).unwrap(); assert!(session.exists(std::path::Path::new("/bin"))); // Verify that the session works let output = String::from_utf8( session .check_output(vec!["ls"], Some(std::path::Path::new("/")), None, None) .unwrap(), ) .unwrap(); let dirs = output.split_whitespace().collect::>(); assert!(dirs.contains(&"bin")); assert!(dirs.contains(&"dev")); assert!(dirs.contains(&"etc")); assert!(dirs.contains(&"home")); assert!(dirs.contains(&"lib")); } #[test] fn test_save_and_reuse() { save_and_reuse("test.tar"); } #[test] fn test_save_and_reuse_gz() { save_and_reuse("test.tar.gz"); } #[test] fn test_mkdir_rmdir() { let session = if let Some(session) = test_session() { session } else { return; }; let path = std::path::Path::new("/tmp/test"); session.mkdir(path).unwrap(); assert!(session.exists(path)); session.rmtree(path).unwrap(); assert!(!session.exists(path)); } #[test] fn test_project_from_directory() { let session = if let Some(session) = test_session() { session } else { return; }; let tempdir = tempfile::tempdir().unwrap(); std::fs::write(tempdir.path().join("test"), "test").unwrap(); let project = session .project_from_directory(tempdir.path(), None) .unwrap(); assert!(project.external_path().exists()); assert!(session.exists(project.internal_path())); session.rmtree(project.internal_path()).unwrap(); assert!(!session.exists(project.internal_path())); assert!(!project.external_path().exists()); } #[test] fn test_session_works_after_panic() { // Skip if we're in CI if std::env::var("GITHUB_ACTIONS").is_ok() { return; } // First, verify we can get the session normally let session1 = test_session().unwrap(); assert!(session1.exists(std::path::Path::new("/bin"))); std::mem::drop(session1); // Now cause a panic while holding the lock let result = std::panic::catch_unwind(|| { let _session = test_session().unwrap(); panic!("Intentional panic to test recovery"); }); // Verify the panic happened assert!(result.is_err()); // Now verify we can still get the session (it shouldn't be blocked) let session2 = test_session().unwrap(); assert!(session2.exists(std::path::Path::new("/bin"))); // Verify the session is still functional by running a command session2 .check_call(vec!["true"], Some(std::path::Path::new("/")), None, None) .unwrap(); } #[test] fn test_cached_debian_session_no_download() { // Test that cached_debian_session returns the correct error when download is not allowed // and no cached file exists let result = UnshareSession::cached_debian_session("test-suite-nonexistent"); assert!(result.is_err()); if let Err(err) = result { assert!( matches!( err, crate::session::Error::ImageError( crate::session::ImageError::CachedImageNotFound { .. } | crate::session::ImageError::NoCachedImage ) ), "Expected CachedImageNotFound error, got {:?}", err ); } } #[test] fn test_cached_debian_session_unsupported_arch() { // This test will only work on architectures that are not x86_64 or aarch64 let arch = std::env::consts::ARCH; if arch == "x86_64" || arch == "aarch64" { // Skip this test on supported architectures return; } let result = UnshareSession::cached_debian_session("sid"); assert!(result.is_err()); if let Err(err) = result { assert!( matches!( err, crate::session::Error::ImageError( crate::session::ImageError::UnsupportedArchitecture { .. } ) ), "Expected UnsupportedArchitecture error, got {:?}", err ); } } #[test] fn test_create_debian_session_with_env_var() { // Test that create_debian_session_for_testing respects OGNIBUILD_DEBIAN_TEST_TARBALL let temp_dir = tempfile::tempdir().unwrap(); let tarball_path = temp_dir.path().join("test.tar.xz"); // Create a minimal test tarball (invalid but exists) std::fs::write(&tarball_path, b"test").unwrap(); // Set the environment variable to use this tarball std::env::set_var( "OGNIBUILD_DEBIAN_TEST_TARBALL", tarball_path.to_str().unwrap(), ); // This should attempt to use the tarball (will fail because it's not valid, but that's ok) let result = create_debian_session_for_testing("sid", false); // Clean up std::env::remove_var("OGNIBUILD_DEBIAN_TEST_TARBALL"); // We expect this to fail because our test tarball is not valid, // but it should fail in from_tarball with a SetupFailure, not because the file doesn't exist assert!(result.is_err()); if let Err(err) = result { // Should be a SetupFailure from tar extraction, not a file not found error assert!( matches!(err, crate::session::Error::SetupFailure(_, _)), "Expected SetupFailure from tar extraction, got {:?}", err ); } } #[test] fn test_create_debian_session_nonexistent_tarball() { // Test that pointing to a non-existent tarball gives the right error std::env::set_var( "OGNIBUILD_DEBIAN_TEST_TARBALL", "/nonexistent/path/tarball.tar.xz", ); let result = create_debian_session_for_testing("sid", false); std::env::remove_var("OGNIBUILD_DEBIAN_TEST_TARBALL"); assert!(result.is_err()); if let Err(err) = result { // Should be a SetupFailure about non-existent file match err { crate::session::Error::SetupFailure(msg, detail) => { assert!( detail.contains("non-existent file"), "Expected error about non-existent file, got: {}", detail ); } _ => panic!("Expected SetupFailure, got {:?}", err), } } } #[cfg(not(feature = "debian"))] #[test] fn test_cached_debian_session_no_debian_feature() { // When debian feature is not enabled, downloading should return DownloadNotAvailable error let result = UnshareSession::cached_debian_session("sid"); // If the cache doesn't exist, it should fail with DownloadNotAvailable // (assuming the cache doesn't exist for this test) if result.is_err() { if let Err(err) = result { // Could be CachedImageNotFound if cache exists, or DownloadNotAvailable if trying to download assert!( matches!(err, crate::session::Error::ImageError(_)), "Expected ImageError, got {:?}", err ); } } } #[test] fn test_popen() { let session = if let Some(session) = test_session() { session } else { return; }; let child = session .popen( vec!["ls"], Some(std::path::Path::new("/")), None, Some(std::process::Stdio::piped()), Some(std::process::Stdio::piped()), Some(std::process::Stdio::piped()), None, ) .unwrap(); let output = String::from_utf8(child.wait_with_output().unwrap().stdout).unwrap(); let dirs = output.split_whitespace().collect::>(); assert!(dirs.contains(&"etc")); assert!(dirs.contains(&"home")); assert!(dirs.contains(&"lib")); assert!(dirs.contains(&"usr")); assert!(dirs.contains(&"proc")); } #[test] fn test_external_path() { let mut session = if let Some(session) = test_session() { session } else { return; }; // Test absolute path let path = std::path::Path::new("/tmp/test"); assert_eq!( session.external_path(path), session.location().join("tmp/test") ); // Test relative path session.chdir(std::path::Path::new("/tmp")).unwrap(); let path = std::path::Path::new("test"); assert_eq!( session.external_path(path), session.location().join("tmp/test") ); } } ognibuild-0.2.6/src/shebang.rs000064400000000000000000000044571046102023000143630ustar 00000000000000use std::io::BufRead; use std::os::unix::fs::PermissionsExt; /// Work out what binary is necessary to run a script based on shebang /// /// # Arguments /// * `path` - Path to the script /// /// # Returns /// * `Ok(Some(binary))` - The binary necessary to run the script pub fn shebang_binary(path: &std::path::Path) -> std::io::Result> { let file = std::fs::File::open(path)?; if file.metadata()?.permissions().mode() & 0o111 == 0 { return Ok(None); } let bufreader = std::io::BufReader::new(file); let firstline = bufreader.lines().next(); let firstline = match firstline { Some(line) => line?, None => return Ok(None), }; if !firstline.starts_with("#!") { return Ok(None); } let args: Vec<&str> = firstline[2..].split_whitespace().collect(); let binary = if args[0] == "/usr/bin/env" || args[0] == "env" { args[1] } else { args[0] }; Ok(Some( std::path::Path::new(binary) .file_name() .unwrap() .to_string_lossy() .to_string(), )) } #[cfg(test)] mod tests { use super::*; fn assert_shebang(content: &str, executable: bool, expected: Option<&str>) { let td = tempfile::tempdir().unwrap(); let path = td.path().join("test.sh"); std::fs::write(&path, content).unwrap(); if executable { let mut perms = std::fs::metadata(&path).unwrap().permissions(); perms.set_mode(0o755); std::fs::set_permissions(&path, perms).unwrap(); } let binary = super::shebang_binary(&path).unwrap(); assert_eq!(binary, expected.map(|s| s.to_string())); } #[test] fn test_empty() { assert_shebang("", true, None); } #[test] fn test_not_executable() { assert_shebang("#!/bin/sh\necho hello", false, None); } #[test] fn test_noshebang_line() { assert_shebang("echo hello", true, None); } #[test] fn test_env() { assert_shebang("#!/usr/bin/env sh\necho hello", true, Some("sh")); } #[test] fn test_plain() { assert_shebang("#!/bin/sh\necho hello", true, Some("sh")); } #[test] fn test_with_arg() { assert_shebang("#!/bin/sh -e\necho hello", true, Some("sh")); } } ognibuild-0.2.6/src/upstream.rs000064400000000000000000000225621046102023000146110ustar 00000000000000//! This module provides a trait for dependencies that can find their upstream metadata. use crate::dependency::Dependency; use lazy_static::lazy_static; use std::sync::RwLock; pub use upstream_ontologist::UpstreamMetadata; /// Type alias for custom upstream metadata providers. pub type UpstreamProvider = Box Option + Send + Sync>; lazy_static! { /// Global registry of custom upstream metadata providers. static ref CUSTOM_PROVIDERS: RwLock> = RwLock::new(Vec::new()); } /// Register a custom upstream metadata provider. /// /// Custom providers are checked before built-in providers when finding upstream metadata. /// /// # Arguments /// * `provider` - A function that takes a dependency and returns optional upstream metadata /// /// # Example /// ```no_run /// use ognibuild::upstream::{register_upstream_provider, UpstreamMetadata}; /// use ognibuild::dependency::Dependency; /// /// register_upstream_provider(|dep| { /// if dep.family() == "custom" { /// Some(UpstreamMetadata::default()) /// } else { /// None /// } /// }); /// ``` pub fn register_upstream_provider(provider: F) where F: Fn(&dyn Dependency) -> Option + Send + Sync + 'static, { CUSTOM_PROVIDERS.write().unwrap().push(Box::new(provider)); } /// Clear all registered custom upstream providers. /// /// This is useful for testing to ensure a clean state between tests. pub fn clear_custom_providers() { CUSTOM_PROVIDERS.write().unwrap().clear(); } /// A trait for dependencies that can find their upstream metadata. pub trait FindUpstream: Dependency { /// Find the upstream metadata for this dependency. fn find_upstream(&self) -> Option; } /// Find the upstream metadata for a dependency. /// /// This function attempts to find upstream metadata (like repository URL, /// homepage, etc.) for the given dependency. It first checks any registered /// custom providers, then falls back to trying to downcast the dependency to /// various concrete dependency types that implement the FindUpstream trait. /// /// # Arguments /// * `dependency` - The dependency to find upstream metadata for /// /// # Returns /// * `Some(UpstreamMetadata)` if upstream metadata was found /// * `None` if no upstream metadata could be found pub fn find_upstream(dependency: &dyn Dependency) -> Option { // First try custom providers for provider in CUSTOM_PROVIDERS.read().unwrap().iter() { if let Some(metadata) = provider(dependency) { return Some(metadata); } } #[cfg(feature = "debian")] if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } if let Some(dep) = dependency .as_any() .downcast_ref::() { return dep.find_upstream(); } None } #[cfg(test)] mod tests { use super::*; use std::any::Any; use std::sync::Mutex; // Global test lock to ensure tests using custom providers don't interfere with each other lazy_static! { static ref TEST_LOCK: Mutex<()> = Mutex::new(()); } #[derive(Debug)] struct TestDependency { #[allow(dead_code)] name: String, } impl Dependency for TestDependency { fn family(&self) -> &'static str { "test" } fn present(&self, _session: &dyn crate::session::Session) -> bool { false } fn project_present(&self, _session: &dyn crate::session::Session) -> bool { false } fn as_any(&self) -> &dyn Any { self } } #[test] fn test_register_custom_provider() { // Acquire test lock to prevent interference from parallel tests let _lock = TEST_LOCK.lock().unwrap(); // Clear any existing providers from other tests clear_custom_providers(); let test_dep = TestDependency { name: "test-package".to_string(), }; // Initially, no upstream metadata should be found let initial_result = find_upstream(&test_dep); assert!( initial_result.is_none(), "Expected no metadata initially, but found: {:?}", initial_result ); // Register a custom provider register_upstream_provider(|dep| { if dep.family() == "test" { let mut metadata = UpstreamMetadata::default(); metadata.insert(upstream_ontologist::UpstreamDatumWithMetadata { datum: upstream_ontologist::UpstreamDatum::Repository( "https://github.com/test/repo".to_string(), ), certainty: Some(upstream_ontologist::Certainty::Certain), origin: None, }); Some(metadata) } else { None } }); // Now upstream metadata should be found via the custom provider let metadata = find_upstream(&test_dep).unwrap(); assert_eq!(metadata.repository(), Some("https://github.com/test/repo")); // Clean up clear_custom_providers(); } #[test] fn test_multiple_custom_providers() { // Acquire test lock to prevent interference from parallel tests let _lock = TEST_LOCK.lock().unwrap(); // Clear any existing providers from other tests clear_custom_providers(); let test_dep = TestDependency { name: "special-package".to_string(), }; // Register first provider (doesn't match) register_upstream_provider(|dep| { if dep.family() == "other" { let mut metadata = UpstreamMetadata::default(); metadata.insert(upstream_ontologist::UpstreamDatumWithMetadata { datum: upstream_ontologist::UpstreamDatum::Repository( "https://example.com/wrong".to_string(), ), certainty: Some(upstream_ontologist::Certainty::Certain), origin: None, }); Some(metadata) } else { None } }); // Register second provider (matches) register_upstream_provider(|dep| { if dep.family() == "test" { let mut metadata = UpstreamMetadata::default(); metadata.insert(upstream_ontologist::UpstreamDatumWithMetadata { datum: upstream_ontologist::UpstreamDatum::Repository( "https://example.com/correct".to_string(), ), certainty: Some(upstream_ontologist::Certainty::Certain), origin: None, }); Some(metadata) } else { None } }); // Should find metadata from the second provider let metadata = find_upstream(&test_dep).unwrap(); assert_eq!(metadata.repository(), Some("https://example.com/correct")); clear_custom_providers(); } #[test] fn test_clear_custom_providers() { // Acquire test lock to prevent interference from parallel tests let _lock = TEST_LOCK.lock().unwrap(); clear_custom_providers(); let test_dep = TestDependency { name: "test-package".to_string(), }; // Register a provider register_upstream_provider(|dep| { if dep.family() == "test" { let mut metadata = UpstreamMetadata::default(); metadata.insert(upstream_ontologist::UpstreamDatumWithMetadata { datum: upstream_ontologist::UpstreamDatum::Homepage( "https://example.com".to_string(), ), certainty: Some(upstream_ontologist::Certainty::Certain), origin: None, }); Some(metadata) } else { None } }); // Verify it works assert!(find_upstream(&test_dep).is_some()); // Clear providers clear_custom_providers(); // Verify provider is gone assert!(find_upstream(&test_dep).is_none()); } } ognibuild-0.2.6/src/vcs.rs000064400000000000000000000061541046102023000135430ustar 00000000000000//! VCS-related functions use breezyshim::branch::Branch; use breezyshim::error::Error as BrzError; use breezyshim::prelude::Repository; use breezyshim::tree::PyTree; use breezyshim::workingtree::{GenericWorkingTree, WorkingTree}; use std::path::{Path, PathBuf}; use url::Url; /// Export a VCS tree to a new location. /// /// # Arguments /// * `tree` - The tree to export /// * `directory` - The directory to export the tree to /// * `subpath` - The subpath to export pub fn export_vcs_tree( tree: &T, directory: &Path, subpath: Option<&Path>, ) -> Result<(), BrzError> { breezyshim::export::export(tree, directory, subpath) } /// A Breezy tree that can be duplicated. pub trait DupableTree { /// Get the basis tree of this tree. fn basis_tree(&self) -> breezyshim::tree::RevisionTree; /// Get the parent location of this tree. fn get_parent(&self) -> Option; /// Get the base directory of this tree, if it has one. fn basedir(&self) -> Option; /// Export this tree to a directory. fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError>; } impl DupableTree for GenericWorkingTree { fn basis_tree(&self) -> breezyshim::tree::RevisionTree { WorkingTree::basis_tree(self).unwrap() } fn get_parent(&self) -> Option { WorkingTree::branch(self).get_parent() } fn basedir(&self) -> Option { Some(WorkingTree::basedir(self)) } fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError> { export_vcs_tree(self, directory, subpath) } } impl DupableTree for breezyshim::tree::RevisionTree { fn basis_tree(&self) -> breezyshim::tree::RevisionTree { self.repository() .revision_tree(&self.get_revision_id()) .unwrap() } fn get_parent(&self) -> Option { let branch = self.repository().controldir().open_branch(None).unwrap(); branch.get_parent() } fn basedir(&self) -> Option { None } fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError> { export_vcs_tree(self, directory, subpath) } } /// Duplicate a VCS tree to a new location, including all history. /// /// For a RevisionTree, this will duplicate the tree to a new location. /// For a WorkingTree, this will duplicate the basis tree to a new location. /// /// # Arguments /// * `orig_tree` - The tree to duplicate /// * `directory` - The directory to duplicate the tree to pub fn dupe_vcs_tree(orig_tree: &dyn DupableTree, directory: &Path) -> Result<(), BrzError> { let tree = orig_tree.basis_tree(); let result = tree.repository().controldir().sprout( Url::from_directory_path(directory).unwrap(), None, Some(true), None, Some(&tree.get_revision_id()), )?; assert!(result.has_workingtree()); // Copy parent location - some scripts need this if let Some(parent) = orig_tree.get_parent() { let mut branch = result.open_branch(None)?; branch.set_parent(&parent); } Ok(()) }