├── .codespellrc ├── .flake8 ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── auto-merge.yaml │ ├── disperse.yml │ └── rust.yaml ├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── TODO ├── disperse.toml ├── examples └── apt-file-search.rs ├── notes ├── debugging.md └── design.md └── src ├── actions ├── build.rs ├── clean.rs ├── dist.rs ├── info.rs ├── install.rs ├── mod.rs └── test.rs ├── analyze.rs ├── bin ├── deb-fix-build.rs ├── deb-upstream-deps.rs ├── dep-server.rs ├── ogni.rs ├── ognibuild-deb.rs ├── ognibuild-dist.rs └── report-apt-deps-status.rs ├── buildlog.rs ├── buildsystem.rs ├── buildsystems ├── bazel.rs ├── gnome.rs ├── go.rs ├── haskell.rs ├── java.rs ├── make.rs ├── meson.rs ├── mod.rs ├── node.rs ├── octave.rs ├── perl.rs ├── python.rs ├── r.rs ├── ruby.rs ├── rust.rs └── waf.rs ├── debian ├── apt.rs ├── build.rs ├── build_deps.rs ├── context.rs ├── dep_server.rs ├── file_search.rs ├── fix_build.rs ├── fixers.rs ├── mod.rs ├── sources_list.rs ├── udd.rs └── upstream_deps.rs ├── dependencies ├── autoconf.rs ├── debian.rs ├── go.rs ├── haskell.rs ├── java.rs ├── latex.rs ├── mod.rs ├── node.rs ├── octave.rs ├── perl.rs ├── php.rs ├── pytest.rs ├── python.rs ├── r.rs ├── vague.rs └── xml.rs ├── dependency.rs ├── dist.rs ├── dist_catcher.rs ├── fix_build.rs ├── fixers.rs ├── installer.rs ├── lib.rs ├── logs.rs ├── output.rs ├── session ├── mod.rs ├── plain.rs ├── schroot.rs └── unshare.rs ├── shebang.rs ├── upstream.rs └── vcs.rs /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | ignore-words-list = crate 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | banned-modules = "silver-platter = Should not use silver-platter" 2 | exclude = "build,.eggs/,target/" 3 | extend-ignore = E203, E266, E501, W293, W291 4 | max-line-length = 88 5 | max-complexity = 18 6 | select = B,C,E,F,W,T4,B9 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jelmer 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jelmer 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | groups: 9 | GitHub_Actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: "weekly" 14 | - package-ecosystem: "cargo" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --squash "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/disperse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Disperse configuration 3 | 4 | "on": 5 | - push 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: jelmer/action-disperse-validate@v2 15 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rust 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | rust: 13 | 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: ['ubuntu-24.04', macos-latest] 18 | fail-fast: false 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.x" 26 | - name: Update apt cache 27 | if: matrix.os == 'ubuntu-24.04' 28 | run: sudo apt-get update 29 | - name: Install Debian tools on Ubuntu 30 | if: matrix.os == 'ubuntu-24.04' 31 | run: sudo apt-get install -y mmdebstrap 32 | - name: Install system breezy and libapt-pkg-dev 33 | if: matrix.os == 'ubuntu-24.04' 34 | run: sudo apt-get install -y brz libapt-pkg-dev libpcre3-dev 35 | - name: Install breezy 36 | run: pip install breezy 37 | - name: Install breezy and brz-debian 38 | run: pip install \ 39 | git+https://github.com/breezy-team/breezy-debian \ 40 | python_apt@git+https://salsa.debian.org/apt-team/python-apt 41 | if: matrix.os == 'ubuntu-24.04' 42 | - name: Build 43 | run: cargo build --verbose 44 | - name: Run tests 45 | # Exclude debian features: 46 | run: cargo test --verbose --no-default-features --features=breezy,dep-server,upstream 47 | if: matrix.os != 'ubuntu-24.04' 48 | - name: Run tests 49 | run: cargo test --verbose 50 | if: matrix.os == 'ubuntu-24.04' 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | build 3 | *~ 4 | ognibuild.egg-info 5 | dist 6 | __pycache__ 7 | .eggs 8 | *.swp 9 | *.swo 10 | *.swn 11 | .mypy_cache 12 | target 13 | *.so 14 | .claude/settings.local.json 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jelmer Vernooij 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socioeconomic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project lead at jelmer@jelmer.uk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ognibuild" 3 | version = "0.0.35" 4 | authors = [ "Jelmer Vernooij "] 5 | edition = "2021" 6 | license = "GPL-2.0+" 7 | description = "Detect and run any build system" 8 | repository = "https://github.com/jelmer/ognibuild.git" 9 | homepage = "https://github.com/jelmer/ognibuild" 10 | default-run = "ogni" 11 | 12 | [dependencies] 13 | pyo3 = "0.25" 14 | breezyshim = { version = "0.5", optional = true } 15 | buildlog-consultant = { version = "0.1.1" } 16 | #buildlog-consultant = { path = "../buildlog-consultant" } 17 | upstream-ontologist = { version = "0.2", optional = true } 18 | axum = { version = "0.8", optional = true, features = ["json", "http2", "tokio"] } 19 | chrono = ">=0.4" 20 | clap = { version = "4", features = ["derive", "env"], optional = true } 21 | deb822-lossless = ">=0.2" 22 | debian-analyzer = { version = "0.158.27", optional = true } 23 | #debian-analyzer = { path = "../lintian-brush/analyzer", optional = true } 24 | debian-changelog = { version = ">=0.2", optional = true } 25 | debian-control = { version = ">=0.1.25", optional = true } 26 | debversion = { version = "0.4", optional = true } 27 | env_logger = { version = ">=0.10", optional = true } 28 | flate2 = { version = "1", optional = true } 29 | fs_extra = "1.3.0" 30 | inventory = ">=0.3" 31 | lazy-regex = ">=2" 32 | lazy_static = "1" 33 | libc = "0.2" 34 | log = "0.4" 35 | lz4_flex = { version = ">=0.11", optional = true } 36 | lzma-rs = { version = "0.3.0", optional = true } 37 | makefile-lossless = "0.2.1" 38 | maplit = "1.0.2" 39 | nix = { version = ">=0.27.0", features = ["user"] } 40 | pep508_rs = "0.9" 41 | percent-encoding = "2.3.1" 42 | pyproject-toml = "0.13" 43 | r-description = { version = ">=0.3", features = ["serde"] } 44 | rand = "0.9.0" 45 | regex = "1.10.6" 46 | reqwest = { version = ">=0.10", optional = true, features = ["blocking", "json"] } 47 | semver = ">=1" 48 | serde = { version = "1.0", features = ["derive"] } 49 | serde_json = "1.0" 50 | serde_yaml = "0.9.34" 51 | shlex = "1.3.0" 52 | sqlx = { version = "0.8.6", optional = true, features = ["postgres", "runtime-tokio-native-tls"] } 53 | stackdriver_logger = { version = "0.8.2", optional = true } 54 | tempfile = ">=3" 55 | tokio = { version = "1", features = ["full"], optional = true } 56 | toml = ">=0.8" 57 | toml_edit = ">=0.22" 58 | url = ">=2" 59 | whoami = { version = ">=1.4", default-features = false } 60 | xmltree = ">=0.10" 61 | dirs = ">=5,<7" 62 | 63 | [features] 64 | default = ["debian", "cli", "udd", "upstream", "breezy", "dep-server"] 65 | debian = ["dep:debian-changelog", "dep:debversion", "dep:debian-control", "dep:flate2", "dep:lzma-rs", "dep:lz4_flex", "dep:reqwest", "breezyshim/debian", "dep:debian-analyzer"] 66 | cli = ["dep:clap", "dep:env_logger"] 67 | udd = ["dep:sqlx", "dep:tokio", "debian"] 68 | dep-server = ["dep:axum", "dep:tokio"] 69 | upstream = ["dep:upstream-ontologist"] 70 | breezy = ["dep:breezyshim"] 71 | 72 | [dev-dependencies] 73 | lazy_static = "1" 74 | test-log = "0.2" 75 | 76 | [[bin]] 77 | name = "ognibuild-deb" 78 | path = "src/bin/ognibuild-deb.rs" 79 | required-features = ["cli", "debian", "breezy"] 80 | 81 | [[example]] 82 | name = "apt-file-search" 83 | path = "examples/apt-file-search.rs" 84 | required-features = ["cli", "debian"] 85 | 86 | [[bin]] 87 | name = "dep-server" 88 | path = "src/bin/dep-server.rs" 89 | required-features = ["dep-server", "cli", "debian"] 90 | 91 | [[bin]] 92 | name = "ognibuild-dist" 93 | path = "src/bin/ognibuild-dist.rs" 94 | required-features = ["cli", "breezy"] 95 | 96 | [[bin]] 97 | name = "ogni" 98 | path = "src/bin/ogni.rs" 99 | required-features = ["cli"] 100 | 101 | [[bin]] 102 | name = "deb-fix-build" 103 | path = "src/bin/deb-fix-build.rs" 104 | required-features = ["debian", "cli", "breezy"] 105 | 106 | [[bin]] 107 | name = "deb-upstream-deps" 108 | path = "src/bin/deb-upstream-deps.rs" 109 | required-features = ["cli", "debian", "breezy"] 110 | 111 | [[bin]] 112 | name = "report-apt-deps-status" 113 | path = "src/bin/report-apt-deps-status.rs" 114 | required-features = ["cli", "debian"] 115 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check:: style 2 | 3 | style: 4 | ruff check py 5 | 6 | check:: testsuite 7 | 8 | build-inplace: 9 | python3 setup.py build_rust --inplace 10 | 11 | testsuite: 12 | cargo test 13 | 14 | check:: typing 15 | 16 | typing: 17 | mypy py tests 18 | 19 | coverage: 20 | PYTHONPATH=$(shell pwd)/py python3 -m coverage run -m unittest tests.test_suite 21 | 22 | coverage-html: 23 | python3 -m coverage html 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ognibuild 2 | 3 | Ognibuild is a simple wrapper with a common interface for invoking any kind of 4 | build tool. 5 | 6 | The ideas is that it can be run to build and install any source code directory 7 | by detecting the build system that is in use and invoking that with the correct 8 | parameters. 9 | 10 | It can also detect and install missing dependencies. 11 | 12 | ## Goals 13 | 14 | The goal of ognibuild is to provide a consistent CLI that can be used for any 15 | software package. It is mostly useful for automated building of 16 | large sets of diverse packages (e.g. different programming languages). 17 | 18 | It is not meant to expose all functionality that is present in the underlying 19 | build systems. To use that, invoke those build systems directly. 20 | 21 | ## Usage 22 | 23 | Ognibuild has a number of subcommands: 24 | 25 | * ``ogni clean`` - remove any built artifacts 26 | * ``ogni dist`` - create a source tarball 27 | * ``ogni build`` - build the package in-tree 28 | * ``ogni install`` - install the package 29 | * ``ogni test`` - run the testsuite in the source directory 30 | 31 | It also includes a subcommand that can fix up the build dependencies 32 | for Debian packages, called deb-fix-build. 33 | 34 | ### Examples 35 | 36 | ``` 37 | ogni -d https://gitlab.gnome.org/GNOME/fractal install 38 | ``` 39 | 40 | ### Debugging 41 | 42 | If you run into any issues, please see [Debugging](notes/debugging.md). 43 | 44 | ## Status 45 | 46 | Ognibuild is functional, but sometimes rough around the edges. If you run into 47 | issues (or lack of support for a particular ecosystem), please file a bug. 48 | 49 | ### Supported Build Systems 50 | 51 | - Bazel 52 | - Cabal 53 | - Cargo 54 | - Golang 55 | - Gradle 56 | - Make, including various makefile generators: 57 | - autoconf/automake 58 | - CMake 59 | - Makefile.PL 60 | - qmake 61 | - Maven 62 | - ninja, including ninja file generators: 63 | - meson 64 | - Node 65 | - Octave 66 | - Perl 67 | - Module::Build::Tiny 68 | - Dist::Zilla 69 | - Minilla 70 | - PHP Pear 71 | - Python - setup.py/setup.cfg/pyproject.toml 72 | - R 73 | - Ruby gems 74 | - Waf 75 | 76 | ### Supported package repositories 77 | 78 | Package repositories are used to install missing dependencies. 79 | 80 | The following "native" repositories are supported: 81 | 82 | - pypi 83 | - cpan 84 | - hackage 85 | - npm 86 | - cargo 87 | - cran 88 | - golang\* 89 | 90 | As well one distribution repository: 91 | 92 | - apt 93 | 94 | ## License 95 | 96 | Ognibuild is licensed under the GNU GPL, v2 or later. 97 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | ognibuild is still under heavy development. Only the latest version is security 6 | supported. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please report security issues by e-mail to jelmer@jelmer.uk, ideally PGP encrypted to the key at https://jelmer.uk/D729A457.asc 11 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Need to be able to check up front whether a requirement is satisfied, before attempting to install it (which is more expensive) 2 | - Cache parsed Contents files during test suite runs and/or speed up reading 3 | -------------------------------------------------------------------------------- /disperse.toml: -------------------------------------------------------------------------------- 1 | tag-name = "v$VERSION" 2 | tarball-location = [] 3 | release-timeout = 5 4 | -------------------------------------------------------------------------------- /examples/apt-file-search.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ognibuild::debian::file_search::{ 3 | get_apt_contents_file_searcher, get_packages_for_paths, FileSearcher, GENERATED_FILE_SEARCHER, 4 | }; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Parser)] 8 | struct Args { 9 | #[clap(short, long)] 10 | /// Search for regex. 11 | regex: bool, 12 | /// Path to search for. 13 | path: Vec, 14 | #[clap(short, long)] 15 | /// Enable debug output. 16 | debug: bool, 17 | #[clap(short, long)] 18 | /// Case insensitive search. 19 | case_insensitive: bool, 20 | } 21 | 22 | pub fn main() -> Result<(), i8> { 23 | let args: Args = Args::parse(); 24 | env_logger::builder() 25 | .filter_level(if args.debug { 26 | log::LevelFilter::Debug 27 | } else { 28 | log::LevelFilter::Info 29 | }) 30 | .init(); 31 | 32 | let session = ognibuild::session::plain::PlainSession::new(); 33 | let main_searcher = get_apt_contents_file_searcher(&session).unwrap(); 34 | let searchers: Vec<&dyn FileSearcher> = vec![ 35 | main_searcher.as_ref(), 36 | &*GENERATED_FILE_SEARCHER as &dyn FileSearcher, 37 | ]; 38 | 39 | let packages = get_packages_for_paths( 40 | args.path 41 | .iter() 42 | .map(|x| x.as_path().to_str().unwrap()) 43 | .collect(), 44 | searchers.as_slice(), 45 | args.regex, 46 | args.case_insensitive, 47 | ); 48 | for package in packages { 49 | println!("{}", package); 50 | } 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /notes/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | Hopefully ognibuild just Does The Right Thing™, but sometimes it doesn't. Here 4 | are some tips for debugging. 5 | 6 | ## Detecting dependencies 7 | 8 | If you're trying to build a project and it's failing, it might be because 9 | ognibuild is missing a dependency. You can use ``ogni info`` 10 | to see what dependencies ognibuild thinks are missing. 11 | 12 | ## Log file parsing 13 | 14 | If a build fails, ognibuild will attempt to parse the log file with 15 | [buildlog-consultant](https://github.com/jelmer/buildlog-consultant) 16 | to try to find out how to fix the build. If you think it's not doing a good job, 17 | you can run buildlog-consultant manually on the log file, and then 18 | possibly file a bug against buildlog-consultant. 19 | 20 | ## Failure to build 21 | 22 | If onibuild fails to determine how to build a project, it will print out 23 | an error message. If you think it should be able to build the project, 24 | please file a bug. 25 | 26 | ## Reporting bugs 27 | 28 | If you think you've found a bug in ognibuild, please report it! You can do so 29 | on GitHub at https://github.com/jelmer/ognibuild/issues/new 30 | 31 | If ognibuild crashed, please include the backtrace with 32 | ``RUST_BACKTRACE=full`` set. 33 | 34 | If it is possible to reproduce the bug on a particular 35 | open source project, please include the URL of that project, 36 | and the exact command you ran. 37 | -------------------------------------------------------------------------------- /notes/design.md: -------------------------------------------------------------------------------- 1 | Ognibuild aims to build and extract build metadata from any software project. It does this through a variety of mechanisms: 2 | 3 | * Detecting know build systems 4 | * Ensuring necessary dependencies are present 5 | * Fixing other issues in the project 6 | 7 | A build system is anything that can create artefacts from source and/or test 8 | and install those artefacts. Some projects use multiple build systems which may 9 | or may not be tightly integrated. 10 | 11 | A build action is one of “clean”, “build”, “install” or “test”. 12 | 13 | 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). 14 | 15 | 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. 16 | 17 | 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. 18 | If no appropriate Fixer can be found then no further action is taken. If the Fixer is successful then the original action is retried. 19 | 20 | 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). 21 | 22 | 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. 23 | -------------------------------------------------------------------------------- /src/actions/build.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::installer::{Error as InstallerError, Installer}; 4 | use crate::logs::{wrap, LogManager}; 5 | use crate::session::Session; 6 | 7 | /// Run the build process using the first applicable build system. 8 | /// 9 | /// This function attempts to build a package using the first build system in the provided list 10 | /// that is applicable for the current project. If the build fails, it will attempt to fix 11 | /// issues using the provided fixers. 12 | /// 13 | /// # Arguments 14 | /// * `session` - The session to run commands in 15 | /// * `buildsystems` - List of build systems to try 16 | /// * `installer` - Installer to use for installing dependencies 17 | /// * `fixers` - List of fixers to try if build fails 18 | /// * `log_manager` - Manager for logging build output 19 | /// 20 | /// # Returns 21 | /// * `Ok(())` if the build succeeds 22 | /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used 23 | /// * Other errors if the build fails and can't be fixed 24 | pub fn run_build( 25 | session: &dyn Session, 26 | buildsystems: &[&dyn BuildSystem], 27 | installer: &dyn Installer, 28 | fixers: &[&dyn BuildFixer], 29 | log_manager: &mut dyn LogManager, 30 | ) -> Result<(), Error> { 31 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 32 | session.create_home()?; 33 | 34 | for buildsystem in buildsystems { 35 | return Ok(iterate_with_build_fixers( 36 | fixers, 37 | || -> Result<_, InterimError> { 38 | Ok(wrap(log_manager, || -> Result<_, Error> { 39 | Ok(buildsystem.build(session, installer)?) 40 | })?) 41 | }, 42 | None, 43 | )?); 44 | } 45 | 46 | Err(Error::NoBuildSystemDetected) 47 | } 48 | -------------------------------------------------------------------------------- /src/actions/clean.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::installer::{Error as InstallerError, Installer}; 4 | use crate::logs::{wrap, LogManager}; 5 | use crate::session::Session; 6 | 7 | /// Run the clean process using the first applicable build system. 8 | /// 9 | /// This function attempts to clean a project using the first build system in the provided list 10 | /// that is applicable for the current project. If the clean operation fails, it will attempt to fix 11 | /// issues using the provided fixers. 12 | /// 13 | /// # Arguments 14 | /// * `session` - The session to run commands in 15 | /// * `buildsystems` - List of build systems to try 16 | /// * `installer` - Installer to use for installing dependencies 17 | /// * `fixers` - List of fixers to try if clean fails 18 | /// * `log_manager` - Manager for logging clean output 19 | /// 20 | /// # Returns 21 | /// * `Ok(())` if the clean succeeds 22 | /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used 23 | /// * Other errors if the clean fails and can't be fixed 24 | pub fn run_clean( 25 | session: &dyn Session, 26 | buildsystems: &[&dyn BuildSystem], 27 | installer: &dyn Installer, 28 | fixers: &[&dyn BuildFixer], 29 | log_manager: &mut dyn LogManager, 30 | ) -> Result<(), Error> { 31 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 32 | session.create_home()?; 33 | 34 | for buildsystem in buildsystems { 35 | return Ok(iterate_with_build_fixers( 36 | fixers, 37 | || -> Result<_, InterimError> { 38 | Ok(wrap(log_manager, || -> Result<_, Error> { 39 | Ok(buildsystem.clean(session, installer)?) 40 | })?) 41 | }, 42 | None, 43 | )?); 44 | } 45 | 46 | Err(Error::NoBuildSystemDetected) 47 | } 48 | -------------------------------------------------------------------------------- /src/actions/dist.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::installer::{Error as InstallerError, Installer}; 4 | use crate::logs::{wrap, LogManager}; 5 | use crate::session::Session; 6 | use std::ffi::OsString; 7 | use std::path::Path; 8 | 9 | /// Run the distribution package creation process using the first applicable build system. 10 | /// 11 | /// This function attempts to create a distribution package using the first build system in the 12 | /// provided list that is applicable for the current project. If the operation fails, it will 13 | /// attempt to fix issues using the provided fixers. 14 | /// 15 | /// # Arguments 16 | /// * `session` - The session to run commands in 17 | /// * `buildsystems` - List of build systems to try 18 | /// * `installer` - Installer to use for installing dependencies 19 | /// * `fixers` - List of fixers to try if dist creation fails 20 | /// * `target_directory` - Directory where distribution packages should be created 21 | /// * `quiet` - Whether to suppress output 22 | /// * `log_manager` - Manager for logging output 23 | /// 24 | /// # Returns 25 | /// * `Ok(OsString)` with the filename of the created package if successful 26 | /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used 27 | /// * Other errors if the dist creation fails and can't be fixed 28 | pub fn run_dist( 29 | session: &dyn Session, 30 | buildsystems: &[&dyn BuildSystem], 31 | installer: &dyn Installer, 32 | fixers: &[&dyn BuildFixer], 33 | target_directory: &Path, 34 | quiet: bool, 35 | log_manager: &mut dyn LogManager, 36 | ) -> Result { 37 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 38 | session.create_home()?; 39 | 40 | for buildsystem in buildsystems { 41 | return Ok(iterate_with_build_fixers( 42 | fixers, 43 | || -> Result<_, InterimError> { 44 | Ok(wrap(log_manager, || -> Result<_, Error> { 45 | Ok(buildsystem.dist(session, installer, target_directory, quiet)?) 46 | })?) 47 | }, 48 | None, 49 | )?); 50 | } 51 | 52 | Err(Error::NoBuildSystemDetected) 53 | } 54 | -------------------------------------------------------------------------------- /src/actions/info.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use crate::fix_build::BuildFixer; 3 | use crate::installer::Error as InstallerError; 4 | use crate::session::Session; 5 | use std::collections::HashMap; 6 | 7 | /// Display information about detected build systems and their dependencies/outputs. 8 | /// 9 | /// This function logs information about each detected build system, including its 10 | /// declared dependencies and outputs. 11 | /// 12 | /// # Arguments 13 | /// * `session` - The session to run commands in 14 | /// * `buildsystems` - List of build systems to get information from 15 | /// * `fixers` - Optional list of fixers to use if getting information fails 16 | /// 17 | /// # Returns 18 | /// * `Ok(())` if information was successfully retrieved and displayed 19 | /// * Errors are logged but not returned, so this function will generally succeed 20 | pub fn run_info( 21 | session: &dyn Session, 22 | buildsystems: &[&dyn BuildSystem], 23 | fixers: Option<&[&dyn BuildFixer]>, 24 | ) -> Result<(), Error> { 25 | for buildsystem in buildsystems { 26 | log::info!("{:?}", buildsystem); 27 | let mut deps = HashMap::new(); 28 | match buildsystem.get_declared_dependencies(session, fixers) { 29 | Ok(declared_deps) => { 30 | for (category, dep) in declared_deps { 31 | deps.entry(category).or_insert_with(Vec::new).push(dep); 32 | } 33 | } 34 | Err(e) => { 35 | log::error!( 36 | "Failed to get declared dependencies from {:?}: {}", 37 | buildsystem, 38 | e 39 | ); 40 | } 41 | } 42 | 43 | if !deps.is_empty() { 44 | log::info!(" Declared dependencies:"); 45 | for (category, deps) in deps { 46 | for dep in deps { 47 | log::info!(" {}: {:?}", category, dep); 48 | } 49 | } 50 | } 51 | 52 | let outputs = match buildsystem.get_declared_outputs(session, fixers) { 53 | Ok(outputs) => outputs, 54 | Err(e) => { 55 | log::error!( 56 | "Failed to get declared outputs from {:?}: {}", 57 | buildsystem, 58 | e 59 | ); 60 | continue; 61 | } 62 | }; 63 | 64 | if !outputs.is_empty() { 65 | log::info!(" Outputs:"); 66 | for output in outputs { 67 | log::info!(" {:?}", output); 68 | } 69 | } 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/actions/install.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error, InstallTarget}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::installer::{Error as InstallerError, InstallationScope, Installer}; 4 | use crate::logs::{wrap, LogManager}; 5 | use crate::session::Session; 6 | use std::path::Path; 7 | 8 | /// Run the installation process using the first applicable build system. 9 | /// 10 | /// This function attempts to install a package using the first build system in the provided list 11 | /// that is applicable for the current project. If the installation fails, it will attempt to fix 12 | /// issues using the provided fixers. 13 | /// 14 | /// # Arguments 15 | /// * `session` - The session to run commands in 16 | /// * `buildsystems` - List of build systems to try 17 | /// * `installer` - Installer to use for installing dependencies 18 | /// * `fixers` - List of fixers to try if installation fails 19 | /// * `log_manager` - Manager for logging installation output 20 | /// * `scope` - Installation scope (user or system) 21 | /// * `prefix` - Optional installation prefix path 22 | /// 23 | /// # Returns 24 | /// * `Ok(())` if the installation succeeds 25 | /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used 26 | /// * Other errors if the installation fails and can't be fixed 27 | pub fn run_install( 28 | session: &dyn Session, 29 | buildsystems: &[&dyn BuildSystem], 30 | installer: &dyn Installer, 31 | fixers: &[&dyn BuildFixer], 32 | log_manager: &mut dyn LogManager, 33 | scope: InstallationScope, 34 | prefix: Option<&Path>, 35 | ) -> Result<(), Error> { 36 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 37 | session.create_home()?; 38 | 39 | let target = InstallTarget { 40 | scope, 41 | prefix: prefix.map(|p| p.to_path_buf()), 42 | }; 43 | 44 | for buildsystem in buildsystems { 45 | return Ok(iterate_with_build_fixers( 46 | fixers, 47 | || -> Result<_, InterimError> { 48 | Ok(wrap(log_manager, || -> Result<_, Error> { 49 | Ok(buildsystem.install(session, installer, &target)?) 50 | })?) 51 | }, 52 | None, 53 | )?); 54 | } 55 | 56 | Err(Error::NoBuildSystemDetected) 57 | } 58 | -------------------------------------------------------------------------------- /src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | /// Build action implementation. 2 | pub mod build; 3 | /// Clean action implementation. 4 | pub mod clean; 5 | /// Distribution creation action implementation. 6 | pub mod dist; 7 | /// Information display action implementation. 8 | pub mod info; 9 | /// Installation action implementation. 10 | pub mod install; 11 | /// Test action implementation. 12 | pub mod test; 13 | -------------------------------------------------------------------------------- /src/actions/test.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::installer::{Error as InstallerError, Installer}; 4 | use crate::logs::{wrap, LogManager}; 5 | use crate::session::Session; 6 | 7 | /// Run tests using the first applicable build system. 8 | /// 9 | /// This function attempts to run tests using the first build system in the provided list 10 | /// that is applicable for the current project. If the tests fail, it will attempt to fix 11 | /// issues using the provided fixers. 12 | /// 13 | /// # Arguments 14 | /// * `session` - The session to run commands in 15 | /// * `buildsystems` - List of build systems to try 16 | /// * `installer` - Installer to use for installing dependencies 17 | /// * `fixers` - List of fixers to try if tests fail 18 | /// * `log_manager` - Manager for logging test output 19 | /// 20 | /// # Returns 21 | /// * `Ok(())` if the tests succeed 22 | /// * `Err(Error::NoBuildSystemDetected)` if no build system could be used 23 | /// * Other errors if the tests fail and can't be fixed 24 | pub fn run_test( 25 | session: &dyn Session, 26 | buildsystems: &[&dyn BuildSystem], 27 | installer: &dyn Installer, 28 | fixers: &[&dyn BuildFixer], 29 | log_manager: &mut dyn LogManager, 30 | ) -> Result<(), Error> { 31 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 32 | session.create_home()?; 33 | 34 | for buildsystem in buildsystems { 35 | return Ok(iterate_with_build_fixers( 36 | fixers, 37 | || -> Result<_, InterimError> { 38 | Ok(wrap(log_manager, || -> Result<_, Error> { 39 | Ok(buildsystem.test(session, installer)?) 40 | })?) 41 | }, 42 | None, 43 | )?); 44 | } 45 | 46 | Err(Error::NoBuildSystemDetected) 47 | } 48 | -------------------------------------------------------------------------------- /src/analyze.rs: -------------------------------------------------------------------------------- 1 | use crate::session::{run_with_tee, Error as SessionError, Session}; 2 | use buildlog_consultant::problems::common::MissingCommand; 3 | 4 | fn default_check_success(status: std::process::ExitStatus, _lines: Vec<&str>) -> bool { 5 | status.success() 6 | } 7 | 8 | #[derive(Debug)] 9 | /// Error type for analyzed command execution errors. 10 | /// 11 | /// This enum represents different kinds of errors that can occur when running 12 | /// and analyzing commands, with details about the specific error. 13 | pub enum AnalyzedError { 14 | /// Error indicating a command was not found. 15 | MissingCommandError { 16 | /// The name of the command that was not found. 17 | command: String, 18 | }, 19 | /// Error from an IO operation. 20 | IoError(std::io::Error), 21 | /// Detailed error with information from the buildlog consultant. 22 | Detailed { 23 | /// The return code of the failed command. 24 | retcode: i32, 25 | /// The specific build problem identified. 26 | error: Box, 27 | }, 28 | /// Error that could not be specifically identified. 29 | Unidentified { 30 | /// The return code of the failed command. 31 | retcode: i32, 32 | /// The output lines from the command. 33 | lines: Vec, 34 | /// Optional secondary information about the error. 35 | secondary: Option>, 36 | }, 37 | } 38 | 39 | impl From for AnalyzedError { 40 | fn from(e: std::io::Error) -> Self { 41 | #[cfg(unix)] 42 | match e.raw_os_error() { 43 | Some(libc::ENOSPC) => { 44 | return AnalyzedError::Detailed { 45 | retcode: 1, 46 | error: Box::new(buildlog_consultant::problems::common::NoSpaceOnDevice), 47 | }; 48 | } 49 | Some(libc::EMFILE) => { 50 | return AnalyzedError::Detailed { 51 | retcode: 1, 52 | error: Box::new(buildlog_consultant::problems::common::TooManyOpenFiles), 53 | } 54 | } 55 | _ => {} 56 | } 57 | AnalyzedError::IoError(e) 58 | } 59 | } 60 | 61 | impl std::fmt::Display for AnalyzedError { 62 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 63 | match self { 64 | AnalyzedError::MissingCommandError { command } => { 65 | write!(f, "Command not found: {}", command) 66 | } 67 | AnalyzedError::IoError(e) => write!(f, "IO error: {}", e), 68 | AnalyzedError::Detailed { retcode, error } => { 69 | write!(f, "Command failed with code {}", retcode)?; 70 | write!(f, "\n{}", error) 71 | } 72 | AnalyzedError::Unidentified { 73 | retcode, 74 | lines, 75 | secondary, 76 | } => { 77 | write!(f, "Command failed with code {}", retcode)?; 78 | if let Some(secondary) = secondary { 79 | write!(f, "\n{}", secondary) 80 | } else { 81 | write!(f, "\n{}", lines.join("\n")) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | impl std::error::Error for AnalyzedError {} 89 | 90 | /// Run a command and analyze the output for common build errors. 91 | /// 92 | /// # Arguments 93 | /// * `session`: Session to run the command in 94 | /// * `args`: Arguments to the command 95 | /// * `check_success`: Function to determine if the command was successful 96 | /// * `quiet`: Whether to log the command being run 97 | /// * `cwd`: Current working directory for the command 98 | /// * `user`: User to run the command as 99 | /// * `env`: Environment variables to set for the command 100 | /// * `stdin`: Stdin for the command 101 | pub fn run_detecting_problems( 102 | session: &dyn Session, 103 | args: Vec<&str>, 104 | check_success: Option<&dyn Fn(std::process::ExitStatus, Vec<&str>) -> bool>, 105 | quiet: bool, 106 | cwd: Option<&std::path::Path>, 107 | user: Option<&str>, 108 | env: Option<&std::collections::HashMap>, 109 | stdin: Option, 110 | ) -> Result, AnalyzedError> { 111 | let check_success = check_success.unwrap_or(&default_check_success); 112 | 113 | let (retcode, contents) = 114 | match run_with_tee(session, args.clone(), cwd, user, env, stdin, quiet) { 115 | Ok((retcode, contents)) => (retcode, contents), 116 | Err(SessionError::SetupFailure(..)) => unreachable!(), 117 | Err(SessionError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => { 118 | let command = args[0].to_string(); 119 | return Err(AnalyzedError::Detailed { 120 | retcode: 127, 121 | error: Box::new(MissingCommand(command)) 122 | as Box, 123 | }); 124 | } 125 | Err(SessionError::IoError(e)) => { 126 | return Err(AnalyzedError::IoError(e)); 127 | } 128 | Err(SessionError::CalledProcessError(retcode)) => (retcode, vec![]), 129 | }; 130 | 131 | log::debug!( 132 | "Command returned code {}, with {} lines of output.", 133 | retcode.code().unwrap_or(1), 134 | contents.len() 135 | ); 136 | 137 | if check_success(retcode, contents.iter().map(|s| s.as_str()).collect()) { 138 | return Ok(contents); 139 | } 140 | let (r#match, error) = buildlog_consultant::common::find_build_failure_description( 141 | contents.iter().map(|x| x.as_str()).collect(), 142 | ); 143 | if let Some(error) = error { 144 | log::debug!("Identified error: {}", error); 145 | Err(AnalyzedError::Detailed { 146 | retcode: retcode.code().unwrap_or(1), 147 | error, 148 | }) 149 | } else { 150 | if let Some(r#match) = r#match.as_ref() { 151 | log::warn!("Build failed with unidentified error:"); 152 | log::warn!("{}", r#match.line().trim_end_matches('\n')); 153 | } else { 154 | log::warn!("Build failed without error being identified."); 155 | } 156 | Err(AnalyzedError::Unidentified { 157 | retcode: retcode.code().unwrap_or(1), 158 | lines: contents, 159 | secondary: r#match, 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/bin/deb-fix-build.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ognibuild::debian::fix_build::{rescue_build_log, IterateBuildError}; 3 | use ognibuild::session::plain::PlainSession; 4 | #[cfg(target_os = "linux")] 5 | use ognibuild::session::schroot::SchrootSession; 6 | use ognibuild::session::Session; 7 | use std::fmt::Write as _; 8 | use std::io::Write as _; 9 | use std::path::PathBuf; 10 | 11 | #[derive(Parser)] 12 | struct Args { 13 | /// Suffix to use for test builds 14 | #[clap(short, long, default_value = "fixbuild1")] 15 | suffix: String, 16 | 17 | /// Suite to target 18 | #[clap(short, long, default_value = "unstable")] 19 | suite: String, 20 | 21 | /// Committer string (name and email) 22 | #[clap(short, long)] 23 | committer: Option, 24 | 25 | /// Document changes in the changelog [default: auto-detect] 26 | #[arg(long, default_value_t = false, conflicts_with = "no_update_changelog")] 27 | update_changelog: bool, 28 | 29 | /// Do not document changes in the changelog (useful when using e.g. "gbp dch") [default: auto-detect] 30 | #[arg(long, default_value_t = false, conflicts_with = "update_changelog")] 31 | no_update_changelog: bool, 32 | 33 | /// Output directory. 34 | #[clap(short, long)] 35 | output_directory: Option, 36 | 37 | /// Build command 38 | #[clap(short, long, default_value = "sbuild -A -s -v")] 39 | build_command: String, 40 | 41 | /// Maximum number of issues to attempt to fix before giving up. 42 | #[clap(short, long, default_value = "10")] 43 | max_iterations: usize, 44 | 45 | #[cfg(target_os = "linux")] 46 | /// chroot to use. 47 | #[clap(short, long)] 48 | schroot: Option, 49 | 50 | /// ognibuild dep server to use 51 | #[clap(short, long, env = "OGNIBUILD_DEPS")] 52 | dep_server_url: Option, 53 | 54 | /// Be verbose 55 | #[clap(short, long)] 56 | verbose: bool, 57 | 58 | /// Directory to use 59 | #[clap(short, long, default_value = ".")] 60 | directory: PathBuf, 61 | } 62 | 63 | fn main() -> Result<(), i32> { 64 | let args = Args::parse(); 65 | 66 | let update_changelog: Option = if args.update_changelog { 67 | Some(true) 68 | } else if args.no_update_changelog { 69 | Some(false) 70 | } else { 71 | None 72 | }; 73 | 74 | env_logger::builder() 75 | .format(|buf, record| writeln!(buf, "{}", record.args())) 76 | .filter( 77 | None, 78 | if args.verbose { 79 | log::LevelFilter::Debug 80 | } else { 81 | log::LevelFilter::Info 82 | }, 83 | ) 84 | .init(); 85 | 86 | let temp_output_dir; 87 | 88 | let output_directory = if let Some(output_directory) = &args.output_directory { 89 | if !output_directory.is_dir() { 90 | log::error!("output directory {:?} is not a directory", output_directory); 91 | std::process::exit(1); 92 | } 93 | output_directory.clone() 94 | } else { 95 | temp_output_dir = Some(tempfile::tempdir().unwrap()); 96 | log::info!("Using output directory {:?}", temp_output_dir); 97 | 98 | temp_output_dir.as_ref().unwrap().path().to_path_buf() 99 | }; 100 | 101 | let (tree, subpath) = breezyshim::workingtree::open_containing(&args.directory).unwrap(); 102 | 103 | #[cfg(target_os = "linux")] 104 | let session = if let Some(schroot) = &args.schroot { 105 | Box::new(SchrootSession::new(schroot, Some("deb-fix-build")).unwrap()) as Box 106 | } else { 107 | Box::new(PlainSession::new()) 108 | }; 109 | 110 | #[cfg(not(target_os = "linux"))] 111 | let session = Box::new(PlainSession::new()); 112 | 113 | let apt = ognibuild::debian::apt::AptManager::new(session.as_ref(), None); 114 | 115 | let committer = args 116 | .committer 117 | .as_ref() 118 | .map(|committer| breezyshim::config::parse_username(committer)); 119 | 120 | let packaging_context = ognibuild::debian::context::DebianPackagingContext::new( 121 | tree.clone(), 122 | &subpath, 123 | committer, 124 | args.update_changelog, 125 | Some(Box::new(breezyshim::commit::NullCommitReporter::new())), 126 | ); 127 | 128 | let fixers = ognibuild::debian::fixers::default_fixers(&packaging_context, &apt); 129 | 130 | match ognibuild::debian::fix_build::build_incrementally( 131 | &tree, 132 | Some(&args.suffix), 133 | Some(&args.suite), 134 | &output_directory, 135 | &args.build_command, 136 | fixers 137 | .iter() 138 | .map(|f| f.as_ref()) 139 | .collect::>() 140 | .as_slice(), 141 | None, 142 | Some(args.max_iterations), 143 | &subpath, 144 | None, 145 | None, 146 | None, 147 | None, 148 | update_changelog == Some(false), 149 | ) { 150 | Ok(build_result) => { 151 | log::info!( 152 | "Built {} - changes file at {:?}.", 153 | build_result.version, 154 | build_result.changes_names, 155 | ); 156 | Ok(()) 157 | } 158 | Err(IterateBuildError::Persistent(phase, error)) => { 159 | log::error!("Error during {}: {}", phase, error); 160 | if let Some(output_directory) = args.output_directory { 161 | rescue_build_log(&output_directory, Some(&tree)).unwrap(); 162 | } 163 | Err(1) 164 | } 165 | Err(IterateBuildError::Unidentified { 166 | phase, 167 | lines, 168 | secondary, 169 | .. 170 | }) => { 171 | let mut header = if let Some(phase) = phase { 172 | format!("Error during {}:", phase) 173 | } else { 174 | "Error:".to_string() 175 | }; 176 | if let Some(m) = secondary { 177 | let linenos = m.linenos(); 178 | write!( 179 | header, 180 | " on lines {}-{}", 181 | linenos[0], 182 | linenos[linenos.len() - 1] 183 | ) 184 | .unwrap(); 185 | } 186 | header.write_str(":").unwrap(); 187 | log::error!("{}", header); 188 | for line in lines { 189 | log::error!(" {}", line); 190 | } 191 | if let Some(output_directory) = args.output_directory { 192 | rescue_build_log(&output_directory, Some(&tree)).unwrap(); 193 | } 194 | Err(1) 195 | } 196 | Err(IterateBuildError::FixerLimitReached(n)) => { 197 | log::error!("Fixer limit reached - {} attempts.", n); 198 | Err(1) 199 | } 200 | Err(IterateBuildError::Other(o)) => { 201 | log::error!("Error: {}", o); 202 | Err(1) 203 | } 204 | Err(IterateBuildError::MissingPhase) => { 205 | log::error!("Missing phase"); 206 | Err(1) 207 | } 208 | Err(IterateBuildError::ResetTree(e)) => { 209 | log::error!("Error resetting tree: {}", e); 210 | Err(1) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/bin/deb-upstream-deps.rs: -------------------------------------------------------------------------------- 1 | use breezyshim::error::Error as BrzError; 2 | use breezyshim::workingtree::{self, WorkingTree}; 3 | use clap::Parser; 4 | use debian_analyzer::editor::{Editor, MutableTreeEdit}; 5 | use debian_control::lossless::relations::Relations; 6 | use ognibuild::session::Session; 7 | use std::io::Write; 8 | use std::path::PathBuf; 9 | 10 | #[derive(Parser)] 11 | struct Args { 12 | #[clap(short, long)] 13 | /// Be verbose 14 | debug: bool, 15 | 16 | #[clap(short, long)] 17 | /// Update current package 18 | update: bool, 19 | 20 | #[clap(short, long, default_value = ".")] 21 | /// Directory to run in 22 | directory: PathBuf, 23 | } 24 | 25 | fn main() -> Result<(), i8> { 26 | let args = Args::parse(); 27 | 28 | env_logger::builder() 29 | .format(|buf, record| writeln!(buf, "{}", record.args())) 30 | .filter( 31 | None, 32 | if args.debug { 33 | log::LevelFilter::Debug 34 | } else { 35 | log::LevelFilter::Info 36 | }, 37 | ) 38 | .init(); 39 | 40 | let (wt, subpath) = match workingtree::open_containing(&args.directory) { 41 | Ok((wt, subpath)) => (wt, subpath), 42 | Err(e @ BrzError::NotBranchError { .. }) => { 43 | log::error!("please run deps in an existing branch: {}", e); 44 | return Err(1); 45 | } 46 | Err(e) => { 47 | log::error!("error opening working tree: {}", e); 48 | return Err(1); 49 | } 50 | }; 51 | 52 | let mut build_deps = vec![]; 53 | let mut test_deps = vec![]; 54 | 55 | let mut session: Box = Box::new(ognibuild::session::plain::PlainSession::new()); 56 | let project = session.project_from_vcs(&wt, None, None).unwrap(); 57 | for (bs_subpath, bs) in 58 | ognibuild::buildsystem::scan_buildsystems(&wt.abspath(&subpath).unwrap()) 59 | { 60 | session 61 | .chdir(&project.internal_path().join(&bs_subpath)) 62 | .unwrap(); 63 | 64 | let (bs_build_deps, bs_test_deps) = 65 | ognibuild::debian::upstream_deps::get_project_wide_deps(session.as_ref(), bs.as_ref()); 66 | build_deps.extend(bs_build_deps); 67 | test_deps.extend(bs_test_deps); 68 | } 69 | if !build_deps.is_empty() { 70 | println!( 71 | "Build-Depends: {}", 72 | build_deps 73 | .iter() 74 | .map(|x| x.relation_string()) 75 | .collect::>() 76 | .join(", ") 77 | ); 78 | } 79 | if !test_deps.is_empty() { 80 | println!( 81 | "Test-Depends: {}", 82 | test_deps 83 | .iter() 84 | .map(|x| x.relation_string()) 85 | .collect::>() 86 | .join(", ") 87 | ); 88 | } 89 | if args.update { 90 | let edit = wt 91 | .edit_file::(&subpath.join("debian/control"), true, true) 92 | .unwrap(); 93 | 94 | let mut source = edit.source().unwrap(); 95 | 96 | for build_dep in build_deps { 97 | for entry in build_dep.iter() { 98 | let mut relations = source.build_depends().unwrap_or_else(Relations::new); 99 | let old_str = relations.to_string(); 100 | debian_analyzer::relations::ensure_relation(&mut relations, entry); 101 | if old_str != relations.to_string() { 102 | log::info!("Bumped to {}", relations); 103 | source.set_build_depends(&relations); 104 | } 105 | } 106 | } 107 | 108 | edit.commit().unwrap(); 109 | } 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /src/bin/dep-server.rs: -------------------------------------------------------------------------------- 1 | use axum::{routing::get, Router}; 2 | use clap::Parser; 3 | use ognibuild::debian::apt::AptManager; 4 | #[cfg(target_os = "linux")] 5 | use ognibuild::session::schroot::SchrootSession; 6 | use ognibuild::session::{plain::PlainSession, Session}; 7 | use std::io::Write; 8 | 9 | #[derive(Parser)] 10 | struct Args { 11 | #[clap(short, long)] 12 | listen_address: String, 13 | 14 | #[clap(short, long)] 15 | port: u16, 16 | 17 | #[cfg(target_os = "linux")] 18 | #[clap(short, long)] 19 | schroot: Option, 20 | 21 | #[clap(short, long)] 22 | debug: bool, 23 | } 24 | 25 | #[tokio::main] 26 | async fn main() -> Result<(), i8> { 27 | let args = Args::parse(); 28 | 29 | env_logger::builder() 30 | .format(|buf, record| writeln!(buf, "{}", record.args())) 31 | .filter( 32 | None, 33 | if args.debug { 34 | log::LevelFilter::Debug 35 | } else { 36 | log::LevelFilter::Info 37 | }, 38 | ) 39 | .init(); 40 | 41 | #[cfg(target_os = "linux")] 42 | let session: Box = if let Some(schroot) = args.schroot { 43 | Box::new(SchrootSession::new(&schroot, None).unwrap()) 44 | } else { 45 | Box::new(PlainSession::new()) 46 | }; 47 | 48 | #[cfg(not(target_os = "linux"))] 49 | let session: Box = Box::new(PlainSession::new()); 50 | 51 | let _apt_mgr = AptManager::from_session(session.as_ref()); 52 | 53 | let app = Router::new() 54 | .route("/health", get(|| async { "ok" })) 55 | .route("/version", get(|| async { env!("CARGO_PKG_VERSION") })) 56 | .route("/ready", get(|| async { "ok" })); 57 | 58 | let listener = tokio::net::TcpListener::bind((args.listen_address.as_str(), args.port)) 59 | .await 60 | .unwrap(); 61 | log::info!("listening on {}", listener.local_addr().unwrap()); 62 | axum::serve(listener, app).await.unwrap(); 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/bin/ognibuild-deb.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ognibuild::debian::build::{BuildOnceError, DEFAULT_BUILDER}; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Parser)] 7 | struct Args { 8 | #[clap(short, long)] 9 | suffix: Option, 10 | #[clap(long, default_value = DEFAULT_BUILDER)] 11 | build_command: String, 12 | #[clap(short, long, default_value = "..")] 13 | output_directory: PathBuf, 14 | #[clap(short, long)] 15 | build_suite: Option, 16 | #[clap(long)] 17 | debug: bool, 18 | #[clap(long)] 19 | build_changelog_entry: Option, 20 | #[clap(short, long, default_value = ".")] 21 | /// The directory to build in 22 | directory: PathBuf, 23 | /// Use gbp dch to generate the changelog entry 24 | #[clap(long, default_value = "false")] 25 | gbp_dch: bool, 26 | } 27 | 28 | pub fn main() -> Result<(), i32> { 29 | let args = Args::parse(); 30 | let dir = args.directory; 31 | let (wt, subpath) = breezyshim::workingtree::open_containing(&dir).unwrap(); 32 | 33 | breezyshim::init(); 34 | breezyshim::plugin::load_plugins(); 35 | 36 | env_logger::builder() 37 | .format(|buf, record| writeln!(buf, "{}", record.args())) 38 | .filter( 39 | None, 40 | if args.debug { 41 | log::LevelFilter::Debug 42 | } else { 43 | log::LevelFilter::Info 44 | }, 45 | ) 46 | .init(); 47 | 48 | log::info!("Using output directory {}", args.output_directory.display()); 49 | 50 | if args.suffix.is_some() && args.build_changelog_entry.is_none() { 51 | log::warn!("--suffix is ignored without --build-changelog-entry"); 52 | } 53 | 54 | if args.build_changelog_entry.is_some() && args.build_suite.is_none() { 55 | log::error!("--build-changelog-entry requires --build-suite"); 56 | return Err(1); 57 | } 58 | 59 | let source_date_epoch = std::env::var("SOURCE_DATE_EPOCH") 60 | .ok() 61 | .map(|s| s.parse().unwrap()); 62 | 63 | match ognibuild::debian::build::attempt_build( 64 | &wt, 65 | args.suffix.as_deref(), 66 | args.build_suite.as_deref(), 67 | &args.output_directory, 68 | &args.build_command, 69 | args.build_changelog_entry.as_deref(), 70 | &subpath, 71 | source_date_epoch, 72 | args.gbp_dch, 73 | None, 74 | None, 75 | None, 76 | ) { 77 | Ok(_) => {} 78 | Err(BuildOnceError::Unidentified { 79 | phase, description, .. 80 | }) => { 81 | if let Some(phase) = phase { 82 | log::error!("build failed during {}: {}", phase, description); 83 | } else { 84 | log::error!("build failed: {}", description); 85 | } 86 | return Err(1); 87 | } 88 | Err(BuildOnceError::Detailed { 89 | phase, 90 | description, 91 | error, 92 | .. 93 | }) => { 94 | if let Some(phase) = phase { 95 | log::error!("build failed during {}: {}", phase, description); 96 | } else { 97 | log::error!("build failed: {}", description); 98 | } 99 | log::info!("error: {:?}", error); 100 | return Err(1); 101 | } 102 | } 103 | 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /src/bin/ognibuild-dist.rs: -------------------------------------------------------------------------------- 1 | use breezyshim::export::export; 2 | use breezyshim::tree::Tree; 3 | use breezyshim::workingtree::{self, WorkingTree}; 4 | use clap::Parser; 5 | #[cfg(feature = "debian")] 6 | use debian_control::Control; 7 | use ognibuild::analyze::AnalyzedError; 8 | use ognibuild::buildsystem::Error; 9 | use std::path::{Path, PathBuf}; 10 | 11 | #[derive(Clone, Default, PartialEq, Eq)] 12 | pub enum Mode { 13 | #[default] 14 | Auto, 15 | Vcs, 16 | Buildsystem, 17 | } 18 | 19 | impl std::str::FromStr for Mode { 20 | type Err = String; 21 | 22 | fn from_str(s: &str) -> Result { 23 | match s { 24 | "auto" => Ok(Mode::Auto), 25 | "vcs" => Ok(Mode::Vcs), 26 | "buildsystem" => Ok(Mode::Buildsystem), 27 | _ => Err(format!("Unknown mode: {}", s)), 28 | } 29 | } 30 | } 31 | 32 | impl std::fmt::Display for Mode { 33 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 34 | match self { 35 | Mode::Auto => write!(f, "auto"), 36 | Mode::Vcs => write!(f, "vcs"), 37 | Mode::Buildsystem => write!(f, "buildsystem"), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Parser)] 43 | struct Args { 44 | #[clap(short, long, default_value = "unstable-amd64-sbuild")] 45 | /// Name of chroot to use 46 | chroot: String, 47 | 48 | #[clap(default_value = ".")] 49 | /// Directory with upstream source. 50 | directory: PathBuf, 51 | 52 | #[clap(long)] 53 | /// Path to packaging directory. 54 | packaging_directory: Option, 55 | 56 | #[clap(long, default_value = "..")] 57 | /// Target directory 58 | target_directory: PathBuf, 59 | 60 | #[clap(long)] 61 | /// Enable debug output. 62 | debug: bool, 63 | 64 | #[clap(long, default_value = "auto")] 65 | /// Mechanism to use to create buildsystem 66 | mode: Mode, 67 | 68 | #[clap(long)] 69 | /// Include control directory in tarball. 70 | include_controldir: bool, 71 | } 72 | 73 | pub fn main() -> Result<(), i32> { 74 | let args = Args::parse(); 75 | env_logger::builder() 76 | .filter_level(if args.debug { 77 | log::LevelFilter::Debug 78 | } else { 79 | log::LevelFilter::Info 80 | }) 81 | .init(); 82 | 83 | let (tree, subpath) = workingtree::open_containing(&args.directory).unwrap(); 84 | 85 | #[cfg(feature = "debian")] 86 | let (packaging_tree, packaging_subdir, package_name): ( 87 | Option>, 88 | Option, 89 | Option, 90 | ) = if let Some(packaging_directory) = &args.packaging_directory { 91 | let (packaging_tree, packaging_subpath) = 92 | workingtree::open_containing(packaging_directory).unwrap(); 93 | let text = packaging_tree 94 | .get_file(Path::new("debian/control")) 95 | .unwrap(); 96 | let control: Control = Control::read(text).unwrap(); 97 | let package_name = control.source().unwrap().name().unwrap(); 98 | ( 99 | Some(Box::new(packaging_tree)), 100 | Some(packaging_subpath), 101 | Some(package_name), 102 | ) 103 | } else { 104 | (None, None, None) 105 | }; 106 | 107 | #[cfg(not(feature = "debian"))] 108 | let (packaging_tree, packaging_subdir): ( 109 | Option>, 110 | Option, 111 | Option, 112 | ) = (None, None, None); 113 | 114 | match args.mode { 115 | Mode::Vcs => { 116 | export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); 117 | Ok(()) 118 | } 119 | Mode::Auto | Mode::Buildsystem => { 120 | #[cfg(not(target_os = "linux"))] 121 | { 122 | log::error!("Unsupported mode: {}", args.mode); 123 | Err(1) 124 | } 125 | #[cfg(target_os = "linux")] 126 | match ognibuild::dist::create_dist_schroot( 127 | &tree, 128 | &args.target_directory.canonicalize().unwrap(), 129 | &args.chroot, 130 | packaging_tree.as_ref().map(|t| &**t as &dyn Tree), 131 | packaging_subdir.as_deref(), 132 | Some(args.include_controldir), 133 | &subpath, 134 | &mut ognibuild::logs::NoLogManager, 135 | None, 136 | package_name.as_deref(), 137 | ) { 138 | Ok(ret) => { 139 | log::info!("Created {}", ret.to_str().unwrap()); 140 | Ok(()) 141 | } 142 | Err(Error::IoError(e)) => { 143 | log::error!("IO error: {}", e); 144 | Err(1) 145 | } 146 | Err(Error::DependencyInstallError(e)) => { 147 | log::error!("Dependency install error: {}", e); 148 | Err(1) 149 | } 150 | Err(Error::NoBuildSystemDetected) => { 151 | if args.mode == Mode::Buildsystem { 152 | log::error!("No build system detected, unable to create tarball"); 153 | Err(1) 154 | } else { 155 | log::info!("No build system detected, falling back to simple export."); 156 | export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); 157 | Ok(()) 158 | } 159 | } 160 | Err(Error::Unimplemented) => { 161 | if args.mode == Mode::Buildsystem { 162 | log::error!("Unable to ask buildsystem for tarball"); 163 | Err(1) 164 | } else { 165 | log::info!("Build system does not support dist tarball creation, falling back to simple export."); 166 | export(&tree, Path::new("dist.tar.gz"), Some(&subpath)).unwrap(); 167 | Ok(()) 168 | } 169 | } 170 | Err(Error::Error(AnalyzedError::Unidentified { lines, .. })) => { 171 | log::error!("Unidentified error: {:?}", lines); 172 | Err(1) 173 | } 174 | Err(Error::Error(AnalyzedError::Detailed { error, .. })) => { 175 | log::error!("Identified error during dist creation: {}", error); 176 | Err(1) 177 | } 178 | Err(e) => { 179 | log::error!("Error: {}", e); 180 | Err(1) 181 | } 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/bin/report-apt-deps-status.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use ognibuild::buildsystem::{detect_buildsystems, DependencyCategory}; 3 | use ognibuild::debian::apt::{dependency_to_deb_dependency, AptManager}; 4 | use ognibuild::dependencies::debian::{ 5 | default_tie_breakers, DebianDependency, DebianDependencyCategory, 6 | }; 7 | use ognibuild::dependency::Dependency; 8 | use ognibuild::session::plain::PlainSession; 9 | use ognibuild::session::Session; 10 | use std::collections::HashMap; 11 | use std::io::Write; 12 | use std::path::PathBuf; 13 | 14 | #[derive(Parser)] 15 | struct Args { 16 | #[clap(long)] 17 | detailed: bool, 18 | 19 | directory: PathBuf, 20 | 21 | #[clap(long)] 22 | debug: bool, 23 | } 24 | 25 | fn main() -> Result<(), i32> { 26 | let args = Args::parse(); 27 | let mut session = PlainSession::new(); 28 | 29 | env_logger::builder() 30 | .format(|buf, record| writeln!(buf, "{}", record.args())) 31 | .filter( 32 | None, 33 | if args.debug { 34 | log::LevelFilter::Debug 35 | } else { 36 | log::LevelFilter::Info 37 | }, 38 | ) 39 | .init(); 40 | 41 | let directory = args.directory.canonicalize().unwrap(); 42 | 43 | session.chdir(&directory).unwrap(); 44 | 45 | let bss = detect_buildsystems(&directory); 46 | 47 | if bss.is_empty() { 48 | eprintln!("No build tools found"); 49 | std::process::exit(1); 50 | } 51 | 52 | log::debug!("Detected buildsystems: {:?}", bss); 53 | 54 | let mut deps: HashMap>> = HashMap::new(); 55 | 56 | for buildsystem in bss { 57 | match buildsystem.get_declared_dependencies(&session, Some(&[])) { 58 | Ok(declared_reqs) => { 59 | for (stage, req) in declared_reqs { 60 | deps.entry(stage).or_default().push(req); 61 | } 62 | } 63 | Err(_e) => { 64 | log::warn!( 65 | "Unable to get dependencies from buildsystem {:?}, skipping", 66 | buildsystem 67 | ); 68 | continue; 69 | } 70 | } 71 | } 72 | 73 | let tie_breakers = default_tie_breakers(&session); 74 | 75 | let apt = AptManager::new(&mut session, None); 76 | 77 | if args.detailed { 78 | let mut unresolved = false; 79 | for (stage, deps) in deps.iter() { 80 | log::info!("Stage: {}", stage); 81 | for dep in deps { 82 | if let Some(deb_dep) = 83 | dependency_to_deb_dependency(&apt, dep.as_ref(), &tie_breakers).unwrap() 84 | { 85 | log::info!("Dependency: {:?} → {}", dep, deb_dep.relation_string()); 86 | } else { 87 | log::warn!("Dependency: {:?} → ??", dep); 88 | unresolved = true; 89 | } 90 | } 91 | log::info!(""); 92 | } 93 | if unresolved { 94 | Err(1) 95 | } else { 96 | Ok(()) 97 | } 98 | } else { 99 | let mut dep_depends: HashMap> = 100 | HashMap::new(); 101 | let mut unresolved = vec![]; 102 | for (stage, reqs) in deps.iter() { 103 | for dep in reqs { 104 | if let Some(deb_dep) = 105 | dependency_to_deb_dependency(&apt, dep.as_ref(), &tie_breakers).unwrap() 106 | { 107 | match stage { 108 | DependencyCategory::Universal => { 109 | dep_depends 110 | .entry(DebianDependencyCategory::Build) 111 | .or_default() 112 | .push(deb_dep.clone()); 113 | dep_depends 114 | .entry(DebianDependencyCategory::Runtime) 115 | .or_default() 116 | .push(deb_dep); 117 | } 118 | DependencyCategory::Build => { 119 | dep_depends 120 | .entry(DebianDependencyCategory::Build) 121 | .or_default() 122 | .push(deb_dep); 123 | } 124 | DependencyCategory::Runtime => { 125 | dep_depends 126 | .entry(DebianDependencyCategory::Runtime) 127 | .or_default() 128 | .push(deb_dep); 129 | } 130 | DependencyCategory::BuildExtra(_name) => { 131 | // TODO: handle build extra: build profile? 132 | } 133 | DependencyCategory::Test => { 134 | dep_depends 135 | .entry(DebianDependencyCategory::Test("test".to_string())) 136 | .or_default() 137 | .push(deb_dep); 138 | } 139 | DependencyCategory::Dev => {} 140 | DependencyCategory::RuntimeExtra(_name) => { 141 | // TODO: handle runtime extra 142 | } 143 | } 144 | } else { 145 | unresolved.push(dep); 146 | } 147 | } 148 | } 149 | for (category, deps) in dep_depends.iter() { 150 | match category { 151 | DebianDependencyCategory::Build => { 152 | log::info!( 153 | "Build-Depends: {}", 154 | deps.iter() 155 | .map(|d| d.relation_string()) 156 | .collect::>() 157 | .join(", ") 158 | ); 159 | } 160 | DebianDependencyCategory::Runtime => { 161 | log::info!( 162 | "Depends: {}", 163 | deps.iter() 164 | .map(|d| d.relation_string()) 165 | .collect::>() 166 | .join(", ") 167 | ); 168 | } 169 | DebianDependencyCategory::Test(test) => { 170 | log::info!( 171 | "Test-Depends ({}): {}", 172 | test, 173 | deps.iter() 174 | .map(|d| d.relation_string()) 175 | .collect::>() 176 | .join(", ") 177 | ); 178 | } 179 | DebianDependencyCategory::Install => { 180 | log::info!( 181 | "Pre-Depends: {}", 182 | deps.iter() 183 | .map(|d| d.relation_string()) 184 | .collect::>() 185 | .join(", ") 186 | ); 187 | } 188 | } 189 | } 190 | if !unresolved.is_empty() { 191 | log::warn!("Unable to find apt packages for the following dependencies:"); 192 | for req in unresolved { 193 | log::warn!("* {:?}", req); 194 | } 195 | Err(1) 196 | } else { 197 | Ok(()) 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/buildlog.rs: -------------------------------------------------------------------------------- 1 | use crate::dependency::Dependency; 2 | use buildlog_consultant::problems::common::*; 3 | use buildlog_consultant::problems::debian::UnsatisfiedAptDependencies; 4 | use buildlog_consultant::Problem; 5 | 6 | /// Trait for converting build problems to dependencies. 7 | /// 8 | /// This trait allows build problems to report what dependencies would be needed to fix them. 9 | pub trait ToDependency: Problem { 10 | /// Convert this problem to a dependency that might fix it. 11 | /// 12 | /// # Returns 13 | /// * `Some(Box)` if the problem can be fixed by installing a dependency 14 | /// * `None` if the problem cannot be fixed by installing a dependency 15 | fn to_dependency(&self) -> Option>; 16 | } 17 | 18 | macro_rules! try_problem_to_dependency { 19 | ($expr:expr, $type:ty) => { 20 | if let Some(p) = $expr 21 | .as_any() 22 | .downcast_ref::<$type>() 23 | .and_then(|p| p.to_dependency()) 24 | { 25 | return Some(p); 26 | } 27 | }; 28 | } 29 | 30 | /// Convert a build problem to a dependency that might fix it. 31 | /// 32 | /// This function tries to convert various known problem types to dependencies 33 | /// that might fix them. 34 | /// 35 | /// # Arguments 36 | /// * `problem` - The build problem to convert 37 | /// 38 | /// # Returns 39 | /// * `Some(Box)` if the problem can be fixed by installing a dependency 40 | /// * `None` if the problem cannot be fixed by installing a dependency or isn't recognized 41 | pub fn problem_to_dependency(problem: &dyn Problem) -> Option> { 42 | // TODO(jelmer): Find a more idiomatic way to do this. 43 | try_problem_to_dependency!(problem, MissingAutoconfMacro); 44 | #[cfg(feature = "debian")] 45 | try_problem_to_dependency!(problem, UnsatisfiedAptDependencies); 46 | try_problem_to_dependency!(problem, MissingGoPackage); 47 | try_problem_to_dependency!(problem, MissingHaskellDependencies); 48 | try_problem_to_dependency!(problem, MissingJavaClass); 49 | try_problem_to_dependency!(problem, MissingJDK); 50 | try_problem_to_dependency!(problem, MissingJRE); 51 | try_problem_to_dependency!(problem, MissingJDKFile); 52 | try_problem_to_dependency!(problem, MissingLatexFile); 53 | try_problem_to_dependency!(problem, MissingCommand); 54 | try_problem_to_dependency!(problem, MissingCommandOrBuildFile); 55 | try_problem_to_dependency!(problem, VcsControlDirectoryNeeded); 56 | try_problem_to_dependency!(problem, MissingLuaModule); 57 | try_problem_to_dependency!(problem, MissingCargoCrate); 58 | try_problem_to_dependency!(problem, MissingRustCompiler); 59 | try_problem_to_dependency!(problem, MissingPkgConfig); 60 | try_problem_to_dependency!(problem, MissingFile); 61 | try_problem_to_dependency!(problem, MissingCHeader); 62 | try_problem_to_dependency!(problem, MissingJavaScriptRuntime); 63 | try_problem_to_dependency!(problem, MissingValaPackage); 64 | try_problem_to_dependency!(problem, MissingRubyGem); 65 | try_problem_to_dependency!(problem, DhAddonLoadFailure); 66 | try_problem_to_dependency!(problem, MissingLibrary); 67 | try_problem_to_dependency!(problem, MissingStaticLibrary); 68 | try_problem_to_dependency!(problem, MissingRubyFile); 69 | try_problem_to_dependency!(problem, MissingSprocketsFile); 70 | try_problem_to_dependency!(problem, CMakeFilesMissing); 71 | try_problem_to_dependency!(problem, MissingMavenArtifacts); 72 | try_problem_to_dependency!(problem, MissingGnomeCommonDependency); 73 | try_problem_to_dependency!(problem, MissingQtModules); 74 | try_problem_to_dependency!(problem, MissingQt); 75 | try_problem_to_dependency!(problem, MissingX11); 76 | try_problem_to_dependency!(problem, UnknownCertificateAuthority); 77 | try_problem_to_dependency!(problem, MissingLibtool); 78 | try_problem_to_dependency!(problem, MissingCMakeComponents); 79 | try_problem_to_dependency!(problem, MissingGnulibDirectory); 80 | try_problem_to_dependency!(problem, MissingIntrospectionTypelib); 81 | try_problem_to_dependency!(problem, MissingCSharpCompiler); 82 | try_problem_to_dependency!(problem, MissingXfceDependency); 83 | try_problem_to_dependency!(problem, MissingNodePackage); 84 | try_problem_to_dependency!(problem, MissingNodeModule); 85 | try_problem_to_dependency!(problem, MissingPerlPredeclared); 86 | try_problem_to_dependency!(problem, MissingPerlFile); 87 | try_problem_to_dependency!(problem, MissingPerlModule); 88 | try_problem_to_dependency!(problem, MissingPhpClass); 89 | try_problem_to_dependency!(problem, MissingPHPExtension); 90 | try_problem_to_dependency!(problem, MissingPytestFixture); 91 | try_problem_to_dependency!(problem, UnsupportedPytestArguments); 92 | try_problem_to_dependency!(problem, UnsupportedPytestConfigOption); 93 | try_problem_to_dependency!(problem, MissingPythonDistribution); 94 | try_problem_to_dependency!(problem, MissingPythonModule); 95 | try_problem_to_dependency!(problem, MissingSetupPyCommand); 96 | try_problem_to_dependency!(problem, MissingRPackage); 97 | try_problem_to_dependency!(problem, MissingVagueDependency); 98 | try_problem_to_dependency!(problem, MissingXmlEntity); 99 | try_problem_to_dependency!(problem, MissingMakeTarget); 100 | 101 | None 102 | } 103 | -------------------------------------------------------------------------------- /src/buildsystems/bazel.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | #[derive(Debug)] 5 | /// Bazel build system representation. 6 | pub struct Bazel { 7 | #[allow(dead_code)] 8 | path: PathBuf, 9 | } 10 | 11 | impl Bazel { 12 | /// Create a new Bazel build system instance. 13 | /// 14 | /// # Arguments 15 | /// * `path` - Path to the Bazel project directory 16 | /// 17 | /// # Returns 18 | /// A new Bazel instance 19 | pub fn new(path: &Path) -> Self { 20 | Self { 21 | path: path.to_path_buf(), 22 | } 23 | } 24 | 25 | /// Probe a directory to check if it contains a Bazel build system. 26 | /// 27 | /// # Arguments 28 | /// * `path` - Path to check for Bazel build files 29 | /// 30 | /// # Returns 31 | /// Some(BuildSystem) if a Bazel build is found, None otherwise 32 | pub fn probe(path: &Path) -> Option> { 33 | if path.join("BUILD").exists() { 34 | Some(Box::new(Self::new(path))) 35 | } else { 36 | None 37 | } 38 | } 39 | 40 | /// Check if a Bazel build system exists at the specified path. 41 | /// 42 | /// # Arguments 43 | /// * `path` - Path to check for Bazel build files 44 | /// 45 | /// # Returns 46 | /// true if a BUILD file exists, false otherwise 47 | pub fn exists(path: &Path) -> bool { 48 | path.join("BUILD").exists() 49 | } 50 | } 51 | 52 | impl BuildSystem for Bazel { 53 | fn name(&self) -> &str { 54 | "bazel" 55 | } 56 | 57 | fn dist( 58 | &self, 59 | _session: &dyn crate::session::Session, 60 | _installer: &dyn crate::installer::Installer, 61 | _target_directory: &Path, 62 | _quiet: bool, 63 | ) -> Result { 64 | Err(Error::Unimplemented) 65 | } 66 | 67 | fn test( 68 | &self, 69 | session: &dyn crate::session::Session, 70 | _installer: &dyn crate::installer::Installer, 71 | ) -> Result<(), crate::buildsystem::Error> { 72 | session 73 | .command(vec!["bazel", "test", "//..."]) 74 | .run_detecting_problems()?; 75 | Ok(()) 76 | } 77 | 78 | fn build( 79 | &self, 80 | session: &dyn crate::session::Session, 81 | _installer: &dyn crate::installer::Installer, 82 | ) -> Result<(), crate::buildsystem::Error> { 83 | session 84 | .command(vec!["bazel", "build", "//..."]) 85 | .run_detecting_problems()?; 86 | Ok(()) 87 | } 88 | 89 | fn clean( 90 | &self, 91 | _session: &dyn crate::session::Session, 92 | _installer: &dyn crate::installer::Installer, 93 | ) -> Result<(), crate::buildsystem::Error> { 94 | Err(Error::Unimplemented) 95 | } 96 | 97 | fn install( 98 | &self, 99 | session: &dyn crate::session::Session, 100 | _installer: &dyn crate::installer::Installer, 101 | _install_target: &crate::buildsystem::InstallTarget, 102 | ) -> Result<(), crate::buildsystem::Error> { 103 | session 104 | .command(vec!["bazel", "build", "//..."]) 105 | .run_detecting_problems()?; 106 | Err(Error::Unimplemented) 107 | } 108 | 109 | fn get_declared_dependencies( 110 | &self, 111 | _session: &dyn crate::session::Session, 112 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 113 | ) -> Result< 114 | Vec<( 115 | crate::buildsystem::DependencyCategory, 116 | Box, 117 | )>, 118 | crate::buildsystem::Error, 119 | > { 120 | Err(Error::Unimplemented) 121 | } 122 | 123 | fn get_declared_outputs( 124 | &self, 125 | _session: &dyn crate::session::Session, 126 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 127 | ) -> Result>, crate::buildsystem::Error> { 128 | Err(Error::Unimplemented) 129 | } 130 | 131 | fn as_any(&self) -> &dyn std::any::Any { 132 | self 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/buildsystems/gnome.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; 2 | use crate::dependencies::vague::VagueDependency; 3 | use std::path::{Path, PathBuf}; 4 | 5 | #[derive(Debug)] 6 | /// Representation of a GNOME Shell extension. 7 | pub struct GnomeShellExtension { 8 | path: PathBuf, 9 | } 10 | 11 | #[derive(Debug, serde::Deserialize)] 12 | #[allow(dead_code)] 13 | struct Metadata { 14 | name: String, 15 | description: String, 16 | uuid: String, 17 | shell_version: String, 18 | version: String, 19 | url: String, 20 | license: String, 21 | authors: Vec, 22 | settings_schema: Option, 23 | gettext_domain: Option, 24 | extension: Option, 25 | _generated: Option, 26 | } 27 | 28 | impl GnomeShellExtension { 29 | /// Create a new GNOME Shell extension instance. 30 | /// 31 | /// # Arguments 32 | /// * `path` - Path to the GNOME Shell extension directory 33 | /// 34 | /// # Returns 35 | /// A new GnomeShellExtension instance 36 | pub fn new(path: PathBuf) -> Self { 37 | Self { path } 38 | } 39 | 40 | /// Check if a GNOME Shell extension exists at the specified path. 41 | /// 42 | /// # Arguments 43 | /// * `path` - Path to check for GNOME Shell extension 44 | /// 45 | /// # Returns 46 | /// true if metadata.json exists, false otherwise 47 | pub fn exists(path: &PathBuf) -> bool { 48 | path.join("metadata.json").exists() 49 | } 50 | 51 | /// Probe a directory to check if it contains a GNOME Shell extension. 52 | /// 53 | /// # Arguments 54 | /// * `path` - Path to check for GNOME Shell extension files 55 | /// 56 | /// # Returns 57 | /// Some(BuildSystem) if a GNOME Shell extension is found, None otherwise 58 | pub fn probe(path: &Path) -> Option> { 59 | if Self::exists(&path.to_path_buf()) { 60 | log::debug!("Found metadata.json , assuming gnome-shell extension."); 61 | Some(Box::new(Self::new(path.to_path_buf()))) 62 | } else { 63 | None 64 | } 65 | } 66 | } 67 | 68 | impl BuildSystem for GnomeShellExtension { 69 | fn name(&self) -> &str { 70 | "gnome-shell-extension" 71 | } 72 | 73 | fn dist( 74 | &self, 75 | _session: &dyn crate::session::Session, 76 | _installer: &dyn crate::installer::Installer, 77 | _target_directory: &std::path::Path, 78 | _quiet: bool, 79 | ) -> Result { 80 | Err(Error::Unimplemented) 81 | } 82 | 83 | fn test( 84 | &self, 85 | _session: &dyn crate::session::Session, 86 | _installer: &dyn crate::installer::Installer, 87 | ) -> Result<(), crate::buildsystem::Error> { 88 | Ok(()) 89 | } 90 | 91 | fn build( 92 | &self, 93 | _session: &dyn crate::session::Session, 94 | _installer: &dyn crate::installer::Installer, 95 | ) -> Result<(), crate::buildsystem::Error> { 96 | Ok(()) 97 | } 98 | 99 | fn clean( 100 | &self, 101 | _session: &dyn crate::session::Session, 102 | _installer: &dyn crate::installer::Installer, 103 | ) -> Result<(), crate::buildsystem::Error> { 104 | Err(Error::Unimplemented) 105 | } 106 | 107 | fn install( 108 | &self, 109 | _session: &dyn crate::session::Session, 110 | _installer: &dyn crate::installer::Installer, 111 | _install_target: &crate::buildsystem::InstallTarget, 112 | ) -> Result<(), crate::buildsystem::Error> { 113 | Err(Error::Unimplemented) 114 | } 115 | 116 | fn get_declared_dependencies( 117 | &self, 118 | _session: &dyn crate::session::Session, 119 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 120 | ) -> Result< 121 | Vec<( 122 | crate::buildsystem::DependencyCategory, 123 | Box, 124 | )>, 125 | crate::buildsystem::Error, 126 | > { 127 | let f = std::fs::File::open(self.path.join("metadata.json")).unwrap(); 128 | 129 | let metadata: Metadata = serde_json::from_reader(f).unwrap(); 130 | 131 | let deps: Vec<(DependencyCategory, Box)> = vec![( 132 | DependencyCategory::Universal, 133 | Box::new(VagueDependency::new( 134 | "gnome-shell", 135 | Some(&metadata.shell_version), 136 | )), 137 | )]; 138 | 139 | Ok(deps) 140 | } 141 | 142 | fn get_declared_outputs( 143 | &self, 144 | _session: &dyn crate::session::Session, 145 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 146 | ) -> Result>, crate::buildsystem::Error> { 147 | Err(Error::Unimplemented) 148 | } 149 | 150 | fn as_any(&self) -> &dyn std::any::Any { 151 | self 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/buildsystems/haskell.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, Error}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | #[derive(Debug)] 5 | /// Haskell Cabal build system representation. 6 | pub struct Cabal { 7 | #[allow(dead_code)] 8 | path: PathBuf, 9 | } 10 | 11 | impl Cabal { 12 | /// Create a new Cabal build system instance. 13 | /// 14 | /// # Arguments 15 | /// * `path` - Path to the Cabal project directory 16 | /// 17 | /// # Returns 18 | /// A new Cabal instance 19 | pub fn new(path: PathBuf) -> Self { 20 | Self { path } 21 | } 22 | 23 | /// Run a Cabal command with the given arguments. 24 | /// 25 | /// Handles common Cabal errors, such as needing to run configure first. 26 | /// 27 | /// # Arguments 28 | /// * `session` - The session to run the command in 29 | /// * `extra_args` - Additional arguments to pass to the Cabal command 30 | /// 31 | /// # Returns 32 | /// Ok(()) if the command succeeded, otherwise an error 33 | fn run( 34 | &self, 35 | session: &dyn crate::session::Session, 36 | extra_args: Vec<&str>, 37 | ) -> Result<(), crate::analyze::AnalyzedError> { 38 | let mut args = vec!["runhaskell", "Setup.hs"]; 39 | args.extend(extra_args); 40 | match session.command(args.clone()).run_detecting_problems() { 41 | Ok(ls) => Ok(ls), 42 | Err(crate::analyze::AnalyzedError::Unidentified { lines, .. }) 43 | if lines.contains(&"Run the 'configure' command first.".to_string()) => 44 | { 45 | session 46 | .command(vec!["runhaskell", "Setup.hs", "configure"]) 47 | .run_detecting_problems()?; 48 | session.command(args).run_detecting_problems() 49 | } 50 | Err(e) => Err(e), 51 | } 52 | .map(|_| ()) 53 | } 54 | 55 | /// Probe a directory to check if it contains a Cabal project. 56 | /// 57 | /// # Arguments 58 | /// * `path` - Path to check for Cabal project files 59 | /// 60 | /// # Returns 61 | /// Some(BuildSystem) if a Cabal project is found, None otherwise 62 | pub fn probe(path: &Path) -> Option> { 63 | if path.join("Setup.hs").exists() { 64 | Some(Box::new(Self::new(path.to_owned()))) 65 | } else { 66 | None 67 | } 68 | } 69 | } 70 | 71 | impl BuildSystem for Cabal { 72 | fn name(&self) -> &str { 73 | "cabal" 74 | } 75 | 76 | fn dist( 77 | &self, 78 | session: &dyn crate::session::Session, 79 | _installer: &dyn crate::installer::Installer, 80 | target_directory: &std::path::Path, 81 | _quiet: bool, 82 | ) -> Result { 83 | let dc = crate::dist_catcher::DistCatcher::new(vec![ 84 | session.external_path(Path::new("dist-newstyle/sdist")), 85 | session.external_path(Path::new("dist")), 86 | ]); 87 | self.run(session, vec!["sdist"])?; 88 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 89 | } 90 | 91 | fn test( 92 | &self, 93 | session: &dyn crate::session::Session, 94 | _installer: &dyn crate::installer::Installer, 95 | ) -> Result<(), crate::buildsystem::Error> { 96 | self.run(session, vec!["test"])?; 97 | Ok(()) 98 | } 99 | 100 | fn build( 101 | &self, 102 | _session: &dyn crate::session::Session, 103 | _installer: &dyn crate::installer::Installer, 104 | ) -> Result<(), crate::buildsystem::Error> { 105 | Err(Error::Unimplemented) 106 | } 107 | 108 | fn clean( 109 | &self, 110 | _session: &dyn crate::session::Session, 111 | _installer: &dyn crate::installer::Installer, 112 | ) -> Result<(), crate::buildsystem::Error> { 113 | Err(Error::Unimplemented) 114 | } 115 | 116 | fn install( 117 | &self, 118 | _session: &dyn crate::session::Session, 119 | _installer: &dyn crate::installer::Installer, 120 | _install_target: &crate::buildsystem::InstallTarget, 121 | ) -> Result<(), crate::buildsystem::Error> { 122 | Err(Error::Unimplemented) 123 | } 124 | 125 | fn get_declared_dependencies( 126 | &self, 127 | _session: &dyn crate::session::Session, 128 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 129 | ) -> Result< 130 | Vec<( 131 | crate::buildsystem::DependencyCategory, 132 | Box, 133 | )>, 134 | crate::buildsystem::Error, 135 | > { 136 | Err(Error::Unimplemented) 137 | } 138 | 139 | fn get_declared_outputs( 140 | &self, 141 | _session: &dyn crate::session::Session, 142 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 143 | ) -> Result>, crate::buildsystem::Error> { 144 | Err(Error::Unimplemented) 145 | } 146 | 147 | fn as_any(&self) -> &dyn std::any::Any { 148 | self 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/buildsystems/meson.rs: -------------------------------------------------------------------------------- 1 | use crate::analyze::AnalyzedError; 2 | use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; 3 | use crate::dependencies::vague::VagueDependency; 4 | use crate::dependency::Dependency; 5 | use crate::dist_catcher::DistCatcher; 6 | use crate::fix_build::BuildFixer; 7 | use crate::installer::Error as InstallerError; 8 | use crate::session::Session; 9 | use std::path::{Path, PathBuf}; 10 | 11 | #[derive(Debug)] 12 | /// Meson build system. 13 | /// 14 | /// Handles projects built with Meson and Ninja. 15 | pub struct Meson { 16 | #[allow(dead_code)] 17 | path: PathBuf, 18 | } 19 | 20 | #[derive(Debug, serde::Deserialize)] 21 | #[allow(dead_code)] 22 | struct MesonDependency { 23 | pub name: String, 24 | pub version: Vec, 25 | pub required: bool, 26 | pub has_fallback: bool, 27 | pub conditional: bool, 28 | } 29 | 30 | #[derive(Debug, serde::Deserialize)] 31 | #[allow(dead_code)] 32 | struct MesonTarget { 33 | r#type: String, 34 | installed: bool, 35 | filename: Vec, 36 | } 37 | 38 | impl Meson { 39 | /// Create a new Meson build system with the specified path. 40 | pub fn new(path: &Path) -> Self { 41 | Self { 42 | path: path.to_path_buf(), 43 | } 44 | } 45 | 46 | fn setup(&self, session: &dyn Session) -> Result<(), Error> { 47 | if !session.exists(Path::new("build")) { 48 | session.mkdir(Path::new("build")).unwrap(); 49 | } 50 | session 51 | .command(vec!["meson", "setup", "build"]) 52 | .run_detecting_problems()?; 53 | Ok(()) 54 | } 55 | 56 | fn introspect serde::Deserialize<'a>>( 57 | &self, 58 | session: &dyn Session, 59 | fixers: Option<&[&dyn BuildFixer]>, 60 | args: &[&str], 61 | ) -> Result { 62 | let args = [&["meson", "introspect"], args, &["./meson.build"]].concat(); 63 | let ret = if let Some(fixers) = fixers { 64 | session 65 | .command(args) 66 | .quiet(true) 67 | .run_fixing_problems::<_, Error>(fixers) 68 | .unwrap() 69 | } else { 70 | session.command(args).run_detecting_problems()? 71 | }; 72 | 73 | let text = ret.concat(); 74 | 75 | Ok(serde_json::from_str(&text).unwrap()) 76 | } 77 | 78 | /// Probe a directory for a Meson build system. 79 | /// 80 | /// Returns a Meson build system if a meson.build file is found. 81 | pub fn probe(path: &Path) -> Option> { 82 | let path = path.join("meson.build"); 83 | if path.exists() { 84 | log::debug!("Found meson.build, assuming meson package."); 85 | Some(Box::new(Self::new(&path))) 86 | } else { 87 | None 88 | } 89 | } 90 | } 91 | 92 | impl BuildSystem for Meson { 93 | fn name(&self) -> &str { 94 | "meson" 95 | } 96 | 97 | fn dist( 98 | &self, 99 | session: &dyn Session, 100 | _installer: &dyn crate::installer::Installer, 101 | target_directory: &Path, 102 | _quiet: bool, 103 | ) -> Result { 104 | self.setup(session)?; 105 | let dc = DistCatcher::new(vec![session.external_path(Path::new("build/meson-dist"))]); 106 | match session 107 | .command(vec!["ninja", "-C", "build", "dist"]) 108 | .run_detecting_problems() 109 | { 110 | Ok(_) => {} 111 | Err(AnalyzedError::Unidentified { lines, .. }) 112 | if lines.contains( 113 | &"ninja: error: unknown target 'dist', did you mean 'dino'?".to_string(), 114 | ) => 115 | { 116 | unimplemented!(); 117 | } 118 | Err(e) => return Err(e.into()), 119 | } 120 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 121 | } 122 | 123 | fn test( 124 | &self, 125 | session: &dyn Session, 126 | _installer: &dyn crate::installer::Installer, 127 | ) -> Result<(), Error> { 128 | self.setup(session)?; 129 | session 130 | .command(vec!["ninja", "-C", "build", "test"]) 131 | .run_detecting_problems()?; 132 | Ok(()) 133 | } 134 | 135 | fn build( 136 | &self, 137 | session: &dyn Session, 138 | _installer: &dyn crate::installer::Installer, 139 | ) -> Result<(), Error> { 140 | self.setup(session)?; 141 | session 142 | .command(vec!["ninja", "-C", "build"]) 143 | .run_detecting_problems()?; 144 | Ok(()) 145 | } 146 | 147 | fn clean( 148 | &self, 149 | session: &dyn Session, 150 | _installer: &dyn crate::installer::Installer, 151 | ) -> Result<(), Error> { 152 | self.setup(session)?; 153 | session 154 | .command(vec!["ninja", "-C", "build", "clean"]) 155 | .run_detecting_problems()?; 156 | Ok(()) 157 | } 158 | 159 | fn install( 160 | &self, 161 | session: &dyn Session, 162 | _installer: &dyn crate::installer::Installer, 163 | _install_target: &crate::buildsystem::InstallTarget, 164 | ) -> Result<(), Error> { 165 | self.setup(session)?; 166 | session 167 | .command(vec!["ninja", "-C", "build", "install"]) 168 | .run_detecting_problems()?; 169 | Ok(()) 170 | } 171 | 172 | fn get_declared_dependencies( 173 | &self, 174 | session: &dyn Session, 175 | fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 176 | ) -> Result)>, Error> { 177 | let mut ret: Vec<(DependencyCategory, Box)> = Vec::new(); 178 | let resp = 179 | self.introspect::>(session, fixers, &["--scan-dependencies"])?; 180 | for entry in resp { 181 | let mut minimum_version = None; 182 | if entry.version.len() == 1 { 183 | if let Some(rest) = entry.version[0].strip_prefix(">=") { 184 | minimum_version = Some(rest.trim().to_string()); 185 | } 186 | } else if entry.version.len() > 1 { 187 | log::warn!("Unable to parse version constraints: {:?}", entry.version); 188 | } 189 | // TODO(jelmer): Include entry['required'] 190 | ret.push(( 191 | DependencyCategory::Universal, 192 | Box::new(VagueDependency { 193 | name: entry.name.to_string(), 194 | minimum_version, 195 | }), 196 | )); 197 | } 198 | Ok(ret) 199 | } 200 | 201 | fn get_declared_outputs( 202 | &self, 203 | session: &dyn Session, 204 | fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 205 | ) -> Result>, Error> { 206 | let mut ret: Vec> = Vec::new(); 207 | let resp = self.introspect::>(session, fixers, &["--targets"])?; 208 | for entry in resp { 209 | if !entry.installed { 210 | continue; 211 | } 212 | if entry.r#type == "executable" { 213 | for p in entry.filename { 214 | ret.push(Box::new(crate::output::BinaryOutput::new( 215 | p.file_name().unwrap().to_str().unwrap(), 216 | ))); 217 | } 218 | } 219 | // TODO(jelmer): Handle other types 220 | } 221 | 222 | Ok(ret) 223 | } 224 | 225 | fn as_any(&self) -> &dyn std::any::Any { 226 | self 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/buildsystems/mod.rs: -------------------------------------------------------------------------------- 1 | /// Bazel build system implementation. 2 | pub mod bazel; 3 | /// GNOME build system implementation. 4 | pub mod gnome; 5 | /// Go build system implementation. 6 | pub mod go; 7 | /// Haskell build system implementation. 8 | pub mod haskell; 9 | /// Java build system implementation. 10 | pub mod java; 11 | /// Make build system implementation. 12 | pub mod make; 13 | /// Meson build system implementation. 14 | pub mod meson; 15 | /// Node.js build system implementation. 16 | pub mod node; 17 | /// Octave build system implementation. 18 | pub mod octave; 19 | /// Perl build system implementation. 20 | pub mod perl; 21 | /// Python build system implementation. 22 | pub mod python; 23 | /// R build system implementation. 24 | pub mod r; 25 | /// Ruby build system implementation. 26 | pub mod ruby; 27 | /// Rust build system implementation. 28 | pub mod rust; 29 | /// Waf build system implementation. 30 | pub mod waf; 31 | -------------------------------------------------------------------------------- /src/buildsystems/node.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, DependencyCategory, Error}; 2 | use crate::dependencies::node::NodePackageDependency; 3 | use crate::dependencies::BinaryDependency; 4 | use crate::dependency::Dependency; 5 | use crate::installer::{Error as InstallerError, InstallationScope, Installer}; 6 | use crate::session::Session; 7 | use serde::Deserialize; 8 | use std::collections::HashMap; 9 | use std::path::PathBuf; 10 | 11 | #[derive(Debug)] 12 | #[allow(dead_code)] 13 | /// Node.js build system. 14 | /// 15 | /// Handles Node.js projects with a package.json file. 16 | pub struct Node { 17 | path: PathBuf, 18 | package: NodePackage, 19 | } 20 | 21 | #[derive(Debug, Deserialize)] 22 | struct NodePackage { 23 | dependencies: HashMap, 24 | #[serde(rename = "devDependencies")] 25 | dev_dependencies: HashMap, 26 | scripts: HashMap, 27 | } 28 | 29 | impl Node { 30 | /// Create a new Node build system with the specified path to package.json. 31 | pub fn new(path: PathBuf) -> Self { 32 | let package = path.join("package.json"); 33 | 34 | let package = std::fs::read_to_string(&package).unwrap(); 35 | 36 | let package: NodePackage = serde_json::from_str(&package).unwrap(); 37 | 38 | Self { path, package } 39 | } 40 | 41 | fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { 42 | let binary_req = BinaryDependency::new("npm"); 43 | if !binary_req.present(session) { 44 | installer.install(&binary_req, InstallationScope::Global)?; 45 | } 46 | Ok(()) 47 | } 48 | 49 | /// Probe a directory for a Node.js build system. 50 | /// 51 | /// Returns a Node build system if a package.json file is found. 52 | pub fn probe(path: &std::path::Path) -> Option> { 53 | let path = path.join("package.json"); 54 | if path.exists() { 55 | log::debug!("Found package.json, assuming node package."); 56 | return Some(Box::new(Self::new(path))); 57 | } 58 | None 59 | } 60 | } 61 | 62 | impl BuildSystem for Node { 63 | fn get_declared_dependencies( 64 | &self, 65 | _session: &dyn Session, 66 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 67 | ) -> Result)>, Error> { 68 | let mut dependencies: Vec<(DependencyCategory, Box)> = vec![]; 69 | 70 | for (name, _version) in self.package.dependencies.iter() { 71 | // TODO(jelmer): Look at version 72 | dependencies.push(( 73 | DependencyCategory::Universal, 74 | Box::new(NodePackageDependency::new(name)), 75 | )); 76 | } 77 | 78 | for (name, _version) in self.package.dev_dependencies.iter() { 79 | // TODO(jelmer): Look at version 80 | dependencies.push(( 81 | DependencyCategory::Build, 82 | Box::new(NodePackageDependency::new(name)), 83 | )); 84 | } 85 | 86 | Ok(dependencies) 87 | } 88 | 89 | fn name(&self) -> &str { 90 | "node" 91 | } 92 | 93 | fn dist( 94 | &self, 95 | session: &dyn Session, 96 | installer: &dyn crate::installer::Installer, 97 | target_directory: &std::path::Path, 98 | quiet: bool, 99 | ) -> Result { 100 | self.setup(session, installer)?; 101 | let dc = crate::dist_catcher::DistCatcher::new(vec![ 102 | session.external_path(std::path::Path::new(".")) 103 | ]); 104 | session 105 | .command(vec!["npm", "pack"]) 106 | .quiet(quiet) 107 | .run_detecting_problems()?; 108 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 109 | } 110 | 111 | fn test( 112 | &self, 113 | session: &dyn crate::session::Session, 114 | installer: &dyn crate::installer::Installer, 115 | ) -> Result<(), crate::buildsystem::Error> { 116 | self.setup(session, installer)?; 117 | if let Some(test_script) = self.package.scripts.get("test") { 118 | session 119 | .command(vec!["bash", "-c", test_script]) 120 | .run_detecting_problems()?; 121 | } else { 122 | log::info!("No test command defined in package.json"); 123 | } 124 | Ok(()) 125 | } 126 | 127 | fn build( 128 | &self, 129 | session: &dyn crate::session::Session, 130 | installer: &dyn crate::installer::Installer, 131 | ) -> Result<(), crate::buildsystem::Error> { 132 | self.setup(session, installer)?; 133 | if let Some(build_script) = self.package.scripts.get("build") { 134 | session 135 | .command(vec!["bash", "-c", build_script]) 136 | .run_detecting_problems()?; 137 | } else { 138 | log::info!("No build command defined in package.json"); 139 | } 140 | Ok(()) 141 | } 142 | 143 | fn clean( 144 | &self, 145 | session: &dyn crate::session::Session, 146 | installer: &dyn crate::installer::Installer, 147 | ) -> Result<(), crate::buildsystem::Error> { 148 | self.setup(session, installer)?; 149 | if let Some(clean_script) = self.package.scripts.get("clean") { 150 | session 151 | .command(vec!["bash", "-c", clean_script]) 152 | .run_detecting_problems()?; 153 | } else { 154 | log::info!("No clean command defined in package.json"); 155 | } 156 | Ok(()) 157 | } 158 | 159 | fn install( 160 | &self, 161 | _session: &dyn crate::session::Session, 162 | _installer: &dyn crate::installer::Installer, 163 | _install_target: &crate::buildsystem::InstallTarget, 164 | ) -> Result<(), crate::buildsystem::Error> { 165 | Err(Error::Unimplemented) 166 | } 167 | 168 | fn get_declared_outputs( 169 | &self, 170 | _session: &dyn crate::session::Session, 171 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 172 | ) -> Result>, crate::buildsystem::Error> { 173 | Err(Error::Unimplemented) 174 | } 175 | 176 | fn as_any(&self) -> &dyn std::any::Any { 177 | self 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/buildsystems/r.rs: -------------------------------------------------------------------------------- 1 | //! Support for R build systems. 2 | //! 3 | //! This module provides functionality for building, testing, and installing 4 | //! R packages using R CMD build and related commands. 5 | 6 | use crate::buildsystem::guaranteed_which; 7 | use crate::buildsystem::{BuildSystem, DependencyCategory}; 8 | use crate::dependencies::r::RPackageDependency; 9 | use crate::dependency::Dependency; 10 | use crate::dist_catcher::DistCatcher; 11 | use crate::output::RPackageOutput; 12 | use std::path::{Path, PathBuf}; 13 | 14 | #[derive(Debug)] 15 | /// R build system for R packages. 16 | /// 17 | /// This build system handles R packages using R CMD commands for building, 18 | /// testing, and installation. 19 | pub struct R { 20 | path: PathBuf, 21 | } 22 | 23 | impl R { 24 | /// Create a new R build system with the specified path. 25 | /// 26 | /// # Arguments 27 | /// * `path` - The path to the R package directory 28 | /// 29 | /// # Returns 30 | /// A new R build system instance 31 | pub fn new(path: PathBuf) -> Self { 32 | Self { path } 33 | } 34 | 35 | /// Run R CMD check on the package to check for issues. 36 | /// 37 | /// # Arguments 38 | /// * `session` - The session to run the command in 39 | /// * `installer` - The installer to use for dependencies 40 | /// 41 | /// # Returns 42 | /// Ok on success or an error 43 | pub fn lint( 44 | &self, 45 | session: &dyn crate::session::Session, 46 | installer: &dyn crate::installer::Installer, 47 | ) -> Result<(), crate::buildsystem::Error> { 48 | let r_path = guaranteed_which(session, installer, "R").unwrap(); 49 | session 50 | .command(vec![r_path.to_str().unwrap(), "CMD", "check"]) 51 | .run_detecting_problems()?; 52 | Ok(()) 53 | } 54 | 55 | /// Probe a directory for an R package. 56 | /// 57 | /// # Arguments 58 | /// * `path` - The path to check 59 | /// 60 | /// # Returns 61 | /// An R build system if an R package exists at the path, `None` otherwise 62 | pub fn probe(path: &Path) -> Option> { 63 | if path.join("DESCRIPTION").exists() && path.join("NAMESPACE").exists() { 64 | Some(Box::new(Self::new(path.to_path_buf()))) 65 | } else { 66 | None 67 | } 68 | } 69 | } 70 | 71 | impl BuildSystem for R { 72 | fn name(&self) -> &str { 73 | "R" 74 | } 75 | 76 | fn dist( 77 | &self, 78 | session: &dyn crate::session::Session, 79 | installer: &dyn crate::installer::Installer, 80 | target_directory: &Path, 81 | _quiet: bool, 82 | ) -> Result { 83 | let dc = DistCatcher::new(vec![session.external_path(Path::new("."))]); 84 | let r_path = guaranteed_which(session, installer, "R").unwrap(); 85 | session 86 | .command(vec![r_path.to_str().unwrap(), "CMD", "build", "."]) 87 | .run_detecting_problems()?; 88 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 89 | } 90 | 91 | fn test( 92 | &self, 93 | session: &dyn crate::session::Session, 94 | installer: &dyn crate::installer::Installer, 95 | ) -> Result<(), crate::buildsystem::Error> { 96 | let r_path = guaranteed_which(session, installer, "R").unwrap(); 97 | if session.exists(Path::new("run_tests.sh")) { 98 | session 99 | .command(vec!["./run_tests.sh"]) 100 | .run_detecting_problems()?; 101 | } else if session.exists(Path::new("tests/testthat")) { 102 | session 103 | .command(vec![ 104 | r_path.to_str().unwrap(), 105 | "-e", 106 | "testthat::test_dir('tests')", 107 | ]) 108 | .run_detecting_problems()?; 109 | } 110 | Ok(()) 111 | } 112 | 113 | fn build( 114 | &self, 115 | _session: &dyn crate::session::Session, 116 | _installer: &dyn crate::installer::Installer, 117 | ) -> Result<(), crate::buildsystem::Error> { 118 | // Nothing to do here 119 | Ok(()) 120 | } 121 | 122 | fn clean( 123 | &self, 124 | _session: &dyn crate::session::Session, 125 | _installer: &dyn crate::installer::Installer, 126 | ) -> Result<(), crate::buildsystem::Error> { 127 | Err(crate::buildsystem::Error::Unimplemented) 128 | } 129 | 130 | fn install( 131 | &self, 132 | session: &dyn crate::session::Session, 133 | installer: &dyn crate::installer::Installer, 134 | install_target: &crate::buildsystem::InstallTarget, 135 | ) -> Result<(), crate::buildsystem::Error> { 136 | let r_path = guaranteed_which(session, installer, "R").unwrap(); 137 | let mut args = vec![ 138 | r_path.to_str().unwrap().to_string(), 139 | "CMD".to_string(), 140 | "INSTALL".to_string(), 141 | ".".to_string(), 142 | ]; 143 | if let Some(prefix) = &install_target.prefix.as_ref() { 144 | args.push(format!("--prefix={}", prefix.to_str().unwrap())); 145 | } 146 | session 147 | .command(args.iter().map(|s| s.as_str()).collect()) 148 | .run_detecting_problems()?; 149 | Ok(()) 150 | } 151 | 152 | fn get_declared_dependencies( 153 | &self, 154 | _session: &dyn crate::session::Session, 155 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 156 | ) -> Result< 157 | Vec<( 158 | crate::buildsystem::DependencyCategory, 159 | Box, 160 | )>, 161 | crate::buildsystem::Error, 162 | > { 163 | let mut ret: Vec<(DependencyCategory, Box)> = vec![]; 164 | let f = std::fs::File::open(self.path.join("DESCRIPTION")).unwrap(); 165 | let description = read_description(f).unwrap(); 166 | for s in description.suggests().unwrap_or_default().iter() { 167 | ret.push(( 168 | DependencyCategory::Build, /* TODO */ 169 | Box::new(RPackageDependency::from(s)), 170 | )); 171 | } 172 | for s in description.depends().unwrap_or_default().iter() { 173 | ret.push(( 174 | DependencyCategory::Build, 175 | Box::new(RPackageDependency::from(s)), 176 | )); 177 | } 178 | for s in description.imports().unwrap_or_default().iter() { 179 | ret.push(( 180 | DependencyCategory::Build, 181 | Box::new(RPackageDependency::from_str(&s)), 182 | )); 183 | } 184 | for s in description.linking_to().unwrap_or_default() { 185 | ret.push(( 186 | DependencyCategory::Build, 187 | Box::new(RPackageDependency::from_str(&s)), 188 | )); 189 | } 190 | Ok(ret) 191 | } 192 | 193 | fn get_declared_outputs( 194 | &self, 195 | _session: &dyn crate::session::Session, 196 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 197 | ) -> Result>, crate::buildsystem::Error> { 198 | let mut ret = vec![]; 199 | let f = std::fs::File::open(self.path.join("DESCRIPTION")).unwrap(); 200 | let description = read_description(f).unwrap(); 201 | if let Some(package) = description.package() { 202 | ret.push(Box::new(RPackageOutput::new(&package)) as Box); 203 | } 204 | Ok(ret) 205 | } 206 | 207 | fn as_any(&self) -> &dyn std::any::Any { 208 | self 209 | } 210 | } 211 | 212 | fn read_description( 213 | mut r: R, 214 | ) -> Result { 215 | // See https://r-pkgs.org/description.html 216 | let mut s = String::new(); 217 | r.read_to_string(&mut s).unwrap(); 218 | let p: r_description::lossless::RDescription = s.parse().unwrap(); 219 | Ok(p) 220 | } 221 | -------------------------------------------------------------------------------- /src/buildsystems/ruby.rs: -------------------------------------------------------------------------------- 1 | //! Support for Ruby build systems. 2 | //! 3 | //! This module provides functionality for building, testing, and installing 4 | //! Ruby gems using the gem command. 5 | 6 | use crate::buildsystem::{guaranteed_which, BuildSystem, Error}; 7 | use std::path::{Path, PathBuf}; 8 | 9 | /// Ruby gem build system. 10 | /// 11 | /// This build system handles Ruby gems for distribution and installation. 12 | #[derive(Debug)] 13 | pub struct Gem { 14 | path: PathBuf, 15 | } 16 | 17 | impl Gem { 18 | /// Create a new Ruby gem build system. 19 | /// 20 | /// # Arguments 21 | /// * `path` - Path to the gem file 22 | /// 23 | /// # Returns 24 | /// A new Gem instance 25 | pub fn new(path: PathBuf) -> Self { 26 | Self { path } 27 | } 28 | 29 | /// Probe a directory to check if it contains Ruby gem files. 30 | /// 31 | /// # Arguments 32 | /// * `path` - Path to check for gem files 33 | /// 34 | /// # Returns 35 | /// Some(BuildSystem) if gem files are found, None otherwise 36 | pub fn probe(path: &Path) -> Option> { 37 | let mut gemfiles = std::fs::read_dir(path) 38 | .unwrap() 39 | .filter_map(|entry| entry.ok().map(|entry| entry.path())) 40 | .filter(|path| path.extension().unwrap_or_default() == "gem") 41 | .collect::>(); 42 | if !gemfiles.is_empty() { 43 | Some(Box::new(Self::new(gemfiles.remove(0)))) 44 | } else { 45 | None 46 | } 47 | } 48 | } 49 | 50 | /// Implementation of BuildSystem for Ruby gems. 51 | impl BuildSystem for Gem { 52 | /// Get the name of this build system. 53 | /// 54 | /// # Returns 55 | /// The string "gem" 56 | fn name(&self) -> &str { 57 | "gem" 58 | } 59 | 60 | /// Create a distribution package from the gem file. 61 | /// 62 | /// # Arguments 63 | /// * `session` - Session to run commands in 64 | /// * `installer` - Installer to use for installing dependencies 65 | /// * `target_directory` - Directory to store the created distribution package 66 | /// * `quiet` - Whether to suppress output 67 | /// 68 | /// # Returns 69 | /// OsString with the name of the created distribution package, or an error 70 | fn dist( 71 | &self, 72 | session: &dyn crate::session::Session, 73 | installer: &dyn crate::installer::Installer, 74 | target_directory: &std::path::Path, 75 | quiet: bool, 76 | ) -> Result { 77 | let mut gemfiles = std::fs::read_dir(&self.path) 78 | .unwrap() 79 | .filter_map(|entry| entry.ok().map(|entry| entry.path())) 80 | .filter(|path| path.extension().unwrap_or_default() == "gem") 81 | .collect::>(); 82 | assert!(!gemfiles.is_empty()); 83 | if gemfiles.len() > 1 { 84 | log::warn!("More than one gemfile. Trying the first?"); 85 | } 86 | let dc = crate::dist_catcher::DistCatcher::default(&session.external_path(Path::new("."))); 87 | session 88 | .command(vec![ 89 | guaranteed_which(session, installer, "gem2tgz")? 90 | .to_str() 91 | .unwrap(), 92 | gemfiles.remove(0).to_str().unwrap(), 93 | ]) 94 | .quiet(quiet) 95 | .run_detecting_problems()?; 96 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 97 | } 98 | 99 | /// Run tests for this gem. 100 | /// 101 | /// # Arguments 102 | /// * `_session` - Session to run commands in 103 | /// * `_installer` - Installer to use for installing dependencies 104 | /// 105 | /// # Returns 106 | /// Always returns Error::Unimplemented as testing is not implemented for gems 107 | fn test( 108 | &self, 109 | _session: &dyn crate::session::Session, 110 | _installer: &dyn crate::installer::Installer, 111 | ) -> Result<(), Error> { 112 | Err(Error::Unimplemented) 113 | } 114 | 115 | /// Build this gem. 116 | /// 117 | /// # Arguments 118 | /// * `_session` - Session to run commands in 119 | /// * `_installer` - Installer to use for installing dependencies 120 | /// 121 | /// # Returns 122 | /// Always returns Error::Unimplemented as building is not implemented for gems 123 | fn build( 124 | &self, 125 | _session: &dyn crate::session::Session, 126 | _installer: &dyn crate::installer::Installer, 127 | ) -> Result<(), Error> { 128 | Err(Error::Unimplemented) 129 | } 130 | 131 | /// Clean build artifacts. 132 | /// 133 | /// # Arguments 134 | /// * `_session` - Session to run commands in 135 | /// * `_installer` - Installer to use for installing dependencies 136 | /// 137 | /// # Returns 138 | /// Always returns Error::Unimplemented as cleaning is not implemented for gems 139 | fn clean( 140 | &self, 141 | _session: &dyn crate::session::Session, 142 | _installer: &dyn crate::installer::Installer, 143 | ) -> Result<(), Error> { 144 | Err(Error::Unimplemented) 145 | } 146 | 147 | /// Install the gem. 148 | /// 149 | /// # Arguments 150 | /// * `_session` - Session to run commands in 151 | /// * `_installer` - Installer to use for installing dependencies 152 | /// * `_install_target` - Target installation directory 153 | /// 154 | /// # Returns 155 | /// Always returns Error::Unimplemented as installation is not implemented for gems 156 | fn install( 157 | &self, 158 | _session: &dyn crate::session::Session, 159 | _installer: &dyn crate::installer::Installer, 160 | _install_target: &crate::buildsystem::InstallTarget, 161 | ) -> Result<(), Error> { 162 | Err(Error::Unimplemented) 163 | } 164 | 165 | /// Get dependencies declared by this gem. 166 | /// 167 | /// # Arguments 168 | /// * `_session` - Session to run commands in 169 | /// * `_fixers` - Build fixers to use if needed 170 | /// 171 | /// # Returns 172 | /// Always returns Error::Unimplemented as dependency discovery is not implemented for gems 173 | fn get_declared_dependencies( 174 | &self, 175 | _session: &dyn crate::session::Session, 176 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 177 | ) -> Result< 178 | Vec<( 179 | crate::buildsystem::DependencyCategory, 180 | Box, 181 | )>, 182 | Error, 183 | > { 184 | Err(Error::Unimplemented) 185 | } 186 | 187 | /// Get outputs declared by this gem. 188 | /// 189 | /// # Arguments 190 | /// * `_session` - Session to run commands in 191 | /// * `_fixers` - Build fixers to use if needed 192 | /// 193 | /// # Returns 194 | /// Always returns Error::Unimplemented as output discovery is not implemented for gems 195 | fn get_declared_outputs( 196 | &self, 197 | _session: &dyn crate::session::Session, 198 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 199 | ) -> Result>, Error> { 200 | Err(Error::Unimplemented) 201 | } 202 | 203 | /// Convert this build system to Any for downcasting. 204 | /// 205 | /// # Returns 206 | /// Reference to self as Any 207 | fn as_any(&self) -> &dyn std::any::Any { 208 | self 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/buildsystems/waf.rs: -------------------------------------------------------------------------------- 1 | //! Support for Waf build systems. 2 | //! 3 | //! This module provides functionality for building, testing, and distributing 4 | //! software that uses the Waf build system. 5 | 6 | use crate::buildsystem::{BuildSystem, Error}; 7 | use crate::dependency::Dependency; 8 | use crate::installer::{InstallationScope, Installer}; 9 | use crate::session::Session; 10 | use std::path::PathBuf; 11 | 12 | /// Waf build system. 13 | /// 14 | /// This build system handles projects that use Waf for building and testing. 15 | #[derive(Debug)] 16 | pub struct Waf { 17 | #[allow(dead_code)] 18 | path: PathBuf, 19 | } 20 | 21 | impl Waf { 22 | /// Create a new Waf build system. 23 | /// 24 | /// # Arguments 25 | /// * `path` - Path to the waf script 26 | /// 27 | /// # Returns 28 | /// A new Waf instance 29 | pub fn new(path: PathBuf) -> Self { 30 | Self { path } 31 | } 32 | 33 | /// Set up the environment for using Waf. 34 | /// 35 | /// Ensures Python 3 is installed as it's required by Waf. 36 | /// 37 | /// # Arguments 38 | /// * `session` - Session to run commands in 39 | /// * `installer` - Installer to use for installing dependencies 40 | /// 41 | /// # Returns 42 | /// Ok on success, Error otherwise 43 | fn setup(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { 44 | let binary_req = crate::dependencies::BinaryDependency::new("python3"); 45 | if !binary_req.present(session) { 46 | installer.install(&binary_req, InstallationScope::Global)?; 47 | } 48 | Ok(()) 49 | } 50 | 51 | /// Probe a directory to check if it contains a Waf build system. 52 | /// 53 | /// # Arguments 54 | /// * `path` - Path to check for a waf script 55 | /// 56 | /// # Returns 57 | /// Some(BuildSystem) if a waf script is found, None otherwise 58 | pub fn probe(path: &std::path::Path) -> Option> { 59 | let path = path.join("waf"); 60 | if path.exists() { 61 | log::debug!("Found waf, assuming waf package."); 62 | Some(Box::new(Self::new(path))) 63 | } else { 64 | None 65 | } 66 | } 67 | } 68 | 69 | /// Implementation of BuildSystem for Waf. 70 | impl BuildSystem for Waf { 71 | /// Get the name of this build system. 72 | /// 73 | /// # Returns 74 | /// The string "waf" 75 | fn name(&self) -> &str { 76 | "waf" 77 | } 78 | 79 | /// Create a distribution package using waf dist command. 80 | /// 81 | /// # Arguments 82 | /// * `session` - Session to run commands in 83 | /// * `installer` - Installer to use for installing dependencies 84 | /// * `target_directory` - Directory to store the created distribution package 85 | /// * `quiet` - Whether to suppress output 86 | /// 87 | /// # Returns 88 | /// OsString with the name of the created distribution package, or an error 89 | fn dist( 90 | &self, 91 | session: &dyn Session, 92 | installer: &dyn Installer, 93 | target_directory: &std::path::Path, 94 | quiet: bool, 95 | ) -> Result { 96 | self.setup(session, installer)?; 97 | let dc = crate::dist_catcher::DistCatcher::default( 98 | &session.external_path(std::path::Path::new(".")), 99 | ); 100 | session 101 | .command(vec!["./waf", "dist"]) 102 | .quiet(quiet) 103 | .run_detecting_problems()?; 104 | Ok(dc.copy_single(target_directory).unwrap().unwrap()) 105 | } 106 | 107 | /// Run tests using waf test command. 108 | /// 109 | /// # Arguments 110 | /// * `session` - Session to run commands in 111 | /// * `installer` - Installer to use for installing dependencies 112 | /// 113 | /// # Returns 114 | /// Ok on success, Error otherwise 115 | fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { 116 | self.setup(session, installer)?; 117 | session 118 | .command(vec!["./waf", "test"]) 119 | .run_detecting_problems()?; 120 | Ok(()) 121 | } 122 | 123 | /// Build the project using waf build command. 124 | /// 125 | /// Automatically runs configure if necessary. 126 | /// 127 | /// # Arguments 128 | /// * `session` - Session to run commands in 129 | /// * `installer` - Installer to use for installing dependencies 130 | /// 131 | /// # Returns 132 | /// Ok on success, Error otherwise 133 | fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> { 134 | self.setup(session, installer)?; 135 | match session 136 | .command(vec!["./waf", "build"]) 137 | .run_detecting_problems() 138 | { 139 | Err(crate::analyze::AnalyzedError::Unidentified { lines, .. }) 140 | if lines.contains( 141 | &"The project was not configured: run \"waf configure\" first!".to_string(), 142 | ) => 143 | { 144 | session 145 | .command(vec!["./waf", "configure"]) 146 | .run_detecting_problems()?; 147 | session 148 | .command(vec!["./waf", "build"]) 149 | .run_detecting_problems() 150 | } 151 | other => other, 152 | }?; 153 | Ok(()) 154 | } 155 | 156 | /// Clean build artifacts. 157 | /// 158 | /// # Arguments 159 | /// * `_session` - Session to run commands in 160 | /// * `_installer` - Installer to use for installing dependencies 161 | /// 162 | /// # Returns 163 | /// Always returns Error::Unimplemented as cleaning is not implemented for Waf 164 | fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> { 165 | Err(Error::Unimplemented) 166 | } 167 | 168 | /// Install the built software. 169 | /// 170 | /// # Arguments 171 | /// * `_session` - Session to run commands in 172 | /// * `_installer` - Installer to use for installing dependencies 173 | /// * `_install_target` - Target installation directory 174 | /// 175 | /// # Returns 176 | /// Always returns Error::Unimplemented as installation is not implemented for Waf 177 | fn install( 178 | &self, 179 | _session: &dyn Session, 180 | _installer: &dyn Installer, 181 | _install_target: &crate::buildsystem::InstallTarget, 182 | ) -> Result<(), Error> { 183 | Err(Error::Unimplemented) 184 | } 185 | 186 | /// Get dependencies declared by this project. 187 | /// 188 | /// # Arguments 189 | /// * `_session` - Session to run commands in 190 | /// * `_fixers` - Build fixers to use if needed 191 | /// 192 | /// # Returns 193 | /// Always returns Error::Unimplemented as dependency discovery is not implemented for Waf 194 | fn get_declared_dependencies( 195 | &self, 196 | _session: &dyn Session, 197 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 198 | ) -> Result< 199 | Vec<( 200 | crate::buildsystem::DependencyCategory, 201 | Box, 202 | )>, 203 | Error, 204 | > { 205 | Err(Error::Unimplemented) 206 | } 207 | 208 | /// Get outputs declared by this project. 209 | /// 210 | /// # Arguments 211 | /// * `_session` - Session to run commands in 212 | /// * `_fixers` - Build fixers to use if needed 213 | /// 214 | /// # Returns 215 | /// Always returns Error::Unimplemented as output discovery is not implemented for Waf 216 | fn get_declared_outputs( 217 | &self, 218 | _session: &dyn Session, 219 | _fixers: Option<&[&dyn crate::fix_build::BuildFixer]>, 220 | ) -> Result>, Error> { 221 | Err(Error::Unimplemented) 222 | } 223 | 224 | /// Convert this build system to Any for downcasting. 225 | /// 226 | /// # Returns 227 | /// Reference to self as Any 228 | fn as_any(&self) -> &dyn std::any::Any { 229 | self 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/debian/build_deps.rs: -------------------------------------------------------------------------------- 1 | //! Debian build dependency handling. 2 | //! 3 | //! This module provides functionality for handling Debian build dependencies, 4 | //! including tie-breaking between multiple potential dependencies. 5 | 6 | use crate::dependencies::debian::DebianDependency; 7 | use crate::dependencies::debian::TieBreaker; 8 | use crate::session::Session; 9 | use breezyshim::debian::apt::{Apt, LocalApt}; 10 | use std::cell::RefCell; 11 | use std::collections::HashMap; 12 | 13 | /// Tie-breaker for Debian build dependencies. 14 | /// 15 | /// This tie-breaker selects the most commonly used dependency based on 16 | /// analyzing build dependencies across all source packages in the APT cache. 17 | pub struct BuildDependencyTieBreaker { 18 | /// Local APT instance for accessing package information 19 | apt: LocalApt, 20 | /// Cached counts of build dependency usage 21 | counts: RefCell>>, 22 | } 23 | 24 | impl BuildDependencyTieBreaker { 25 | /// Create a new BuildDependencyTieBreaker from a session. 26 | /// 27 | /// # Arguments 28 | /// * `session` - Session to use for accessing the local APT cache 29 | /// 30 | /// # Returns 31 | /// A new BuildDependencyTieBreaker instance 32 | pub fn from_session(session: &dyn Session) -> Self { 33 | Self { 34 | apt: LocalApt::new(Some(&session.location())).unwrap(), 35 | counts: RefCell::new(None), 36 | } 37 | } 38 | 39 | /// Count the occurrences of each build dependency across all source packages. 40 | /// 41 | /// This method scans all source packages in the APT cache and counts how many 42 | /// times each package is used as a build dependency. 43 | /// 44 | /// # Returns 45 | /// HashMap mapping package names to their usage count 46 | fn count(&self) -> HashMap { 47 | let mut counts = HashMap::new(); 48 | for source in self.apt.iter_sources() { 49 | source 50 | .build_depends() 51 | .into_iter() 52 | .chain(source.build_depends_indep().into_iter()) 53 | .chain(source.build_depends_arch().into_iter()) 54 | .for_each(|r| { 55 | for e in r.entries() { 56 | e.relations().for_each(|r| { 57 | let count = counts.entry(r.name().clone()).or_insert(0); 58 | *count += 1; 59 | }); 60 | } 61 | }); 62 | } 63 | counts 64 | } 65 | } 66 | 67 | /// Implementation of TieBreaker for BuildDependencyTieBreaker. 68 | impl TieBreaker for BuildDependencyTieBreaker { 69 | /// Break a tie between multiple Debian dependencies by selecting the most commonly used one. 70 | /// 71 | /// # Arguments 72 | /// * `reqs` - Slice of Debian dependency candidates to choose from 73 | /// 74 | /// # Returns 75 | /// The most commonly used dependency, or None if no candidates are available 76 | fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { 77 | if self.counts.borrow().is_none() { 78 | let counts = self.count(); 79 | self.counts.replace(Some(counts)); 80 | } 81 | 82 | let c = self.counts.borrow(); 83 | let count = c.clone().unwrap(); 84 | let mut by_count = HashMap::new(); 85 | for req in reqs { 86 | let name = req.package_names().into_iter().next().unwrap(); 87 | by_count.insert(req, count[&name]); 88 | } 89 | if by_count.is_empty() { 90 | return None; 91 | } 92 | let top = by_count.iter().max_by_key(|k| k.1).unwrap(); 93 | log::info!( 94 | "Breaking tie between [{:?}] to {:?} based on build-depends count", 95 | reqs.iter().map(|r| r.relation_string()).collect::>(), 96 | top.0.relation_string(), 97 | ); 98 | Some(*top.0) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/debian/dep_server.rs: -------------------------------------------------------------------------------- 1 | //! Dependency server integration for Debian packages. 2 | //! 3 | //! This module provides functionality for resolving dependencies using 4 | //! a remote dependency server that can translate generic dependencies 5 | //! into Debian package dependencies. 6 | 7 | use crate::debian::apt::AptManager; 8 | use crate::dependencies::debian::DebianDependency; 9 | use crate::dependency::Dependency; 10 | use crate::installer::{Error, Explanation, InstallationScope, Installer}; 11 | use crate::session::Session; 12 | use reqwest::StatusCode; 13 | use tokio::runtime::Runtime; 14 | use url::Url; 15 | 16 | /// Resolve a requirement to an APT requirement with a dep server. 17 | /// 18 | /// # Arguments 19 | /// * `url` - Dep server URL 20 | /// * `req` - Dependency to resolve 21 | /// 22 | /// # Returns 23 | /// List of APT requirements. 24 | async fn resolve_apt_requirement_dep_server( 25 | url: &url::Url, 26 | _dep: &dyn Dependency, 27 | ) -> Result, Error> { 28 | let client = reqwest::Client::new(); 29 | let response = client 30 | .post(url.join("resolve-apt").unwrap()) 31 | .json(&serde_json::json!( { 32 | "requirement": { 33 | // TODO: Use the actual dependency 34 | } 35 | })) 36 | .send() 37 | .await 38 | .unwrap(); 39 | 40 | match response.status() { 41 | StatusCode::NOT_FOUND => { 42 | if response 43 | .headers() 44 | .get("Reason") 45 | .map(|x| x.to_str().unwrap()) 46 | == Some("family-unknown") 47 | { 48 | return Err(Error::UnknownDependencyFamily); 49 | } 50 | Ok(None) 51 | } 52 | StatusCode::OK => { 53 | let body = response.json::().await.unwrap(); 54 | Ok(Some(body)) 55 | } 56 | _ => { 57 | panic!("Unexpected response status: {}", response.status()); 58 | } 59 | } 60 | } 61 | 62 | /// Installer that uses a dependency server to resolve and install dependencies. 63 | /// 64 | /// This installer connects to a remote dependency server that can translate 65 | /// generic dependencies into Debian package dependencies and then installs them 66 | /// using APT. 67 | pub struct DepServerAptInstaller<'a> { 68 | /// APT manager for package operations 69 | apt: AptManager<'a>, 70 | /// URL of the dependency server 71 | dep_server_url: Url, 72 | } 73 | 74 | impl<'a> DepServerAptInstaller<'a> { 75 | /// Create a new DepServerAptInstaller with the given APT manager and server URL. 76 | /// 77 | /// # Arguments 78 | /// * `apt` - APT manager to use for installing dependencies 79 | /// * `dep_server_url` - URL of the dependency server 80 | /// 81 | /// # Returns 82 | /// A new DepServerAptInstaller instance 83 | pub fn new(apt: AptManager<'a>, dep_server_url: &Url) -> Self { 84 | Self { 85 | apt, 86 | dep_server_url: dep_server_url.clone(), 87 | } 88 | } 89 | 90 | /// Create a new DepServerAptInstaller from a session and server URL. 91 | /// 92 | /// # Arguments 93 | /// * `session` - Session to use for running commands 94 | /// * `dep_server_url` - URL of the dependency server 95 | /// 96 | /// # Returns 97 | /// A new DepServerAptInstaller instance 98 | pub fn from_session(session: &'a dyn Session, dep_server_url: &'_ Url) -> Self { 99 | let apt = AptManager::from_session(session); 100 | Self::new(apt, dep_server_url) 101 | } 102 | 103 | /// Resolve a dependency to a Debian package dependency using the dependency server. 104 | /// 105 | /// # Arguments 106 | /// * `req` - Generic dependency to resolve 107 | /// 108 | /// # Returns 109 | /// Some(DebianDependency) if the server could resolve it, None if not found, 110 | /// or Error if there was a problem communicating with the server 111 | pub fn resolve(&self, req: &dyn Dependency) -> Result, Error> { 112 | let rt = Runtime::new().unwrap(); 113 | match rt.block_on(resolve_apt_requirement_dep_server( 114 | &self.dep_server_url, 115 | req, 116 | )) { 117 | Ok(deps) => Ok(deps), 118 | Err(o) => { 119 | log::warn!("Falling back to resolving error locally"); 120 | Err(Error::Other(o.to_string())) 121 | } 122 | } 123 | } 124 | } 125 | 126 | /// Implementation of the Installer trait for DepServerAptInstaller. 127 | impl<'a> Installer for DepServerAptInstaller<'a> { 128 | fn install( 129 | &self, 130 | dep: &dyn Dependency, 131 | scope: crate::installer::InstallationScope, 132 | ) -> Result<(), Error> { 133 | match scope { 134 | InstallationScope::User => { 135 | return Err(Error::UnsupportedScope(scope)); 136 | } 137 | InstallationScope::Global => {} 138 | InstallationScope::Vendor => { 139 | return Err(Error::UnsupportedScope(scope)); 140 | } 141 | } 142 | let dep = self.resolve(dep)?; 143 | 144 | if let Some(dep) = dep { 145 | match self 146 | .apt 147 | .satisfy(vec![crate::debian::apt::SatisfyEntry::Required( 148 | dep.relation_string(), 149 | )]) { 150 | Ok(_) => {} 151 | Err(e) => { 152 | return Err(Error::Other(e.to_string())); 153 | } 154 | } 155 | Ok(()) 156 | } else { 157 | Err(Error::UnknownDependencyFamily) 158 | } 159 | } 160 | 161 | fn explain( 162 | &self, 163 | dep: &dyn Dependency, 164 | scope: crate::installer::InstallationScope, 165 | ) -> Result { 166 | match scope { 167 | InstallationScope::User => { 168 | return Err(Error::UnsupportedScope(scope)); 169 | } 170 | InstallationScope::Global => {} 171 | InstallationScope::Vendor => { 172 | return Err(Error::UnsupportedScope(scope)); 173 | } 174 | } 175 | let dep = self.resolve(dep)?; 176 | 177 | let dep = dep.ok_or_else(|| Error::UnknownDependencyFamily)?; 178 | 179 | let apt_deb_str = dep.relation_string(); 180 | let cmd = self.apt.satisfy_command(vec![apt_deb_str.as_str()]); 181 | Ok(Explanation { 182 | message: format!( 183 | "Install {}", 184 | dep.package_names() 185 | .iter() 186 | .map(|x| x.as_str()) 187 | .collect::>() 188 | .join(", ") 189 | ), 190 | command: Some(cmd.iter().map(|s| s.to_string()).collect()), 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/debian/mod.rs: -------------------------------------------------------------------------------- 1 | //! Debian packaging support for ognibuild. 2 | //! 3 | //! This module provides functionality for working with Debian packages, 4 | //! including managing build dependencies, interacting with APT, 5 | //! fixing build issues, and working with Debian package sources. 6 | 7 | /// APT package management functionality. 8 | pub mod apt; 9 | /// Debian package build functionality. 10 | pub mod build; 11 | /// Build dependency resolution for Debian packages. 12 | pub mod build_deps; 13 | /// Context management for Debian operations. 14 | pub mod context; 15 | /// Dependency server integration. 16 | #[cfg(feature = "dep-server")] 17 | pub mod dep_server; 18 | /// File search utilities for Debian packages. 19 | pub mod file_search; 20 | /// Debian-specific build fixing functionality. 21 | pub mod fix_build; 22 | /// Build fixers for Debian packages. 23 | pub mod fixers; 24 | /// Debian sources.list handling. 25 | pub mod sources_list; 26 | /// Ultimate Debian Database integration. 27 | #[cfg(feature = "udd")] 28 | pub mod udd; 29 | /// Upstream dependency handling for Debian packages. 30 | pub mod upstream_deps; 31 | use breezyshim::tree::{Path, Tree}; 32 | 33 | use crate::session::Session; 34 | 35 | /// Satisfy build dependencies for a Debian package. 36 | /// 37 | /// This function parses the debian/control file and installs all required 38 | /// build dependencies while ensuring conflicts are resolved. 39 | /// 40 | /// # Arguments 41 | /// * `session` - Session to run commands in 42 | /// * `tree` - Tree representing the package source 43 | /// * `debian_path` - Path to the debian directory 44 | /// 45 | /// # Returns 46 | /// Ok on success, Error if dependencies cannot be satisfied 47 | pub fn satisfy_build_deps( 48 | session: &dyn Session, 49 | tree: &dyn Tree, 50 | debian_path: &Path, 51 | ) -> Result<(), apt::Error> { 52 | let path = debian_path.join("control"); 53 | 54 | let f = tree.get_file_text(&path).unwrap(); 55 | 56 | let control: debian_control::Control = String::from_utf8(f).unwrap().parse().unwrap(); 57 | 58 | let source = control.source().unwrap(); 59 | 60 | let mut deps = vec![]; 61 | 62 | for dep in source 63 | .build_depends() 64 | .iter() 65 | .chain(source.build_depends_indep().iter()) 66 | .chain(source.build_depends_arch().iter()) 67 | { 68 | deps.push(apt::SatisfyEntry::Required(dep.to_string())); 69 | } 70 | 71 | for dep in source 72 | .build_conflicts() 73 | .iter() 74 | .chain(source.build_conflicts_indep().iter()) 75 | .chain(source.build_conflicts_arch().iter()) 76 | { 77 | deps.push(apt::SatisfyEntry::Conflict(dep.to_string())); 78 | } 79 | 80 | let apt_mgr = apt::AptManager::new(session, None); 81 | apt_mgr.satisfy(deps) 82 | } 83 | -------------------------------------------------------------------------------- /src/debian/sources_list.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufRead, BufReader}; 3 | use std::path::Path; 4 | 5 | /// Entry in a Debian APT sources.list file. 6 | /// 7 | /// This enum represents the two types of entries that can appear in a 8 | /// sources.list file: 'deb' for binary packages and 'deb-src' for source packages. 9 | #[derive(Debug, PartialEq, Eq)] 10 | pub enum SourcesEntry { 11 | /// Binary package repository entry (deb line). 12 | Deb { 13 | /// Repository URI 14 | uri: String, 15 | /// Distribution name (e.g., "stable", "bullseye") 16 | dist: String, 17 | /// Component names (e.g., "main", "contrib", "non-free") 18 | comps: Vec, 19 | }, 20 | /// Source package repository entry (deb-src line). 21 | DebSrc { 22 | /// Repository URI 23 | uri: String, 24 | /// Distribution name (e.g., "stable", "bullseye") 25 | dist: String, 26 | /// Component names (e.g., "main", "contrib", "non-free") 27 | comps: Vec, 28 | }, 29 | } 30 | 31 | /// Parse a line from a sources.list file into a SourcesEntry. 32 | /// 33 | /// # Arguments 34 | /// * `line` - Line from sources.list to parse 35 | /// 36 | /// # Returns 37 | /// Some(SourcesEntry) if the line is a valid deb or deb-src line, 38 | /// None otherwise (e.g., comments, blank lines, invalid syntax) 39 | pub fn parse_sources_list_entry(line: &str) -> Option { 40 | let parts = line.split_whitespace().collect::>(); 41 | if parts.len() < 3 { 42 | return None; 43 | } 44 | let uri = parts[1]; 45 | let dist = parts[2]; 46 | let comps = parts[3..].iter().map(|x| x.to_string()).collect::>(); 47 | if parts[0] == "deb" { 48 | return Some(SourcesEntry::Deb { 49 | uri: uri.to_string(), 50 | dist: dist.to_string(), 51 | comps, 52 | }); 53 | } 54 | if parts[0] == "deb-src" { 55 | return Some(SourcesEntry::DebSrc { 56 | uri: uri.to_string(), 57 | dist: dist.to_string(), 58 | comps, 59 | }); 60 | } 61 | None 62 | } 63 | 64 | /// Representation of a Debian APT sources.list file. 65 | /// 66 | /// This struct holds a collection of SourcesEntry objects, representing 67 | /// the contents of one or more sources.list files. 68 | pub struct SourcesList { 69 | /// List of sources entries 70 | list: Vec, 71 | } 72 | 73 | impl SourcesList { 74 | /// Create an empty sources list. 75 | /// 76 | /// # Returns 77 | /// A new SourcesList with no entries 78 | pub fn empty() -> SourcesList { 79 | SourcesList { list: vec![] } 80 | } 81 | 82 | /// Get an iterator over the entries in this sources list. 83 | /// 84 | /// # Returns 85 | /// An iterator over references to SourcesEntry objects 86 | pub fn iter(&self) -> std::slice::Iter { 87 | self.list.iter() 88 | } 89 | 90 | /// Load sources entries from a file. 91 | /// 92 | /// # Arguments 93 | /// * `path` - Path to the sources.list file to load 94 | pub fn load(&mut self, path: &Path) { 95 | let f = File::open(path).unwrap(); 96 | for line in BufReader::new(f).lines() { 97 | let line = line.unwrap(); 98 | if let Some(entry) = parse_sources_list_entry(&line) { 99 | self.list.push(entry); 100 | } 101 | } 102 | } 103 | 104 | /// Create a SourcesList from an APT directory. 105 | /// 106 | /// This loads both the main sources.list file and any additional files 107 | /// in the sources.list.d directory. 108 | /// 109 | /// # Arguments 110 | /// * `apt_dir` - Path to the APT configuration directory (usually /etc/apt) 111 | /// 112 | /// # Returns 113 | /// A new SourcesList containing entries from all sources files 114 | pub fn from_apt_dir(apt_dir: &Path) -> SourcesList { 115 | let mut sl = SourcesList::empty(); 116 | sl.load(&apt_dir.join("sources.list")); 117 | for entry in apt_dir.read_dir().unwrap() { 118 | let entry = entry.unwrap(); 119 | if entry.file_type().unwrap().is_file() { 120 | let path = entry.path(); 121 | sl.load(&path); 122 | } 123 | } 124 | sl 125 | } 126 | } 127 | 128 | impl Default for SourcesList { 129 | fn default() -> Self { 130 | Self::from_apt_dir(Path::new("/etc/apt")) 131 | } 132 | } 133 | 134 | #[cfg(test)] 135 | mod tests { 136 | #[test] 137 | fn test_parse_sources_list_entry() { 138 | use super::parse_sources_list_entry; 139 | use super::SourcesEntry; 140 | assert_eq!( 141 | parse_sources_list_entry( 142 | "deb http://archive.ubuntu.com/ubuntu/ bionic main restricted" 143 | ), 144 | Some(SourcesEntry::Deb { 145 | uri: "http://archive.ubuntu.com/ubuntu/".to_string(), 146 | dist: "bionic".to_string(), 147 | comps: vec!["main".to_string(), "restricted".to_string()] 148 | }) 149 | ); 150 | assert_eq!( 151 | parse_sources_list_entry( 152 | "deb-src http://archive.ubuntu.com/ubuntu/ bionic main restricted" 153 | ), 154 | Some(SourcesEntry::DebSrc { 155 | uri: "http://archive.ubuntu.com/ubuntu/".to_string(), 156 | dist: "bionic".to_string(), 157 | comps: vec!["main".to_string(), "restricted".to_string()] 158 | }) 159 | ); 160 | } 161 | 162 | #[test] 163 | fn test_sources_list() { 164 | let td = tempfile::tempdir().unwrap(); 165 | let path = td.path().join("sources.list"); 166 | std::fs::write( 167 | &path, 168 | "deb http://archive.ubuntu.com/ubuntu/ bionic main restricted\n", 169 | ) 170 | .unwrap(); 171 | let mut sl = super::SourcesList::empty(); 172 | sl.load(&path); 173 | assert_eq!(sl.list.len(), 1); 174 | assert_eq!( 175 | sl.list[0], 176 | super::SourcesEntry::Deb { 177 | uri: "http://archive.ubuntu.com/ubuntu/".to_string(), 178 | dist: "bionic".to_string(), 179 | comps: vec!["main".to_string(), "restricted".to_string()] 180 | } 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/debian/udd.rs: -------------------------------------------------------------------------------- 1 | use crate::dependencies::debian::DebianDependency; 2 | use crate::dependencies::debian::TieBreaker; 3 | use sqlx::{Error, PgPool}; 4 | use tokio::runtime::Runtime; 5 | 6 | /// Connection to the Ultimate Debian Database (UDD). 7 | /// 8 | /// UDD is a central Debian database that combines data from various 9 | /// Debian sources, such as the archive, the BTS, popcon, etc. 10 | pub struct UDD { 11 | /// Database connection pool 12 | pool: PgPool, 13 | } 14 | 15 | impl UDD { 16 | // Function to create a new instance of UDD with a database connection 17 | /// Connect to the UDD database. 18 | /// 19 | /// # Returns 20 | /// A new UDD instance connected to the database, or an error if the connection fails 21 | pub async fn connect() -> Result { 22 | let pool = 23 | PgPool::connect("postgres://udd-mirror:udd-mirror@udd-mirror.debian.net:5432/udd") 24 | .await 25 | .unwrap(); 26 | Ok(UDD { pool }) 27 | } 28 | } 29 | 30 | /// Find the most popular package from a list of dependencies according to popcon. 31 | /// 32 | /// # Arguments 33 | /// * `reqs` - List of Debian dependencies to choose from 34 | /// 35 | /// # Returns 36 | /// The name of the most popular package, or None if no package is found in popcon 37 | async fn get_most_popular(reqs: &[&DebianDependency]) -> Result, Error> { 38 | let udd = UDD::connect().await.unwrap(); 39 | let names = reqs 40 | .iter() 41 | .flat_map(|req| req.package_names()) 42 | .collect::>(); 43 | 44 | let (max_popcon_name,): (Option,) = sqlx::query_as( 45 | "SELECT package FROM popcon WHERE package IN $1 ORDER BY insts DESC LIMIT 1", 46 | ) 47 | .bind(names) 48 | .fetch_one(&udd.pool) 49 | .await 50 | .unwrap(); 51 | 52 | Ok(max_popcon_name) 53 | } 54 | 55 | /// Tie-breaker that selects dependencies based on popcon popularity. 56 | /// 57 | /// This tie-breaker uses the Debian Popularity Contest (popcon) statistics 58 | /// to determine which package is most commonly installed among Debian users. 59 | pub struct PopconTieBreaker; 60 | 61 | impl TieBreaker for PopconTieBreaker { 62 | fn break_tie<'a>(&self, reqs: &[&'a DebianDependency]) -> Option<&'a DebianDependency> { 63 | // TODO(jelmer): Pick package based on what appears most commonly in 64 | // build-depends{-indep,-arch} 65 | let rt = Runtime::new().unwrap(); 66 | let package = rt.block_on(get_most_popular(reqs)).unwrap(); 67 | if package.is_none() { 68 | log::info!("No relevant popcon information found, not ranking by popcon"); 69 | return None; 70 | } 71 | let package = package.unwrap(); 72 | let winner = reqs 73 | .into_iter() 74 | .find(|req| req.package_names().contains(&package.to_string())); 75 | 76 | if winner.is_none() { 77 | log::info!("No relevant popcon information found, not ranking by popcon"); 78 | } 79 | 80 | winner.copied() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/debian/upstream_deps.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{BuildSystem, DependencyCategory}; 2 | use crate::dependencies::debian::DebianDependency; 3 | use crate::installer::Error as InstallerError; 4 | use crate::session::Session; 5 | 6 | /// Get the project-wide dependencies for a project. 7 | /// 8 | /// This function will return a tuple of two vectors of `DebianDependency` objects. The first 9 | /// vector will contain the build dependencies, and the second vector will contain the test 10 | /// dependencies. 11 | /// 12 | /// # Arguments 13 | /// * `session` - The session to use for the operation. 14 | /// * `buildsystem` - The build system to use for the operation. 15 | pub fn get_project_wide_deps( 16 | session: &dyn Session, 17 | buildsystem: &dyn BuildSystem, 18 | ) -> (Vec, Vec) { 19 | let mut build_deps = vec![]; 20 | let mut test_deps = vec![]; 21 | 22 | let apt = crate::debian::apt::AptManager::new(session, None); 23 | 24 | let apt_installer = crate::debian::apt::AptInstaller::new(apt); 25 | 26 | let scope = crate::installer::InstallationScope::Global; 27 | 28 | let build_fixers = [ 29 | Box::new(crate::fixers::InstallFixer::new(&apt_installer, scope)) 30 | as Box>, 31 | ]; 32 | 33 | let apt = crate::debian::apt::AptManager::new(session, None); 34 | let tie_breakers = vec![ 35 | Box::new(crate::debian::build_deps::BuildDependencyTieBreaker::from_session(session)) 36 | as Box, 37 | #[cfg(feature = "udd")] 38 | { 39 | Box::new(crate::debian::udd::PopconTieBreaker) 40 | as Box 41 | }, 42 | ]; 43 | match buildsystem.get_declared_dependencies( 44 | session, 45 | Some( 46 | build_fixers 47 | .iter() 48 | .map(|x| x.as_ref()) 49 | .collect::>() 50 | .as_slice(), 51 | ), 52 | ) { 53 | Err(e) => { 54 | log::error!("Unable to obtain declared dependencies: {}", e); 55 | } 56 | Ok(upstream_deps) => { 57 | for (kind, dep) in upstream_deps { 58 | let apt_dep = crate::debian::apt::dependency_to_deb_dependency( 59 | &apt, 60 | dep.as_ref(), 61 | tie_breakers.as_slice(), 62 | ) 63 | .unwrap(); 64 | if apt_dep.is_none() { 65 | log::warn!( 66 | "Unable to map upstream requirement {:?} (kind {}) to a Debian package", 67 | dep, 68 | kind, 69 | ); 70 | continue; 71 | } 72 | let apt_dep = apt_dep.unwrap(); 73 | log::debug!("Mapped {:?} (kind: {}) to {:?}", dep, kind, apt_dep); 74 | if [DependencyCategory::Universal, DependencyCategory::Build].contains(&kind) { 75 | build_deps.push(apt_dep.clone()); 76 | } 77 | if [DependencyCategory::Universal, DependencyCategory::Test].contains(&kind) { 78 | test_deps.push(apt_dep.clone()); 79 | } 80 | } 81 | } 82 | } 83 | (build_deps, test_deps) 84 | } 85 | -------------------------------------------------------------------------------- /src/dependencies/autoconf.rs: -------------------------------------------------------------------------------- 1 | use crate::dependency::Dependency; 2 | use crate::session::Session; 3 | use serde::{Deserialize, Serialize}; 4 | use std::io::BufRead; 5 | 6 | /// Dependency on an Autoconf macro. 7 | /// 8 | /// This represents a dependency on a specific Autoconf macro that can be 9 | /// used in configure.ac files. 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct AutoconfMacroDependency { 12 | /// Name of the Autoconf macro 13 | macro_name: String, 14 | } 15 | 16 | impl AutoconfMacroDependency { 17 | /// Create a new AutoconfMacroDependency. 18 | /// 19 | /// # Arguments 20 | /// * `macro_name` - Name of the Autoconf macro 21 | /// 22 | /// # Returns 23 | /// A new AutoconfMacroDependency instance 24 | pub fn new(macro_name: &str) -> Self { 25 | Self { 26 | macro_name: macro_name.to_string(), 27 | } 28 | } 29 | } 30 | 31 | impl Dependency for AutoconfMacroDependency { 32 | /// Returns the family name for this dependency type. 33 | /// 34 | /// # Returns 35 | /// The string "autoconf-macro" 36 | fn family(&self) -> &'static str { 37 | "autoconf-macro" 38 | } 39 | 40 | /// Checks if the Autoconf macro is present in the system. 41 | /// 42 | /// # Arguments 43 | /// * `_session` - The session in which to check 44 | /// 45 | /// # Returns 46 | /// This method is not implemented yet and will panic if called 47 | fn present(&self, _session: &dyn Session) -> bool { 48 | todo!() 49 | } 50 | 51 | /// Checks if the Autoconf macro is present in the project. 52 | /// 53 | /// # Arguments 54 | /// * `_session` - The session in which to check 55 | /// 56 | /// # Returns 57 | /// This method is not implemented yet and will panic if called 58 | fn project_present(&self, _session: &dyn Session) -> bool { 59 | todo!() 60 | } 61 | 62 | /// Returns this dependency as a trait object. 63 | /// 64 | /// # Returns 65 | /// Reference to this object as a trait object 66 | fn as_any(&self) -> &dyn std::any::Any { 67 | self 68 | } 69 | } 70 | 71 | /// Create a regular expression to find a macro definition in M4 files. 72 | /// 73 | /// This function generates a regex pattern that can match various ways a macro 74 | /// might be defined in M4 files (via AC_DEFUN, AU_ALIAS, or m4_copy). 75 | /// 76 | /// # Arguments 77 | /// * `macro` - Name of the Autoconf macro to search for 78 | /// 79 | /// # Returns 80 | /// Regular expression string pattern for finding the macro definition 81 | fn m4_macro_regex(r#macro: &str) -> String { 82 | let defun_prefix = regex::escape(format!("AC_DEFUN([{}],", r#macro).as_str()); 83 | let au_alias_prefix = regex::escape(format!("AU_ALIAS([{}],", r#macro).as_str()); 84 | let m4_copy = format!(r"m4_copy\(.*,\s*\[{}\]\)", regex::escape(r#macro)); 85 | [ 86 | "(", 87 | &defun_prefix, 88 | "|", 89 | &au_alias_prefix, 90 | "|", 91 | &m4_copy, 92 | ")", 93 | ] 94 | .concat() 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | 101 | #[test] 102 | fn test_autoconf_macro_dependency_new() { 103 | let dependency = AutoconfMacroDependency::new("PKG_CHECK_MODULES"); 104 | assert_eq!(dependency.macro_name, "PKG_CHECK_MODULES"); 105 | } 106 | 107 | #[test] 108 | fn test_autoconf_macro_dependency_family() { 109 | let dependency = AutoconfMacroDependency::new("PKG_CHECK_MODULES"); 110 | assert_eq!(dependency.family(), "autoconf-macro"); 111 | } 112 | 113 | #[test] 114 | fn test_m4_macro_regex() { 115 | let regex = m4_macro_regex("PKG_CHECK_MODULES"); 116 | 117 | // Test AC_DEFUN matching 118 | assert!(regex::Regex::new(®ex) 119 | .unwrap() 120 | .is_match("AC_DEFUN([PKG_CHECK_MODULES],")); 121 | 122 | // Test AU_ALIAS matching 123 | assert!(regex::Regex::new(®ex) 124 | .unwrap() 125 | .is_match("AU_ALIAS([PKG_CHECK_MODULES],")); 126 | 127 | // Test m4_copy matching 128 | assert!(regex::Regex::new(®ex) 129 | .unwrap() 130 | .is_match("m4_copy([SOME_MACRO], [PKG_CHECK_MODULES])")); 131 | 132 | // Test negative case 133 | assert!(!regex::Regex::new(®ex) 134 | .unwrap() 135 | .is_match("PKG_CHECK_MODULES")); 136 | } 137 | } 138 | 139 | #[cfg(feature = "debian")] 140 | /// Find a local M4 macro file that contains the definition of a given macro. 141 | /// 142 | /// Searches in `/usr/share/aclocal` for files containing the definition 143 | /// of the specified macro. 144 | /// 145 | /// # Arguments 146 | /// * `macro` - Name of the Autoconf macro to search for 147 | /// 148 | /// # Returns 149 | /// Path to the M4 file containing the macro definition, or None if not found 150 | fn find_local_m4_macro(r#macro: &str) -> Option { 151 | // TODO(jelmer): Query some external service that can search all binary packages? 152 | let p = regex::Regex::new(&m4_macro_regex(r#macro)).unwrap(); 153 | for entry in std::fs::read_dir("/usr/share/aclocal").unwrap() { 154 | let entry = entry.unwrap(); 155 | if !entry.metadata().unwrap().is_file() { 156 | continue; 157 | } 158 | let f = std::fs::File::open(entry.path()).unwrap(); 159 | let reader = std::io::BufReader::new(f); 160 | for line in reader.lines() { 161 | if p.find(line.unwrap().as_str()).is_some() { 162 | return Some(entry.path().to_str().unwrap().to_string()); 163 | } 164 | } 165 | } 166 | None 167 | } 168 | 169 | #[cfg(feature = "debian")] 170 | impl crate::dependencies::debian::IntoDebianDependency for AutoconfMacroDependency { 171 | /// Convert this dependency to a list of Debian package dependencies. 172 | /// 173 | /// Attempts to find the Debian packages that provide the Autoconf macro by 174 | /// searching for M4 files in standard locations. 175 | /// 176 | /// # Arguments 177 | /// * `apt` - The APT package manager to use for queries 178 | /// 179 | /// # Returns 180 | /// A list of Debian package dependencies if found, or None if not found 181 | fn try_into_debian_dependency( 182 | &self, 183 | apt: &crate::debian::apt::AptManager, 184 | ) -> std::option::Option> { 185 | let path = find_local_m4_macro(&self.macro_name); 186 | if path.is_none() { 187 | log::info!("No local m4 file found defining {}", self.macro_name); 188 | return None; 189 | } 190 | Some( 191 | apt.get_packages_for_paths(vec![path.as_ref().unwrap()], false, false) 192 | .unwrap() 193 | .iter() 194 | .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) 195 | .collect(), 196 | ) 197 | } 198 | } 199 | 200 | impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingAutoconfMacro { 201 | /// Convert a MissingAutoconfMacro problem to a Dependency. 202 | /// 203 | /// # Returns 204 | /// An AutoconfMacroDependency boxed as a Dependency trait object 205 | fn to_dependency(&self) -> Option> { 206 | Some(Box::new(AutoconfMacroDependency::new(&self.r#macro))) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/dependencies/latex.rs: -------------------------------------------------------------------------------- 1 | use crate::analyze::AnalyzedError; 2 | use crate::dependency::Dependency; 3 | use crate::installer::{Error, Explanation, InstallationScope, Installer}; 4 | use crate::session::Session; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | /// A dependency on a LaTeX package 9 | pub struct LatexPackageDependency { 10 | /// The name of the LaTeX package 11 | pub package: String, 12 | } 13 | 14 | impl LatexPackageDependency { 15 | /// Creates a new `LatexPackageDependency` instance 16 | pub fn new(package: &str) -> Self { 17 | Self { 18 | package: package.to_string(), 19 | } 20 | } 21 | } 22 | 23 | impl Dependency for LatexPackageDependency { 24 | fn family(&self) -> &'static str { 25 | "latex-package" 26 | } 27 | 28 | fn present(&self, _session: &dyn Session) -> bool { 29 | todo!() 30 | } 31 | 32 | fn project_present(&self, _session: &dyn Session) -> bool { 33 | todo!() 34 | } 35 | 36 | fn as_any(&self) -> &dyn std::any::Any { 37 | self 38 | } 39 | } 40 | 41 | impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingLatexFile { 42 | fn to_dependency(&self) -> Option> { 43 | if let Some(filename) = self.0.strip_suffix(".sty") { 44 | Some(Box::new(LatexPackageDependency::new(filename))) 45 | } else { 46 | None 47 | } 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | use crate::buildlog::ToDependency; 55 | use std::any::Any; 56 | 57 | #[test] 58 | fn test_latex_package_dependency_new() { 59 | let dependency = LatexPackageDependency::new("graphicx"); 60 | assert_eq!(dependency.package, "graphicx"); 61 | } 62 | 63 | #[test] 64 | fn test_latex_package_dependency_family() { 65 | let dependency = LatexPackageDependency::new("graphicx"); 66 | assert_eq!(dependency.family(), "latex-package"); 67 | } 68 | 69 | #[test] 70 | fn test_latex_package_dependency_as_any() { 71 | let dependency = LatexPackageDependency::new("graphicx"); 72 | let any_dep: &dyn Any = dependency.as_any(); 73 | assert!(any_dep.downcast_ref::().is_some()); 74 | } 75 | 76 | #[test] 77 | fn test_missing_latex_file_to_dependency() { 78 | let problem = 79 | buildlog_consultant::problems::common::MissingLatexFile("graphicx.sty".to_string()); 80 | let dependency = problem.to_dependency(); 81 | assert!(dependency.is_some()); 82 | let dep = dependency.unwrap(); 83 | assert_eq!(dep.family(), "latex-package"); 84 | let latex_dep = dep 85 | .as_any() 86 | .downcast_ref::() 87 | .unwrap(); 88 | assert_eq!(latex_dep.package, "graphicx"); 89 | } 90 | 91 | #[test] 92 | fn test_missing_latex_file_non_sty_to_dependency() { 93 | // Non .sty files should return None 94 | let problem = 95 | buildlog_consultant::problems::common::MissingLatexFile("graphicx.cls".to_string()); 96 | let dependency = problem.to_dependency(); 97 | assert!(dependency.is_none()); 98 | } 99 | } 100 | 101 | /// A resolver for LaTeX package dependencies using tlmgr 102 | pub struct TlmgrResolver<'a> { 103 | session: &'a dyn Session, 104 | repository: String, 105 | } 106 | 107 | impl<'a> TlmgrResolver<'a> { 108 | /// Creates a new `TlmgrResolver` instance 109 | pub fn new(session: &'a dyn Session, repository: &str) -> Self { 110 | Self { 111 | session, 112 | repository: repository.to_string(), 113 | } 114 | } 115 | 116 | fn cmd( 117 | &self, 118 | reqs: &[&LatexPackageDependency], 119 | scope: InstallationScope, 120 | ) -> Result, Error> { 121 | let mut ret = vec![ 122 | "tlmgr".to_string(), 123 | format!("--repository={}", self.repository), 124 | "install".to_string(), 125 | ]; 126 | match scope { 127 | InstallationScope::User => { 128 | ret.push("--usermode".to_string()); 129 | } 130 | InstallationScope::Global => {} 131 | InstallationScope::Vendor => { 132 | return Err(Error::UnsupportedScope(scope)); 133 | } 134 | } 135 | ret.extend(reqs.iter().map(|req| req.package.clone())); 136 | Ok(ret) 137 | } 138 | } 139 | 140 | impl<'a> Installer for TlmgrResolver<'a> { 141 | fn explain( 142 | &self, 143 | dep: &dyn Dependency, 144 | scope: InstallationScope, 145 | ) -> Result { 146 | let dep = dep 147 | .as_any() 148 | .downcast_ref::() 149 | .ok_or(Error::UnknownDependencyFamily)?; 150 | let cmd = self.cmd(&[dep], scope)?; 151 | Ok(Explanation { 152 | message: format!("Install the LaTeX package {}", dep.package), 153 | command: Some(cmd), 154 | }) 155 | } 156 | 157 | fn install(&self, dep: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { 158 | let dep = dep 159 | .as_any() 160 | .downcast_ref::() 161 | .ok_or(Error::UnknownDependencyFamily)?; 162 | let cmd = self.cmd(&[dep], scope)?; 163 | log::info!("tlmgr: running {:?}", cmd); 164 | 165 | match self 166 | .session 167 | .command(cmd.iter().map(|x| x.as_str()).collect()) 168 | .run_detecting_problems() 169 | { 170 | Ok(_) => Ok(()), 171 | Err(AnalyzedError::Unidentified { 172 | lines, 173 | retcode, 174 | secondary, 175 | }) => { 176 | if lines.contains( 177 | &"tlmgr: user mode not initialized, please read the documentation!".to_string(), 178 | ) { 179 | self.session 180 | .command(vec!["tlmgr", "init-usertree"]) 181 | .check_call()?; 182 | Ok(()) 183 | } else { 184 | Err(Error::AnalyzedError(AnalyzedError::Unidentified { 185 | retcode, 186 | lines, 187 | secondary, 188 | })) 189 | } 190 | } 191 | Err(e) => Err(e.into()), 192 | } 193 | } 194 | } 195 | 196 | /// Creates a new `TlmgrResolver` instance for the CTAN repository 197 | pub fn ctan<'a>(session: &'a dyn Session) -> TlmgrResolver<'a> { 198 | TlmgrResolver::new(session, "ctan") 199 | } 200 | -------------------------------------------------------------------------------- /src/dependencies/octave.rs: -------------------------------------------------------------------------------- 1 | use crate::dependency::Dependency; 2 | use crate::installer::{Error, Explanation, InstallationScope, Installer}; 3 | use crate::session::Session; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | /// OctavePackageDependency represents a dependency on an Octave package. 8 | pub struct OctavePackageDependency { 9 | package: String, 10 | minimum_version: Option, 11 | } 12 | 13 | impl OctavePackageDependency { 14 | /// Creates a new OctavePackageDependency with a specified minimum version. 15 | pub fn new(package: &str, minimum_version: Option<&str>) -> Self { 16 | Self { 17 | package: package.to_string(), 18 | minimum_version: minimum_version.map(|s| s.to_string()), 19 | } 20 | } 21 | 22 | /// Creates a new OctavePackageDependency with no minimum version. 23 | pub fn simple(package: &str) -> Self { 24 | Self { 25 | package: package.to_string(), 26 | minimum_version: None, 27 | } 28 | } 29 | } 30 | 31 | impl std::str::FromStr for OctavePackageDependency { 32 | type Err = String; 33 | 34 | fn from_str(s: &str) -> Result { 35 | if let Some((_, name, min_version)) = lazy_regex::regex_captures!("(.*) \\(>= (.*)\\)", s) { 36 | Ok(Self::new(name, Some(min_version))) 37 | } else if !s.contains(" ") { 38 | Ok(Self::simple(s)) 39 | } else { 40 | Err(format!("Failed to parse Octave package dependency: {}", s)) 41 | } 42 | } 43 | } 44 | 45 | impl Dependency for OctavePackageDependency { 46 | fn family(&self) -> &'static str { 47 | "octave-package" 48 | } 49 | 50 | fn present(&self, session: &dyn Session) -> bool { 51 | session 52 | .command(vec![ 53 | "octave", 54 | "--eval", 55 | &format!("pkg load {}", self.package), 56 | ]) 57 | .stdout(std::process::Stdio::null()) 58 | .stderr(std::process::Stdio::null()) 59 | .run() 60 | .unwrap() 61 | .success() 62 | } 63 | 64 | fn project_present(&self, _session: &dyn Session) -> bool { 65 | todo!() 66 | } 67 | 68 | fn as_any(&self) -> &dyn std::any::Any { 69 | self 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | use std::any::Any; 77 | 78 | #[test] 79 | fn test_octave_package_dependency_new() { 80 | let dependency = OctavePackageDependency::new("signal", Some("1.0.0")); 81 | assert_eq!(dependency.package, "signal"); 82 | assert_eq!(dependency.minimum_version, Some("1.0.0".to_string())); 83 | } 84 | 85 | #[test] 86 | fn test_octave_package_dependency_simple() { 87 | let dependency = OctavePackageDependency::simple("signal"); 88 | assert_eq!(dependency.package, "signal"); 89 | assert_eq!(dependency.minimum_version, None); 90 | } 91 | 92 | #[test] 93 | fn test_octave_package_dependency_family() { 94 | let dependency = OctavePackageDependency::simple("signal"); 95 | assert_eq!(dependency.family(), "octave-package"); 96 | } 97 | 98 | #[test] 99 | fn test_octave_package_dependency_as_any() { 100 | let dependency = OctavePackageDependency::simple("signal"); 101 | let any_dep: &dyn Any = dependency.as_any(); 102 | assert!(any_dep.downcast_ref::().is_some()); 103 | } 104 | 105 | #[test] 106 | fn test_octave_package_dependency_from_str_simple() { 107 | let dependency: OctavePackageDependency = "signal".parse().unwrap(); 108 | assert_eq!(dependency.package, "signal"); 109 | assert_eq!(dependency.minimum_version, None); 110 | } 111 | 112 | #[test] 113 | fn test_octave_package_dependency_from_str_with_version() { 114 | let dependency: OctavePackageDependency = "signal (>= 1.0.0)".parse().unwrap(); 115 | assert_eq!(dependency.package, "signal"); 116 | assert_eq!(dependency.minimum_version, Some("1.0.0".to_string())); 117 | } 118 | 119 | #[test] 120 | fn test_octave_package_dependency_from_str_invalid() { 121 | let result: Result = "signal with bad format".parse(); 122 | assert!(result.is_err()); 123 | } 124 | } 125 | 126 | /// OctaveForgeResolver is an installer for Octave packages using the Octave Forge repository. 127 | pub struct OctaveForgeResolver<'a> { 128 | session: &'a dyn Session, 129 | } 130 | 131 | impl<'a> OctaveForgeResolver<'a> { 132 | /// Creates a new OctaveForgeResolver. 133 | pub fn new(session: &'a dyn Session) -> Self { 134 | Self { session } 135 | } 136 | 137 | fn cmd( 138 | &self, 139 | dependency: &OctavePackageDependency, 140 | scope: InstallationScope, 141 | ) -> Result, Error> { 142 | match scope { 143 | InstallationScope::Global => Ok(vec![ 144 | "octave-cli".to_string(), 145 | "--eval".to_string(), 146 | format!("pkg install -forge -global {}", dependency.package), 147 | ]), 148 | InstallationScope::User => Ok(vec![ 149 | "octave-cli".to_string(), 150 | "--eval".to_string(), 151 | format!("pkg install -forge -local {}", dependency.package), 152 | ]), 153 | InstallationScope::Vendor => Err(Error::UnsupportedScope(scope)), 154 | } 155 | } 156 | } 157 | 158 | impl<'a> Installer for OctaveForgeResolver<'a> { 159 | fn explain( 160 | &self, 161 | dependency: &dyn Dependency, 162 | scope: InstallationScope, 163 | ) -> Result { 164 | let dependency = dependency 165 | .as_any() 166 | .downcast_ref::() 167 | .unwrap(); 168 | let cmd = self.cmd(dependency, scope)?; 169 | Ok(Explanation { 170 | command: Some(cmd), 171 | message: format!("Install Octave package {}", dependency.package), 172 | }) 173 | } 174 | 175 | fn install(&self, dependency: &dyn Dependency, scope: InstallationScope) -> Result<(), Error> { 176 | let dependency = dependency 177 | .as_any() 178 | .downcast_ref::() 179 | .ok_or(Error::UnknownDependencyFamily)?; 180 | let cmd = self.cmd(dependency, scope)?; 181 | log::info!("Octave: installing {}", dependency.package); 182 | self.session 183 | .command(cmd.iter().map(|x| x.as_str()).collect()) 184 | .run_detecting_problems()?; 185 | Ok(()) 186 | } 187 | } 188 | 189 | #[cfg(feature = "debian")] 190 | impl crate::dependencies::debian::IntoDebianDependency for OctavePackageDependency { 191 | fn try_into_debian_dependency( 192 | &self, 193 | _apt: &crate::debian::apt::AptManager, 194 | ) -> std::option::Option> { 195 | if let Some(minimum_version) = &self.minimum_version { 196 | Some(vec![ 197 | crate::dependencies::debian::DebianDependency::new_with_min_version( 198 | &format!("octave-{}", &self.package), 199 | &minimum_version.parse().unwrap(), 200 | ), 201 | ]) 202 | } else { 203 | Some(vec![crate::dependencies::debian::DebianDependency::new( 204 | &format!("octave-{}", &self.package), 205 | )]) 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/dependencies/xml.rs: -------------------------------------------------------------------------------- 1 | //! Support for XML entity dependencies. 2 | //! 3 | //! This module provides functionality for working with XML entity dependencies, 4 | //! including checking if entities are defined in the local XML catalog and 5 | //! mapping between URLs and filesystem paths. 6 | 7 | use crate::dependencies::Dependency; 8 | use crate::session::Session; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | /// A dependency on an XML entity, such as a DocBook DTD. 13 | /// 14 | /// This represents a dependency on an XML entity, which is typically resolved 15 | /// through an XML catalog. 16 | pub struct XmlEntityDependency { 17 | url: String, 18 | } 19 | 20 | impl XmlEntityDependency { 21 | /// Create a new XML entity dependency with the specified URL. 22 | /// 23 | /// # Arguments 24 | /// * `url` - The URL of the XML entity 25 | /// 26 | /// # Returns 27 | /// A new XmlEntityDependency 28 | pub fn new(url: &str) -> Self { 29 | Self { 30 | url: url.to_string(), 31 | } 32 | } 33 | } 34 | 35 | impl Dependency for XmlEntityDependency { 36 | fn family(&self) -> &'static str { 37 | "xml-entity" 38 | } 39 | 40 | fn present(&self, session: &dyn Session) -> bool { 41 | // Check if the entity is defined in the local XML catalog 42 | session 43 | .command(vec!["xmlcatalog", "--noout", "--resolve", &self.url]) 44 | .stdout(std::process::Stdio::null()) 45 | .stderr(std::process::Stdio::null()) 46 | .run() 47 | .unwrap() 48 | .success() 49 | } 50 | 51 | fn project_present(&self, _session: &dyn Session) -> bool { 52 | todo!() 53 | } 54 | fn as_any(&self) -> &dyn std::any::Any { 55 | self 56 | } 57 | } 58 | 59 | /// Mapping between XML entity URLs and their filesystem locations. 60 | /// 61 | /// This constant maps from entity URLs to their corresponding filesystem paths, 62 | /// which is used to locate entities when resolving dependencies. 63 | pub const XML_ENTITY_URL_MAP: &[(&str, &str)] = &[( 64 | "http://www.oasis-open.org/docbook/xml/", 65 | "/usr/share/xml/docbook/schema/dtd/", 66 | )]; 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::*; 71 | use std::any::Any; 72 | 73 | #[test] 74 | fn test_xml_entity_dependency_new() { 75 | let url = "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"; 76 | let dependency = XmlEntityDependency::new(url); 77 | assert_eq!(dependency.url, url); 78 | } 79 | 80 | #[test] 81 | fn test_xml_entity_dependency_family() { 82 | let dependency = XmlEntityDependency::new("http://www.example.com/entity"); 83 | assert_eq!(dependency.family(), "xml-entity"); 84 | } 85 | 86 | #[test] 87 | fn test_xml_entity_dependency_as_any() { 88 | let dependency = XmlEntityDependency::new("http://www.example.com/entity"); 89 | let any_dep: &dyn Any = dependency.as_any(); 90 | assert!(any_dep.downcast_ref::().is_some()); 91 | } 92 | 93 | #[test] 94 | fn test_xml_entity_url_map() { 95 | assert!(XML_ENTITY_URL_MAP 96 | .iter() 97 | .any(|(url, _)| *url == "http://www.oasis-open.org/docbook/xml/")); 98 | 99 | // Test that the URL map can be used to transform URLs 100 | let input_url = "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"; 101 | let expected_path = "/usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd"; 102 | 103 | let transformed = XML_ENTITY_URL_MAP.iter().find_map(|(url, path)| { 104 | input_url 105 | .strip_prefix(url) 106 | .map(|rest| format!("{}{}", path, rest)) 107 | }); 108 | 109 | assert_eq!(transformed, Some(expected_path.to_string())); 110 | } 111 | } 112 | 113 | #[cfg(feature = "debian")] 114 | impl crate::dependencies::debian::IntoDebianDependency for XmlEntityDependency { 115 | fn try_into_debian_dependency( 116 | &self, 117 | apt: &crate::debian::apt::AptManager, 118 | ) -> std::option::Option> { 119 | let path = XML_ENTITY_URL_MAP.iter().find_map(|(url, path)| { 120 | self.url 121 | .strip_prefix(url) 122 | .map(|rest| format!("{}{}", path, rest)) 123 | }); 124 | 125 | path.as_ref()?; 126 | 127 | Some( 128 | apt.get_packages_for_paths(vec![path.as_ref().unwrap()], false, false) 129 | .unwrap() 130 | .iter() 131 | .map(|p| crate::dependencies::debian::DebianDependency::simple(p.as_str())) 132 | .collect(), 133 | ) 134 | } 135 | } 136 | 137 | impl crate::buildlog::ToDependency for buildlog_consultant::problems::common::MissingXmlEntity { 138 | fn to_dependency(&self) -> Option> { 139 | Some(Box::new(XmlEntityDependency::new(&self.url))) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/dependency.rs: -------------------------------------------------------------------------------- 1 | use crate::session::Session; 2 | 3 | /// A dependency is a component that is required by a project to build or run. 4 | pub trait Dependency: std::fmt::Debug { 5 | /// Get the family of this dependency (e.g., "apt", "pip", etc.). 6 | /// 7 | /// # Returns 8 | /// A string identifying the dependency type family 9 | fn family(&self) -> &'static str; 10 | 11 | /// Check whether the dependency is present in the session. 12 | fn present(&self, session: &dyn Session) -> bool; 13 | 14 | /// Check whether the dependency is present in the project. 15 | fn project_present(&self, session: &dyn Session) -> bool; 16 | 17 | /// Convert this dependency to Any for dynamic casting. 18 | /// 19 | /// This method allows for conversion of the dependency to concrete types at runtime. 20 | /// 21 | /// # Returns 22 | /// A reference to this dependency as Any 23 | fn as_any(&self) -> &dyn std::any::Any; 24 | } 25 | -------------------------------------------------------------------------------- /src/dist.rs: -------------------------------------------------------------------------------- 1 | use crate::buildsystem::{detect_buildsystems, Error}; 2 | use crate::fix_build::{iterate_with_build_fixers, BuildFixer, InterimError}; 3 | use crate::fixers::*; 4 | use crate::installer::{ 5 | auto_installation_scope, auto_installer, Error as InstallerError, InstallationScope, 6 | }; 7 | use crate::logs::{wrap, LogManager}; 8 | use crate::session::Session; 9 | use std::ffi::OsString; 10 | use std::path::Path; 11 | 12 | /// Create a distribution package using the detected build system. 13 | /// 14 | /// # Arguments 15 | /// * `session` - The session to run commands in 16 | /// * `export_directory` - Directory to search for build systems 17 | /// * `reldir` - Relative directory to change to before building 18 | /// * `target_dir` - Directory to write distribution package to 19 | /// * `log_manager` - Log manager for capturing output 20 | /// * `version` - Optional version to use for the package 21 | /// * `quiet` - Whether to suppress output 22 | /// 23 | /// # Returns 24 | /// The filename of the created distribution package 25 | pub fn dist( 26 | session: &mut dyn Session, 27 | export_directory: &Path, 28 | reldir: &Path, 29 | target_dir: &Path, 30 | log_manager: &mut dyn LogManager, 31 | version: Option<&str>, 32 | quiet: bool, 33 | ) -> Result { 34 | session.chdir(reldir)?; 35 | 36 | if let Some(version) = version { 37 | // TODO(jelmer): Shouldn't include backend-specific code here 38 | std::env::set_var("SETUPTOOLS_SCM_PRETEND_VERSION", version); 39 | } 40 | 41 | // TODO(jelmer): use scan_buildsystems to also look in subdirectories 42 | let buildsystems = detect_buildsystems(export_directory); 43 | let scope = auto_installation_scope(session); 44 | let installer = auto_installer(session, scope, None); 45 | let mut fixers: Vec>> = vec![ 46 | Box::new(UnexpandedAutoconfMacroFixer::new( 47 | session, 48 | installer.as_ref(), 49 | )), 50 | Box::new(GnulibDirectoryFixer::new(session)), 51 | Box::new(MinimumAutoconfFixer::new(session)), 52 | Box::new(MissingGoSumEntryFixer::new(session)), 53 | Box::new(InstallFixer::new( 54 | installer.as_ref(), 55 | InstallationScope::User, 56 | )), 57 | ]; 58 | 59 | if session.is_temporary() { 60 | // Only muck about with temporary sessions 61 | fixers.extend([ 62 | Box::new(GitIdentityFixer::new(session)) as Box>, 63 | Box::new(SecretGpgKeyFixer::new(session)) as Box>, 64 | ]); 65 | } 66 | 67 | // Some things want to write to the user's home directory, e.g. pip caches in ~/.cache 68 | session.create_home()?; 69 | 70 | for buildsystem in buildsystems { 71 | return Ok(iterate_with_build_fixers( 72 | fixers 73 | .iter() 74 | .map(|x| x.as_ref()) 75 | .collect::>() 76 | .as_slice(), 77 | || -> Result<_, InterimError> { 78 | Ok(wrap(log_manager, || -> Result<_, Error> { 79 | buildsystem.dist(session, installer.as_ref(), target_dir, quiet) 80 | })?) 81 | }, 82 | None, 83 | )?); 84 | } 85 | 86 | Err(Error::NoBuildSystemDetected) 87 | } 88 | 89 | #[cfg(feature = "breezy")] 90 | // This is the function used by debianize() 91 | /// Create a dist tarball for a tree. 92 | /// 93 | /// # Arguments 94 | /// * `session` - session to run it 95 | /// * `tree` - Tree object to work in 96 | /// * `target_dir` - Directory to write tarball into 97 | /// * `include_controldir` - Whether to include the version control directory 98 | /// * `temp_subdir` - name of subdirectory in which to check out the source code; 99 | /// defaults to "package" 100 | pub fn create_dist( 101 | session: &mut dyn Session, 102 | tree: &T, 103 | target_dir: &Path, 104 | include_controldir: Option, 105 | log_manager: &mut dyn LogManager, 106 | version: Option<&str>, 107 | _subpath: &Path, 108 | temp_subdir: Option<&str>, 109 | ) -> Result { 110 | let temp_subdir = temp_subdir.unwrap_or("package"); 111 | 112 | let project = session.project_from_vcs(tree, include_controldir, Some(temp_subdir))?; 113 | 114 | dist( 115 | session, 116 | project.external_path(), 117 | project.internal_path(), 118 | target_dir, 119 | log_manager, 120 | version, 121 | false, 122 | ) 123 | } 124 | 125 | #[cfg(feature = "breezy")] 126 | #[cfg(target_os = "linux")] 127 | /// Create a dist tarball for a tree. 128 | /// 129 | /// # Arguments 130 | /// * `session` - session to run it 131 | /// * `tree` - Tree object to work in 132 | /// * `target_dir` - Directory to write tarball into 133 | /// * `include_controldir` - Whether to include the version control directory 134 | /// * `temp_subdir` - name of subdirectory in which to check out the source code; 135 | /// defaults to "package" 136 | pub fn create_dist_schroot( 137 | tree: &T, 138 | target_dir: &Path, 139 | chroot: &str, 140 | packaging_tree: Option<&dyn breezyshim::tree::Tree>, 141 | packaging_subpath: Option<&Path>, 142 | include_controldir: Option, 143 | subpath: &Path, 144 | log_manager: &mut dyn LogManager, 145 | version: Option<&str>, 146 | temp_subdir: Option<&str>, 147 | ) -> Result { 148 | // TODO(jelmer): pass in package name as part of session prefix 149 | let mut session = crate::session::schroot::SchrootSession::new(chroot, Some("ognibuild-dist"))?; 150 | #[cfg(feature = "debian")] 151 | if let (Some(packaging_tree), Some(packaging_subpath)) = (packaging_tree, packaging_subpath) { 152 | crate::debian::satisfy_build_deps(&session, packaging_tree, packaging_subpath) 153 | .map_err(|e| Error::Other(format!("Failed to satisfy build dependencies: {:?}", e)))?; 154 | } 155 | #[cfg(not(feature = "debian"))] 156 | if packaging_tree.is_some() || packaging_subpath.is_some() { 157 | log::warn!("Ignoring packaging tree and subpath as debian feature is not enabled"); 158 | } 159 | create_dist( 160 | &mut session, 161 | tree, 162 | target_dir, 163 | include_controldir, 164 | log_manager, 165 | version, 166 | subpath, 167 | temp_subdir, 168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /src/dist_catcher.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ffi::OsString; 3 | use std::path::{Path, PathBuf}; 4 | 5 | /// List of supported distribution file extensions. 6 | pub const SUPPORTED_DIST_EXTENSIONS: &[&str] = &[ 7 | ".tar.gz", 8 | ".tgz", 9 | ".tar.bz2", 10 | ".tar.xz", 11 | ".tar.lzma", 12 | ".tbz2", 13 | ".tar", 14 | ".zip", 15 | ]; 16 | 17 | /// Check if a file has a supported distribution extension. 18 | pub fn supported_dist_file(file: &Path) -> bool { 19 | SUPPORTED_DIST_EXTENSIONS 20 | .iter() 21 | .any(|&ext| file.ends_with(ext)) 22 | } 23 | 24 | /// Utility to detect and collect distribution files created by build systems. 25 | /// 26 | /// This monitors directories for new or updated distribution files that appear 27 | /// after a build process runs. 28 | pub struct DistCatcher { 29 | existing_files: Option>>, 30 | directories: Vec, 31 | files: std::sync::Mutex>, 32 | start_time: std::time::SystemTime, 33 | } 34 | 35 | impl DistCatcher { 36 | /// Create a new DistCatcher to monitor the specified directories. 37 | pub fn new(directories: Vec) -> Self { 38 | Self { 39 | directories: directories 40 | .iter() 41 | .map(|d| d.canonicalize().unwrap()) 42 | .collect(), 43 | files: std::sync::Mutex::new(Vec::new()), 44 | start_time: std::time::SystemTime::now(), 45 | existing_files: None, 46 | } 47 | } 48 | 49 | /// Create a DistCatcher with default directory locations. 50 | pub fn default(directory: &Path) -> Self { 51 | Self::new(vec![ 52 | directory.join("dist"), 53 | directory.to_path_buf(), 54 | directory.join(".."), 55 | ]) 56 | } 57 | 58 | /// Initialize the file monitoring process. 59 | /// 60 | /// Takes a snapshot of existing files to later detect new or modified files. 61 | pub fn start(&mut self) { 62 | self.existing_files = Some( 63 | self.directories 64 | .iter() 65 | .map(|d| { 66 | let mut map = HashMap::new(); 67 | for entry in d.read_dir().unwrap() { 68 | let entry = entry.unwrap(); 69 | map.insert(entry.path(), entry); 70 | } 71 | (d.clone(), map) 72 | }) 73 | .collect(), 74 | ); 75 | } 76 | 77 | /// Search for new or updated distribution files. 78 | /// 79 | /// Returns the path to a found file if any. 80 | pub fn find_files(&self) -> Option { 81 | let existing_files = self.existing_files.as_ref().unwrap(); 82 | let mut files = self.files.lock().unwrap(); 83 | for directory in &self.directories { 84 | let old_files = existing_files.get(directory).unwrap(); 85 | let mut possible_new = Vec::new(); 86 | let mut possible_updated = Vec::new(); 87 | if !directory.is_dir() { 88 | continue; 89 | } 90 | for entry in directory.read_dir().unwrap() { 91 | let entry = entry.unwrap(); 92 | if !entry.file_type().unwrap().is_file() || !supported_dist_file(&entry.path()) { 93 | continue; 94 | } 95 | let old_entry = old_files.get(&entry.path()); 96 | if old_entry.is_none() { 97 | possible_new.push(entry); 98 | continue; 99 | } 100 | if entry.metadata().unwrap().modified().unwrap() > self.start_time { 101 | possible_updated.push(entry); 102 | continue; 103 | } 104 | } 105 | if possible_new.len() == 1 { 106 | let entry = possible_new[0].path(); 107 | log::info!("Found new tarball {:?} in {:?}", entry, directory); 108 | files.push(entry.clone()); 109 | return Some(entry); 110 | } else if possible_new.len() > 1 { 111 | log::warn!( 112 | "Found multiple tarballs {:?} in {:?}", 113 | possible_new.iter().map(|e| e.path()).collect::>(), 114 | directory 115 | ); 116 | files.extend(possible_new.iter().map(|e| e.path())); 117 | return Some(possible_new[0].path()); 118 | } 119 | 120 | if possible_updated.len() == 1 { 121 | let entry = possible_updated[0].path(); 122 | log::info!("Found updated tarball {:?} in {:?}", entry, directory); 123 | files.push(entry.clone()); 124 | return Some(entry); 125 | } 126 | } 127 | None 128 | } 129 | 130 | /// Copy a single distribution file to the target directory. 131 | /// 132 | /// Returns the filename of the copied file if successful. 133 | pub fn copy_single(&self, target_dir: &Path) -> Result, std::io::Error> { 134 | for path in self.files.lock().unwrap().iter() { 135 | match std::fs::copy(path, target_dir.join(path.file_name().unwrap())) { 136 | Ok(_) => return Ok(Some(path.file_name().unwrap().into())), 137 | Err(e) => { 138 | if e.kind() == std::io::ErrorKind::AlreadyExists { 139 | continue; 140 | } 141 | return Err(e); 142 | } 143 | } 144 | } 145 | log::info!("No tarball created :("); 146 | Err(std::io::Error::new( 147 | std::io::ErrorKind::NotFound, 148 | "No tarball found", 149 | )) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Library for building packages from source code. 3 | 4 | /// Action implementations like build, clean, test, etc. 5 | pub mod actions; 6 | /// Analyze build errors and execution problems. 7 | pub mod analyze; 8 | /// Build log handling and parsing. 9 | pub mod buildlog; 10 | /// BuildSystem trait and related types. 11 | pub mod buildsystem; 12 | /// Implementations of different build systems. 13 | pub mod buildsystems; 14 | #[cfg(feature = "debian")] 15 | /// Debian-specific functionality. 16 | pub mod debian; 17 | /// Dependency resolution implementations. 18 | pub mod dependencies; 19 | /// Dependency trait and related types. 20 | pub mod dependency; 21 | /// Distribution package creation. 22 | pub mod dist; 23 | /// Utilities for catching distribution packages. 24 | pub mod dist_catcher; 25 | /// Build fixing utilities. 26 | pub mod fix_build; 27 | /// Implementations of different build fixers. 28 | pub mod fixers; 29 | /// Package installer functionality. 30 | pub mod installer; 31 | /// Logging utilities. 32 | pub mod logs; 33 | /// Output formatting and handling. 34 | pub mod output; 35 | /// Session handling for build environments. 36 | pub mod session; 37 | /// Shebang detection and processing. 38 | pub mod shebang; 39 | #[cfg(feature = "upstream")] 40 | /// Upstream package handling. 41 | pub mod upstream; 42 | #[cfg(feature = "breezy")] 43 | /// Version control system utilities. 44 | pub mod vcs; 45 | -------------------------------------------------------------------------------- /src/logs.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use std::fs; 3 | use std::fs::File; 4 | use std::io::{self, Write}; 5 | use std::os::unix::io::{AsRawFd, RawFd}; 6 | use std::path::{Path, PathBuf}; 7 | use std::process::Command; 8 | 9 | struct RedirectOutput { 10 | old_stdout: RawFd, 11 | old_stderr: RawFd, 12 | } 13 | 14 | impl RedirectOutput { 15 | fn new(to_file: &File) -> io::Result { 16 | let stdout = io::stdout(); 17 | let stderr = io::stderr(); 18 | 19 | stdout.lock().flush()?; 20 | stderr.lock().flush()?; 21 | 22 | let old_stdout = unsafe { libc::dup(libc::STDOUT_FILENO) }; 23 | let old_stderr = unsafe { libc::dup(libc::STDERR_FILENO) }; 24 | 25 | if old_stdout == -1 || old_stderr == -1 { 26 | return Err(io::Error::last_os_error()); 27 | } 28 | 29 | unsafe { 30 | libc::dup2(to_file.as_raw_fd(), libc::STDOUT_FILENO); 31 | libc::dup2(to_file.as_raw_fd(), libc::STDERR_FILENO); 32 | } 33 | 34 | Ok(RedirectOutput { 35 | old_stdout, 36 | old_stderr, 37 | }) 38 | } 39 | } 40 | 41 | impl Drop for RedirectOutput { 42 | fn drop(&mut self) { 43 | let stdout = io::stdout(); 44 | let stderr = io::stderr(); 45 | 46 | stdout.lock().flush().unwrap(); 47 | stderr.lock().flush().unwrap(); 48 | 49 | unsafe { 50 | libc::dup2(self.old_stdout, libc::STDOUT_FILENO); 51 | libc::dup2(self.old_stderr, libc::STDERR_FILENO); 52 | libc::close(self.old_stdout); 53 | libc::close(self.old_stderr); 54 | } 55 | } 56 | } 57 | 58 | struct CopyOutput { 59 | old_stdout: RawFd, 60 | old_stderr: RawFd, 61 | new_fd: Option, 62 | } 63 | 64 | impl CopyOutput { 65 | fn new(output_log: &std::path::Path, tee: bool) -> io::Result { 66 | let old_stdout = unsafe { libc::dup(libc::STDOUT_FILENO) }; 67 | let old_stderr = unsafe { libc::dup(libc::STDERR_FILENO) }; 68 | 69 | let new_fd = if tee { 70 | let process = Command::new("tee") 71 | .arg(output_log) 72 | .stdin(std::process::Stdio::piped()) 73 | .spawn()?; 74 | process.stdin.unwrap().as_raw_fd() 75 | } else { 76 | File::create(output_log)?.as_raw_fd() 77 | }; 78 | 79 | unsafe { 80 | libc::dup2(new_fd, libc::STDOUT_FILENO); 81 | libc::dup2(new_fd, libc::STDERR_FILENO); 82 | } 83 | 84 | Ok(CopyOutput { 85 | old_stdout, 86 | old_stderr, 87 | new_fd: Some(new_fd), 88 | }) 89 | } 90 | } 91 | 92 | impl Drop for CopyOutput { 93 | fn drop(&mut self) { 94 | if let Some(fd) = self.new_fd.take() { 95 | unsafe { 96 | libc::fsync(fd); 97 | libc::close(fd); 98 | } 99 | } 100 | 101 | unsafe { 102 | libc::dup2(self.old_stdout, libc::STDOUT_FILENO); 103 | libc::dup2(self.old_stderr, libc::STDERR_FILENO); 104 | libc::close(self.old_stdout); 105 | libc::close(self.old_stderr); 106 | } 107 | } 108 | } 109 | 110 | /// Rotate a log file, moving it to a new file with a timestamp. 111 | /// 112 | /// # Arguments 113 | /// * `source_path` - Path to the log file to rotate 114 | /// 115 | /// # Returns 116 | /// * `Ok(())` - If the log file was rotated successfully 117 | /// * `Err(Error)` - If rotating the log file failed 118 | pub fn rotate_logfile(source_path: &std::path::Path) -> std::io::Result<()> { 119 | if source_path.exists() { 120 | let directory_path = source_path.parent().unwrap_or_else(|| Path::new("")); 121 | let name = source_path.file_name().unwrap().to_str().unwrap(); 122 | 123 | let mut i = 1; 124 | while directory_path.join(format!("{}.{}", name, i)).exists() { 125 | i += 1; 126 | } 127 | 128 | let target_path: PathBuf = directory_path.join(format!("{}.{}", name, i)); 129 | fs::rename(source_path, &target_path)?; 130 | 131 | debug!("Storing previous build log at {}", target_path.display()); 132 | } 133 | Ok(()) 134 | } 135 | 136 | /// Mode for logging. 137 | pub enum LogMode { 138 | /// Copy output to the log file. 139 | Copy, 140 | /// Redirect output to the log file. 141 | Redirect, 142 | } 143 | 144 | /// Trait for managing log files for build operations. 145 | pub trait LogManager { 146 | /// Start logging to the log file. 147 | /// 148 | /// # Returns 149 | /// * `Ok(())` - If logging was started successfully 150 | /// * `Err(Error)` - If starting logging failed 151 | fn start(&mut self) -> std::io::Result<()>; 152 | 153 | /// Stop logging to the log file. 154 | fn stop(&mut self) {} 155 | } 156 | 157 | /// Run a function capturing its output to a log file. 158 | pub fn wrap(logs: &mut dyn LogManager, f: impl FnOnce() -> R) -> R { 159 | logs.start().unwrap(); 160 | let result = f(); 161 | std::io::stdout().flush().unwrap(); 162 | std::io::stderr().flush().unwrap(); 163 | logs.stop(); 164 | result 165 | } 166 | 167 | /// Log manager that logs to a file in a directory. 168 | pub struct DirectoryLogManager { 169 | path: PathBuf, 170 | mode: LogMode, 171 | copy_output: Option, 172 | redirect_output: Option, 173 | } 174 | 175 | impl DirectoryLogManager { 176 | /// Create a new DirectoryLogManager. 177 | /// 178 | /// # Arguments 179 | /// * `path` - Path to the log file 180 | /// * `mode` - Mode for logging 181 | /// 182 | /// # Returns 183 | /// A new DirectoryLogManager instance 184 | pub fn new(path: PathBuf, mode: LogMode) -> Self { 185 | Self { 186 | path, 187 | mode, 188 | copy_output: None, 189 | redirect_output: None, 190 | } 191 | } 192 | } 193 | 194 | impl LogManager for DirectoryLogManager { 195 | fn start(&mut self) -> std::io::Result<()> { 196 | rotate_logfile(&self.path)?; 197 | match self.mode { 198 | LogMode::Copy => { 199 | self.copy_output = Some(CopyOutput::new(&self.path, true)?); 200 | } 201 | LogMode::Redirect => { 202 | self.redirect_output = Some(RedirectOutput::new(&File::create(&self.path)?)?); 203 | } 204 | } 205 | Ok(()) 206 | } 207 | 208 | fn stop(&mut self) { 209 | self.copy_output = None; 210 | self.redirect_output = None; 211 | } 212 | } 213 | 214 | /// Log manager that does nothing. 215 | pub struct NoLogManager; 216 | 217 | impl NoLogManager { 218 | /// Create a new NoLogManager. 219 | /// 220 | /// # Returns 221 | /// A new NoLogManager instance 222 | pub fn new() -> Self { 223 | Self {} 224 | } 225 | } 226 | 227 | impl Default for NoLogManager { 228 | fn default() -> Self { 229 | Self::new() 230 | } 231 | } 232 | 233 | impl LogManager for NoLogManager { 234 | fn start(&mut self) -> std::io::Result<()> { 235 | Ok(()) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | /// Trait for build system outputs. 2 | /// 3 | /// This trait is implemented by types that represent outputs from a build system, 4 | /// such as binary packages, library packages, etc. 5 | pub trait Output: std::fmt::Debug { 6 | /// Get the family of this output (e.g., "binary", "python-package", etc.). 7 | /// 8 | /// # Returns 9 | /// A string identifying the output type family 10 | fn family(&self) -> &'static str; 11 | 12 | /// Get the dependencies declared by this output. 13 | /// 14 | /// # Returns 15 | /// A list of dependency names 16 | fn get_declared_dependencies(&self) -> Vec; 17 | } 18 | 19 | #[derive(Debug)] 20 | /// Output representing a binary executable. 21 | pub struct BinaryOutput(pub String); 22 | 23 | impl BinaryOutput { 24 | /// Create a new binary output. 25 | /// 26 | /// # Arguments 27 | /// * `name` - Name of the binary 28 | /// 29 | /// # Returns 30 | /// A new BinaryOutput instance 31 | pub fn new(name: &str) -> Self { 32 | BinaryOutput(name.to_string()) 33 | } 34 | } 35 | 36 | impl Output for BinaryOutput { 37 | fn family(&self) -> &'static str { 38 | "binary" 39 | } 40 | 41 | fn get_declared_dependencies(&self) -> Vec { 42 | vec![] 43 | } 44 | } 45 | 46 | #[derive(Debug)] 47 | /// Output representing a Python package. 48 | pub struct PythonPackageOutput { 49 | /// Name of the Python package. 50 | pub name: String, 51 | /// Optional version of the Python package. 52 | pub version: Option, 53 | } 54 | 55 | impl PythonPackageOutput { 56 | /// Create a new Python package output. 57 | /// 58 | /// # Arguments 59 | /// * `name` - Name of the Python package 60 | /// * `version` - Optional version of the Python package 61 | /// 62 | /// # Returns 63 | /// A new PythonPackageOutput instance 64 | pub fn new(name: &str, version: Option<&str>) -> Self { 65 | PythonPackageOutput { 66 | name: name.to_string(), 67 | version: version.map(|s| s.to_string()), 68 | } 69 | } 70 | } 71 | 72 | impl Output for PythonPackageOutput { 73 | fn family(&self) -> &'static str { 74 | "python-package" 75 | } 76 | 77 | fn get_declared_dependencies(&self) -> Vec { 78 | vec![] 79 | } 80 | } 81 | 82 | #[derive(Debug)] 83 | /// Output representing an R package. 84 | pub struct RPackageOutput { 85 | /// Name of the R package. 86 | pub name: String, 87 | } 88 | 89 | impl RPackageOutput { 90 | /// Create a new R package output. 91 | /// 92 | /// # Arguments 93 | /// * `name` - Name of the R package 94 | /// 95 | /// # Returns 96 | /// A new RPackageOutput instance 97 | pub fn new(name: &str) -> Self { 98 | RPackageOutput { 99 | name: name.to_string(), 100 | } 101 | } 102 | } 103 | 104 | impl Output for RPackageOutput { 105 | fn family(&self) -> &'static str { 106 | "r-package" 107 | } 108 | 109 | fn get_declared_dependencies(&self) -> Vec { 110 | vec![] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/shebang.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | use std::os::unix::fs::PermissionsExt; 3 | 4 | /// Work out what binary is necessary to run a script based on shebang 5 | /// 6 | /// # Arguments 7 | /// * `path` - Path to the script 8 | /// 9 | /// # Returns 10 | /// * `Ok(Some(binary))` - The binary necessary to run the script 11 | pub fn shebang_binary(path: &std::path::Path) -> std::io::Result> { 12 | let file = std::fs::File::open(path)?; 13 | if file.metadata()?.permissions().mode() & 0o111 == 0 { 14 | return Ok(None); 15 | } 16 | 17 | let bufreader = std::io::BufReader::new(file); 18 | 19 | let firstline = bufreader.lines().next(); 20 | let firstline = match firstline { 21 | Some(line) => line?, 22 | None => return Ok(None), 23 | }; 24 | 25 | if !firstline.starts_with("#!") { 26 | return Ok(None); 27 | } 28 | 29 | let args: Vec<&str> = firstline[2..].split_whitespace().collect(); 30 | let binary = if args[0] == "/usr/bin/env" || args[0] == "env" { 31 | args[1] 32 | } else { 33 | args[0] 34 | }; 35 | 36 | Ok(Some( 37 | std::path::Path::new(binary) 38 | .file_name() 39 | .unwrap() 40 | .to_string_lossy() 41 | .to_string(), 42 | )) 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | fn assert_shebang(content: &str, executable: bool, expected: Option<&str>) { 49 | let td = tempfile::tempdir().unwrap(); 50 | let path = td.path().join("test.sh"); 51 | std::fs::write(&path, content).unwrap(); 52 | if executable { 53 | let mut perms = std::fs::metadata(&path).unwrap().permissions(); 54 | perms.set_mode(0o755); 55 | std::fs::set_permissions(&path, perms).unwrap(); 56 | } 57 | let binary = super::shebang_binary(&path).unwrap(); 58 | assert_eq!(binary, expected.map(|s| s.to_string())); 59 | } 60 | #[test] 61 | fn test_empty() { 62 | assert_shebang("", true, None); 63 | } 64 | 65 | #[test] 66 | fn test_not_executable() { 67 | assert_shebang("#!/bin/sh\necho hello", false, None); 68 | } 69 | 70 | #[test] 71 | fn test_noshebang_line() { 72 | assert_shebang("echo hello", true, None); 73 | } 74 | 75 | #[test] 76 | fn test_env() { 77 | assert_shebang("#!/usr/bin/env sh\necho hello", true, Some("sh")); 78 | } 79 | 80 | #[test] 81 | fn test_plain() { 82 | assert_shebang("#!/bin/sh\necho hello", true, Some("sh")); 83 | } 84 | 85 | #[test] 86 | fn test_with_arg() { 87 | assert_shebang("#!/bin/sh -e\necho hello", true, Some("sh")); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/upstream.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a trait for dependencies that can find their upstream metadata. 2 | use crate::dependency::Dependency; 3 | pub use upstream_ontologist::UpstreamMetadata; 4 | 5 | /// A trait for dependencies that can find their upstream metadata. 6 | pub trait FindUpstream: Dependency { 7 | /// Find the upstream metadata for this dependency. 8 | fn find_upstream(&self) -> Option; 9 | } 10 | 11 | /// Find the upstream metadata for a dependency. 12 | /// 13 | /// This function attempts to find upstream metadata (like repository URL, 14 | /// homepage, etc.) for the given dependency by trying to downcast it to 15 | /// various concrete dependency types that implement the FindUpstream trait. 16 | /// 17 | /// # Arguments 18 | /// * `dependency` - The dependency to find upstream metadata for 19 | /// 20 | /// # Returns 21 | /// * `Some(UpstreamMetadata)` if upstream metadata was found 22 | /// * `None` if no upstream metadata could be found 23 | pub fn find_upstream(dependency: &dyn Dependency) -> Option { 24 | #[cfg(feature = "debian")] 25 | if let Some(dep) = dependency 26 | .as_any() 27 | .downcast_ref::() 28 | { 29 | return dep.find_upstream(); 30 | } 31 | 32 | if let Some(dep) = dependency 33 | .as_any() 34 | .downcast_ref::() 35 | { 36 | return dep.find_upstream(); 37 | } 38 | 39 | if let Some(dep) = dependency 40 | .as_any() 41 | .downcast_ref::() 42 | { 43 | return dep.find_upstream(); 44 | } 45 | 46 | if let Some(dep) = dependency 47 | .as_any() 48 | .downcast_ref::() 49 | { 50 | return dep.find_upstream(); 51 | } 52 | 53 | if let Some(dep) = dependency 54 | .as_any() 55 | .downcast_ref::() 56 | { 57 | return dep.find_upstream(); 58 | } 59 | 60 | if let Some(dep) = dependency 61 | .as_any() 62 | .downcast_ref::() 63 | { 64 | return dep.find_upstream(); 65 | } 66 | 67 | if let Some(dep) = dependency 68 | .as_any() 69 | .downcast_ref::() 70 | { 71 | return dep.find_upstream(); 72 | } 73 | 74 | if let Some(dep) = dependency 75 | .as_any() 76 | .downcast_ref::() 77 | { 78 | return dep.find_upstream(); 79 | } 80 | 81 | None 82 | } 83 | -------------------------------------------------------------------------------- /src/vcs.rs: -------------------------------------------------------------------------------- 1 | //! VCS-related functions 2 | use breezyshim::branch::Branch; 3 | use breezyshim::error::Error as BrzError; 4 | use breezyshim::prelude::Repository; 5 | use breezyshim::tree::PyTree; 6 | use breezyshim::workingtree::{GenericWorkingTree, WorkingTree}; 7 | use std::path::{Path, PathBuf}; 8 | use url::Url; 9 | 10 | /// Export a VCS tree to a new location. 11 | /// 12 | /// # Arguments 13 | /// * `tree` - The tree to export 14 | /// * `directory` - The directory to export the tree to 15 | /// * `subpath` - The subpath to export 16 | pub fn export_vcs_tree( 17 | tree: &T, 18 | directory: &Path, 19 | subpath: Option<&Path>, 20 | ) -> Result<(), BrzError> { 21 | breezyshim::export::export(tree, directory, subpath) 22 | } 23 | 24 | /// A Breezy tree that can be duplicated. 25 | pub trait DupableTree { 26 | /// Get the basis tree of this tree. 27 | fn basis_tree(&self) -> breezyshim::tree::RevisionTree; 28 | 29 | /// Get the parent location of this tree. 30 | fn get_parent(&self) -> Option; 31 | 32 | /// Get the base directory of this tree, if it has one. 33 | fn basedir(&self) -> Option; 34 | 35 | /// Export this tree to a directory. 36 | fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError>; 37 | } 38 | 39 | impl DupableTree for GenericWorkingTree { 40 | fn basis_tree(&self) -> breezyshim::tree::RevisionTree { 41 | WorkingTree::basis_tree(self).unwrap() 42 | } 43 | 44 | fn get_parent(&self) -> Option { 45 | WorkingTree::branch(self).get_parent() 46 | } 47 | 48 | fn basedir(&self) -> Option { 49 | Some(WorkingTree::basedir(self)) 50 | } 51 | 52 | fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError> { 53 | export_vcs_tree(self, directory, subpath) 54 | } 55 | } 56 | 57 | impl DupableTree for breezyshim::tree::RevisionTree { 58 | fn basis_tree(&self) -> breezyshim::tree::RevisionTree { 59 | self.repository() 60 | .revision_tree(&self.get_revision_id()) 61 | .unwrap() 62 | } 63 | 64 | fn get_parent(&self) -> Option { 65 | let branch = self.repository().controldir().open_branch(None).unwrap(); 66 | 67 | branch.get_parent() 68 | } 69 | 70 | fn basedir(&self) -> Option { 71 | None 72 | } 73 | 74 | fn export_to(&self, directory: &Path, subpath: Option<&Path>) -> Result<(), BrzError> { 75 | export_vcs_tree(self, directory, subpath) 76 | } 77 | } 78 | 79 | /// Duplicate a VCS tree to a new location, including all history. 80 | /// 81 | /// For a RevisionTree, this will duplicate the tree to a new location. 82 | /// For a WorkingTree, this will duplicate the basis tree to a new location. 83 | /// 84 | /// # Arguments 85 | /// * `orig_tree` - The tree to duplicate 86 | /// * `directory` - The directory to duplicate the tree to 87 | pub fn dupe_vcs_tree(orig_tree: &dyn DupableTree, directory: &Path) -> Result<(), BrzError> { 88 | let tree = orig_tree.basis_tree(); 89 | let result = tree.repository().controldir().sprout( 90 | Url::from_directory_path(directory).unwrap(), 91 | None, 92 | Some(true), 93 | None, 94 | Some(&tree.get_revision_id()), 95 | )?; 96 | 97 | assert!(result.has_workingtree()); 98 | 99 | // Copy parent location - some scripts need this 100 | if let Some(parent) = orig_tree.get_parent() { 101 | let mut branch = result.open_branch(None)?; 102 | branch.set_parent(&parent); 103 | } 104 | 105 | Ok(()) 106 | } 107 | --------------------------------------------------------------------------------