├── .github ├── ISSUE_TEMPLATE │ ├── blank_issue.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── docs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets └── example_output.png ├── docs ├── book.toml └── src │ ├── SUMMARY.md │ ├── apk.md │ ├── completions.md │ ├── configuration.md │ ├── deb.md │ ├── edit.md │ ├── env.md │ ├── images.md │ ├── inheritance.md │ ├── installation.md │ ├── metadata.md │ ├── new.md │ ├── output.md │ ├── pkg.md │ ├── recipes.md │ ├── rpm.md │ ├── scripts.md │ ├── signing.md │ └── usage.md ├── example ├── conf.yml ├── images │ ├── arch │ │ └── Dockerfile │ ├── debian │ │ └── Dockerfile │ └── rocky │ │ └── Dockerfile └── recipes │ ├── base-package │ └── recipe.yml │ ├── child-package1 │ └── recipe.yml │ ├── child-package2 │ └── recipe.yml │ ├── pkger-custom-images │ └── recipe.yml │ ├── pkger-prebuilt │ └── recipe.yml │ ├── pkger-simple │ └── recipe.yml │ ├── test-common-dependencies │ └── recipe.yml │ ├── test-fail-non-existent-patch │ └── recipe.yml │ ├── test-package │ └── recipe.yml │ ├── test-patches │ ├── recipe.yml │ ├── root.patch │ ├── src.patch │ ├── src │ │ └── testfile │ └── testrootfile │ └── test-suite │ ├── recipe.yml │ ├── some_dir │ └── some_file2.txt │ └── some_file.txt ├── libs ├── apkbuild │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ └── lib.rs ├── debbuild │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── binary.rs │ │ ├── lib.rs │ │ └── source.rs ├── pkgbuild │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ └── lib.rs ├── pkgspec-core │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── pkgspec │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ ├── lib.rs │ │ ├── parse.rs │ │ └── spec_impl.rs └── rpmspec │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ └── lib.rs ├── pkger-cli ├── Cargo.toml └── src │ ├── app │ ├── build.rs │ └── mod.rs │ ├── completions.rs │ ├── config.rs │ ├── gen.rs │ ├── job.rs │ ├── main.rs │ ├── metadata.rs │ ├── opts.rs │ └── table.rs └── pkger-core ├── Cargo.toml └── src ├── archive.rs ├── build ├── container.rs ├── deps.rs ├── image.rs ├── mod.rs ├── package │ ├── apk.rs │ ├── deb.rs │ ├── gzip.rs │ ├── mod.rs │ ├── pkg.rs │ ├── rpm.rs │ └── sign.rs ├── patches.rs ├── remote.rs └── scripts.rs ├── gpg.rs ├── image ├── mod.rs ├── os.rs └── state.rs ├── lib.rs ├── log.rs ├── oneshot.rs ├── proxy.rs ├── recipe ├── cmd.rs ├── envs.rs ├── loader.rs ├── metadata.rs ├── metadata │ ├── arch.rs │ ├── deps.rs │ ├── git.rs │ ├── image.rs │ ├── os.rs │ ├── patches.rs │ └── target.rs ├── mod.rs └── target.rs ├── runtime ├── container.rs ├── docker.rs ├── mod.rs └── podman.rs ├── ssh.rs └── template ├── lexer.rs └── mod.rs /.github/ISSUE_TEMPLATE/blank_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Issue 3 | about: Create a blank issue. 4 | --- 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: C-bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 12 | 13 | ### Reproduction steps 14 | 15 | ### Environment 16 | 17 | - Platform: 18 | - pkger version: 19 | 20 |
output (if applies) 21 | 22 | ``` 23 | Please provide a copy of the output when ran with this display options `--filter sf -t` 24 | ``` 25 | 26 |
27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or improvement 4 | title: '' 5 | labels: C-enhancement 6 | assignees: '' 7 | --- 8 | 9 | 11 | 12 | #### Describe your feature request 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: pkger CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths-ignore: 7 | - "*.md" 8 | - "LICENSE" 9 | - "docs" 10 | branches: 11 | - master 12 | pull_request: 13 | paths-ignore: 14 | - "*.md" 15 | - "LICENSE" 16 | - "docs" 17 | branches: 18 | - master 19 | 20 | jobs: 21 | lint: 22 | strategy: 23 | matrix: 24 | os: 25 | - ubuntu-latest 26 | - macos-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Set up Rust 30 | uses: hecrj/setup-rust-action@v1 31 | with: 32 | components: clippy,rustfmt 33 | - uses: actions/checkout@v3 34 | - run: make lint 35 | 36 | test: 37 | needs: [lint] 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: Setup Rust 42 | uses: hecrj/setup-rust-action@v1 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | - name: Test 46 | run: make test 47 | - name: Verify DEB package 48 | run: | 49 | sudo dpkg -i example/output/debian/test-package-0.1.0-0.amd64.deb 50 | cat /test/deb/test_file 51 | - name: Install alien 52 | run: sudo apt install -y alien rpm 53 | - name: Verify RPM package 54 | run: | 55 | sudo alien -i example/output/rocky/test-package-0.1.0-0.x86_64.rpm 56 | cat /test/rpm/test_file 57 | - name: Create a new image 58 | run: | 59 | cargo run -- -c example/conf.yml new image test-image 60 | cat example/images/test-image/Dockerfile 61 | - name: Create a new recipe 62 | run: | 63 | cargo run -- -c example/conf.yml new recipe test-recipe --version 0.1.0 --license MIT 64 | cat example/recipes/test-recipe/recipe.yml 65 | cat example/recipes/test-recipe/recipe.yml | grep name 66 | cat example/recipes/test-recipe/recipe.yml | grep license 67 | cat example/recipes/test-recipe/recipe.yml | grep version 68 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths-ignore: 7 | - "src" 8 | - "example" 9 | - "tests" 10 | - "assets" 11 | - "Cargo.*" 12 | - "README.md" 13 | - "CHANGELOG.md" 14 | - "LICENSE" 15 | - ".github" 16 | - "pkger-cli" 17 | - "pkger-core" 18 | - "libs" 19 | - "MakeFile" 20 | branches: 21 | - master 22 | 23 | jobs: 24 | deploy: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Setup mdBook 30 | uses: peaceiris/actions-mdbook@v1 31 | with: 32 | mdbook-version: 'latest' 33 | 34 | - name: Build the book 35 | run: mdbook build docs 36 | 37 | - name: Deploy it 38 | uses: peaceiris/actions-gh-pages@v3 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | publish_dir: ./docs/book 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "!*" 7 | tags: 8 | - "**" 9 | jobs: 10 | lint: 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | - macos-latest 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Set up Rust 19 | uses: hecrj/setup-rust-action@v1 20 | with: 21 | components: clippy,rustfmt 22 | - uses: actions/checkout@v3 23 | - run: make lint 24 | 25 | test: 26 | needs: [lint] 27 | strategy: 28 | matrix: 29 | os: 30 | - ubuntu-latest 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - name: Setup Rust 34 | uses: hecrj/setup-rust-action@v1 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | - name: Install rpm 38 | if: matrix.os == 'ubuntu-latest' 39 | run: | 40 | sudo apt -y update 41 | sudo apt -y install rpm 42 | - name: Test 43 | run: make test 44 | 45 | build_and_upload_artifacts: 46 | name: Upload Artifacts 47 | needs: [test] 48 | runs-on: ${{ matrix.os }} 49 | strategy: 50 | matrix: 51 | build: [linux, macos] 52 | include: 53 | - build: linux 54 | os: ubuntu-latest 55 | target: x86_64-unknown-linux 56 | - build: macos 57 | os: macos-latest 58 | target: x86_64-apple-darwin 59 | 60 | steps: 61 | - name: Set up Rust 62 | uses: hecrj/setup-rust-action@v1 63 | - uses: actions/checkout@v3 64 | - name: Set version 65 | run: echo "PKGER_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 66 | - name: Set archive name 67 | run: echo "PKGER_ARCHIVE=pkger-${{env.PKGER_VERSION}}-${{ matrix.target}}" >> $GITHUB_ENV 68 | - run: cargo build --release 69 | name: Release build 70 | - name: Install help2man mac 71 | if: matrix.os == 'macos-latest' 72 | run: brew install help2man 73 | - name: Install help2man ubuntu 74 | if: matrix.os == 'ubuntu-latest' 75 | run: | 76 | sudo apt -y update 77 | sudo apt -y install help2man 78 | - name: Prepare archive directory 79 | run: mkdir pkger 80 | - name: Generate manual 81 | run: | 82 | help2man target/release/pkger > pkger/pkger.1 83 | - name: Move release files 84 | run: | 85 | mv target/release/pkger pkger/ 86 | mv README.md pkger/ 87 | mv LICENSE pkger/ 88 | - name: Create archives 89 | run: | 90 | tar -zcvf ${{ env.PKGER_ARCHIVE }}.tar.gz pkger 91 | tar -Jcvf ${{ env.PKGER_ARCHIVE }}.tar.xz pkger 92 | - run: cp ${{ env.PKGER_ARCHIVE }}.tar.gz example/recipes/pkger-prebuilt/ 93 | if: matrix.os == 'ubuntu-latest' 94 | name: Copy archive 95 | - run: ./pkger/pkger -c example/conf.yml build -s rpm -s deb -- pkger-prebuilt 96 | if: matrix.os == 'ubuntu-latest' 97 | name: Build deb and rpm packages 98 | - run: | 99 | cd example/output/pkger-deb && \ 100 | mv pkger-prebuilt-${{env.PKGER_VERSION}}-0.amd64.deb pkger-${{env.PKGER_VERSION}}-0.amd64.deb 101 | cd ../pkger-rpm && \ 102 | mv pkger-prebuilt-${{env.PKGER_VERSION}}-0.x86_64.rpm pkger-${{env.PKGER_VERSION}}-0.x86_64.rpm 103 | if: matrix.os == 'ubuntu-latest' 104 | name: Rename artifacts 105 | - name: Upload gz 106 | uses: svenstaro/upload-release-action@v2 107 | with: 108 | repo_name: wojciechkepka/pkger 109 | repo_token: ${{ secrets.GITHUB_TOKEN }} 110 | file: ${{ env.PKGER_ARCHIVE }}.tar.gz 111 | asset_name: ${{ env.PKGER_ARCHIVE }}.tar.gz 112 | tag: ${{ env.PKGER_VERSION }} 113 | overwrite: true 114 | - name: Upload xz 115 | uses: svenstaro/upload-release-action@v2 116 | with: 117 | repo_name: wojciechkepka/pkger 118 | repo_token: ${{ secrets.GITHUB_TOKEN }} 119 | file: ${{ env.PKGER_ARCHIVE }}.tar.xz 120 | asset_name: ${{ env.PKGER_ARCHIVE }}.tar.xz 121 | tag: ${{ env.PKGER_VERSION }} 122 | overwrite: true 123 | - name: Upload deb 124 | if: matrix.os == 'ubuntu-latest' 125 | uses: svenstaro/upload-release-action@v2 126 | with: 127 | repo_name: wojciechkepka/pkger 128 | repo_token: ${{ secrets.GITHUB_TOKEN }} 129 | file: example/output/pkger-deb/pkger-${{env.PKGER_VERSION}}-0.amd64.deb 130 | asset_name: pkger-${{env.PKGER_VERSION}}-0.amd64.deb 131 | tag: ${{ env.PKGER_VERSION }} 132 | overwrite: true 133 | - name: Upload rpm 134 | if: matrix.os == 'ubuntu-latest' 135 | uses: svenstaro/upload-release-action@v2 136 | with: 137 | repo_name: wojciechkepka/pkger 138 | repo_token: ${{ secrets.GITHUB_TOKEN }} 139 | file: example/output/pkger-rpm/pkger-${{env.PKGER_VERSION}}-0.x86_64.rpm 140 | asset_name: pkger-${{env.PKGER_VERSION}}-0.x86_64.rpm 141 | tag: ${{ env.PKGER_VERSION }} 142 | overwrite: true 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .DS_Store 3 | .pkger.state 4 | example/output 5 | docs/book/ 6 | **/Cargo.lock 7 | .idea 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "pkger-cli", 5 | "pkger-core", 6 | "libs/pkgspec", 7 | "libs/pkgspec-core", 8 | "libs/rpmspec", 9 | "libs/debbuild", 10 | "libs/pkgbuild", 11 | "libs/apkbuild" 12 | ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := pkger 2 | 3 | 4 | .PHONY: all 5 | all: clean test build 6 | 7 | 8 | .PHONY: all_debug 9 | all_debug: clean test build_debug 10 | 11 | 12 | .PHONY: run_debug 13 | run_debug: build_debug 14 | @./target/debug/$(PROJECT) 15 | 16 | 17 | .PHONY: run 18 | run: build 19 | @./target/release/$(PROJECT) 20 | 21 | 22 | .PHONY: build_debug 23 | build_debug: ./target/debug/$(PROJECT) 24 | 25 | 26 | .PHONY: build 27 | build: ./target/release/$(PROJECT) 28 | 29 | 30 | .PHONY: lint 31 | lint: fmt_check clippy 32 | 33 | .PHONY: check 34 | check: 35 | cargo check --all 36 | 37 | .PHONY: test 38 | test: 39 | cargo t --all-targets --all-features -- --test-threads=1 40 | cargo r -- -c example/conf.yml build test-package test-suite child-package1 child-package2 41 | cargo r -- -c example/conf.yml build -s apk -s pkg -- test-package 42 | # below should fail 43 | -cargo r -- -c example/conf.yml build -s rpm -- test-fail-non-existent-patch 44 | test $? 1 45 | cargo r -- -c example/conf.yml build -i rocky debian -- test-common-dependencies 46 | @rpm -qp --requires example/output/rocky/test-common-dependencies-0.1.0-0.x86_64.rpm | grep openssl-devel 47 | @rpm -qp --conflicts example/output/rocky/test-common-dependencies-0.1.0-0.x86_64.rpm | grep httpd 48 | @rpm -qp --obsoletes example/output/rocky/test-common-dependencies-0.1.0-0.x86_64.rpm | grep bison1 49 | @dpkg-deb -I example/output/debian/test-common-dependencies-0.1.0-0.amd64.deb | grep Depends | grep libssl-dev 50 | @dpkg-deb -I example/output/debian/test-common-dependencies-0.1.0-0.amd64.deb | grep Conflicts | grep apache2 51 | cargo r -- -c example/conf.yml build -s rpm -- test-patches 52 | 53 | 54 | .PHONY: fmt_check 55 | fmt_check: 56 | cargo fmt --all -- --check 57 | 58 | 59 | .PHONY: fmt 60 | fmt: 61 | cargo fmt --all 62 | 63 | 64 | .PHONY: clippy 65 | clippy: 66 | @rustup component add clippy 67 | cargo clippy --all-targets --all-features -- -D clippy::all 68 | 69 | 70 | .PHONY: clean 71 | clean: 72 | @rm -rf target/* 73 | 74 | 75 | ./target/debug/$(PROJECT): 76 | @cargo build 77 | 78 | 79 | ./target/release/$(PROJECT): 80 | @cargo build --release 81 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkger 📦 2 | 3 | [![Documentation Status](https://github.com/vv9k/pkger/actions/workflows/docs.yml/badge.svg)](https://vv9k.github.io/pkger/) 4 | [![Build Status](https://github.com/vv9k/pkger/workflows/pkger%20CI/badge.svg)](https://github.com/vv9k/pkger/actions?query=workflow%3A%22pkger+CI%22) 5 | 6 | **pkger** is a tool that automates building *RPMs*, *DEBs*, *PKG*, *APK* and other packages on multiple *Linux* distributions, versions and architectures with the help of Docker or Podman. 7 | 8 | To learn more about **pkger** head over to the [user guide](https://vv9k.github.io/pkger/). 9 | 10 | ![Example output](https://github.com/vv9k/pkger/blob/master/assets/example_output.png) 11 | 12 | 13 | ## Example 14 | 15 | - Example configuration, recipes and images can be found in [`example` directory of `master` branch](https://github.com/vv9k/pkger/tree/master/example) 16 | 17 | ## License 18 | [MIT](https://github.com/vv9k/pkger/blob/master/LICENSE) 19 | -------------------------------------------------------------------------------- /assets/example_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vv9k/pkger/a5f1ea1384494b1fe63282fdf3483563bf495c6f/assets/example_output.png -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Wojciech Kępka"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "pkger" 7 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Installation](./installation.md) 4 | - [Configuration](./configuration.md) 5 | - [Recipes](./recipes.md) 6 | - [Metadata](./metadata.md) 7 | - [RPM](./rpm.md) 8 | - [DEB](./deb.md) 9 | - [PKG](./pkg.md) 10 | - [APK](./apk.md) 11 | - [Scripts](./scripts.md) 12 | - [Env](./env.md) 13 | - [Inheritance](./inheritance.md) 14 | - [Images](./images.md) 15 | - [Build a package](./usage.md) 16 | - [Signing packages](./signing.md) 17 | - [Formatting output](./output.md) 18 | - [Create new recipes and images](./new.md) 19 | - [Edit recipes, images and config](./edit.md) 20 | - [Shell completions](./completions.md) 21 | 22 | -------------------------------------------------------------------------------- /docs/src/apk.md: -------------------------------------------------------------------------------- 1 | # APK fields 2 | 3 | Optional fields that will be used when building a APK package. 4 | 5 | ```yaml 6 | apk: 7 | install: "$pkgname.pre-install $pkgname.post-install" 8 | 9 | # A list of packages that this package replaces 10 | replaces: [] 11 | 12 | # A list of dependencies for the check phase 13 | checkdepends: [] 14 | 15 | # If not provided a new generated key will be used to 16 | # sign the package 17 | private_key: "/location/of/apk_signing_key" 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/src/completions.md: -------------------------------------------------------------------------------- 1 | # Shell completions 2 | 3 | **pkger** provides a subcommand to print shell completions. Supported shells are: *bash*, *zsh*, *fish*, *powershell*, *elvish*. 4 | 5 | To print the completions run: 6 | ```shell 7 | pkger print-completions bash 8 | ``` 9 | 10 | replacing `bash` with whatever shell you prefer. 11 | 12 | 13 | To have completions automatically add something along those lines to your `.bashrc`, `.zshrc`...: 14 | ```shell 15 | . <(pkger print-completions bash) 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/src/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | By default **pkger** will look for the config file named `.pkger.yml` in the config directory appropriate for the OS 4 | that **pkger** is run on. If there is no global configuration, current directory will be scanned for the same file. 5 | To specify the location of the config file use `--config` or `-c` parameter. 6 | 7 | The configuration file has a following structure: 8 | 9 | ```yaml 10 | # required 11 | recipes_dir: "" 12 | output_dir: "" 13 | 14 | # optional 15 | log_dir: "" 16 | images_dir: "" 17 | runtime_uri: "unix:///var/run/docker.sock" 18 | 19 | # Disable colored output globally 20 | no_color: true 21 | 22 | ssh: 23 | # this will make the ssh auth socket available to the container so that it can use private keys from the host. 24 | forward_agent: true 25 | 26 | # This will allow tools that use SSH to connect to hosts that are not present in the `known_hosts` file 27 | disable_key_verification: true 28 | 29 | 30 | # override default images used by pkger 31 | custom_simple_images: 32 | deb: ubuntu:latest 33 | rpm: centos:latest 34 | 35 | # To define custom images add the following 36 | images: 37 | - name: rocky 38 | target: rpm 39 | - name: debian 40 | target: deb 41 | # if pkger fails to find out the operating system you can specify it by os parameter 42 | - name: arch 43 | target: pkg 44 | os: Arch Linux 45 | ``` 46 | 47 | The required fields when running a build are `recipes_dir` and `output_dir`. First tells **pkger** where to look for 48 | [recipes](./recipes.md) to build, the second is the directory where the final packages will end up. 49 | 50 | When using [custom images](./images.md) their location can be specified with `images_dir`. 51 | 52 | If container runtime daemon that **pkger** should connect does not run on a default unix socket override the uri with `runtime_uri` parameter. **pkger** will automatically determine wether the provided runtime uri is a Podman or Docker daemon. 53 | 54 | If an option is available as both configuration parameter and cli argument **pkger** will favour the arguments passed 55 | during startup. 56 | 57 | 58 | ## Generate configuration file and directories 59 | 60 | To quickly start of with **pkger** use the `pkger init` subcommand that will create necessary directories and the 61 | configuration file. Default locations can be overridden by command line parameters. 62 | -------------------------------------------------------------------------------- /docs/src/deb.md: -------------------------------------------------------------------------------- 1 | # DEB fields 2 | 3 | Optional fields that may be used when building a DEB package. 4 | 5 | ```yaml 6 | deb: 7 | priority: "" 8 | built_using: "" 9 | essential: true 10 | 11 | # specify the content of post install script 12 | postinst: "" 13 | 14 | # same as all other dependencies but deb specific 15 | pre_depends: [] 16 | recommends: [] 17 | suggests: [] 18 | breaks: [] 19 | replaces: [] 20 | enhances: [] 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/src/edit.md: -------------------------------------------------------------------------------- 1 | # Edit recipes, images and config 2 | 3 | **pkger** provides utility subcommand `edit` that invokes the default editor defined by `$EDITOR` environment variable. 4 | To make this functionality work, export this variable in your shell's init script like `~/.bashrc`. 5 | 6 | Edit images and recipes by name: 7 | 8 | ``` 9 | # This will open up the Dockerfile in the `rocky` image. 10 | $ pkger edit image rocky 11 | 12 | # This will open up the `recipe.yml` or `recipe.yaml` file in `pkger-simple` recipe directory 13 | $ pkger edit recipe pkger-simple 14 | 15 | ``` 16 | 17 | 18 | To edit the configuration file run: 19 | ``` 20 | $ pkger edit config 21 | 22 | 23 | # or shorhand 'e' for 'edit' 24 | $ pkger e 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/src/env.md: -------------------------------------------------------------------------------- 1 | # env (Optional) 2 | 3 | Optional environment variables that should be available during the [scripts](./scripts.md) phase and 4 | in some metadata fields. 5 | 6 | ```yaml 7 | env: 8 | HTTPS_PROXY: http://proxy.domain.com:1234 9 | RUST_LOG: trace 10 | ``` 11 | 12 | # **pkger** variables 13 | Some variables will be available to use during the build like: 14 | - `$PKGER_OS` the distribution of current container 15 | - `$PKGER_OS_VERSION` version of the distribution if applies 16 | - `$PKGER_BLD_DIR` the build directory with fetched source or git repo in the container 17 | - `$PKGER_OUT_DIR` the final directory from which **pkger** will copy files to target package 18 | - `$RECIPE` the name of the recipe that is built 19 | - `$RECIPE_VERSION` the version of the recipe 20 | - `$RECIPE_RELEASE` the release of the recipe 21 | -------------------------------------------------------------------------------- /docs/src/images.md: -------------------------------------------------------------------------------- 1 | # Images 2 | 3 | Images are an optional feature of **pkger**.By default **pkger** will create necessary images to build simple targets, 4 | they are completely distinct from user defined ones. User images offer higher customisation when it comes to preparing 5 | the build environment. 6 | 7 | In the images directory specified by the [configuration](./configuration.md) **pkger** will treat each subdirectory 8 | containing a `Dockerfile` as an image. The name of the directory will become the name of the image. 9 | 10 | So example structure like this: 11 | ``` 12 | images 13 | ├── arch 14 | │ └── Dockerfile 15 | ├── rocky 16 | │ └── Dockerfile 17 | └── debian 18 | └── Dockerfile 19 | ``` 20 | **pkger** will detect 3 images - *arch*, *rocky* and *debian*. 21 | 22 | Images with dependencies installed will be cached for each recipe-target combo to reduce the number of times the 23 | dependencies have to be pulled from remote sources. This saves a lot of space, time and bandwith. 24 | 25 | 26 | You can declare a new image with a subcommand. It will automatically create a directory in `images_dir` 27 | containing an empty `Dockerfile`. 28 | 29 | ```shell 30 | $ pkger new image 31 | ``` 32 | 33 | There is also a way to remove images. The `remove` subcommand erases the whole directory of an image if 34 | such exists: 35 | 36 | ```shell 37 | $ pkger remove images ... 38 | 39 | # or shorhand 'rm' for 'remove' and 'img' for 'images' 40 | $ pkger rm img ... 41 | ``` 42 | 43 | To see existing images use: 44 | ```shell 45 | $ pkger list images 46 | 47 | # or shorhand 'ls' for 'list' and 'img' for 'images' 48 | $ pkger ls img 49 | 50 | # for more detailed output 51 | $ pkger list -v images 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /docs/src/inheritance.md: -------------------------------------------------------------------------------- 1 | # Recipes inheritance 2 | 3 | 4 | Recipes support inheriting fields from a defined base recipe to avoid repetition. For example here is a definition of a base package: 5 | 6 | ```yaml 7 | --- 8 | metadata: 9 | name: base-package 10 | version: 0.1.0 11 | description: pkger base package testing recipe inheritance 12 | arch: x86_64 13 | license: MIT 14 | images: [ rocky, debian ] 15 | build: 16 | working_dir: $PKGER_OUT_DIR 17 | steps: 18 | - cmd: echo 123 >> ${RECIPE}_${RECIPE_VERSION} 19 | ``` 20 | 21 | And here is a child recipe using `from` field to define the parent recipe: 22 | 23 | ```yaml 24 | --- 25 | from: base-package 26 | metadata: 27 | name: child-package1 28 | version: 0.2.0 29 | description: pkger child package testing recipe inheritance from base-package 30 | install: 31 | shell: /bin/bash 32 | steps: 33 | - cmd: >- 34 | if [[ $(cat ${RECIPE}_${RECIPE_VERSION}) =~ 123 ]]; then exit 0; else 35 | echo "Test file ${RECIPE}_${RECIPE_VERSION} has invalid content"; exit 1; fi 36 | ``` 37 | 38 | 39 | The `child-package1` will share the `build` steps as well as `arch`, `license`, `images` fields. After merging the child recipe will look something like this: 40 | 41 | ```yaml 42 | --- 43 | from: base-package 44 | metadata: 45 | name: child-package1 46 | version: 0.2.0 47 | description: pkger child package testing recipe inheritance from base-package 48 | arch: x86_64 49 | license: MIT 50 | images: [ rocky, debian ] 51 | build: 52 | working_dir: $PKGER_OUT_DIR 53 | steps: 54 | - cmd: echo 123 >> ${RECIPE}_${RECIPE_VERSION} 55 | install: 56 | shell: /bin/bash 57 | steps: 58 | - cmd: >- 59 | if [[ $(cat ${RECIPE}_${RECIPE_VERSION}) =~ 123 ]]; then exit 0; else 60 | echo "Test file ${RECIPE}_${RECIPE_VERSION} has invalid content"; exit 1; fi 61 | ``` 62 | 63 | 64 | When defining a child recipe only `from` and `metadata.name` fields are required. Here is a minimal child recipe: 65 | 66 | ```yaml 67 | --- 68 | from: base-package 69 | metadata: 70 | name: child-package2 71 | ``` 72 | 73 | For a working example refer to the [`example` directory](https://github.com/vv9k/pkger/tree/master/example) of **pkger** source tree. 74 | -------------------------------------------------------------------------------- /docs/src/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | If you're using Arch Linux you can get **pkger** from *AUR* repositories with your favourite package manager like so: 4 | - `paru pkger-rs` 5 | 6 | On other Linux distributions or MacOS download one of the latest prebuild static binaries from 7 | [here](https://github.com/vv9k/pkger/releases). If your desired target is not on the list you'll have to build 8 | **pkger** from source by cloning the repository from `https://github.com/vv9k/pkger` and building it with: 9 | - `cargo build --release` 10 | -------------------------------------------------------------------------------- /docs/src/metadata.md: -------------------------------------------------------------------------------- 1 | # Metadata (Required) 2 | 3 | Contains all fields that describe the package being built. 4 | 5 | ## required fields 6 | 7 | ```yaml 8 | metadata: 9 | name: pkger 10 | description: pkger 11 | license: MIT 12 | version: 0.1.0 13 | ``` 14 | 15 | ## optional fields 16 | 17 | To specify which images a recipe should use add images parameter with a list of image targets. This field is ignored 18 | when building with `--simple` flag. 19 | 20 | ```yaml 21 | images: [ rocky, debian ] 22 | ``` 23 | 24 | You can also specify that all images apply to this recipe with: 25 | 26 | ```yaml 27 | all_images: true 28 | ``` 29 | 30 | ### sources 31 | 32 | This fields are responsible for fetching the files used for the build. When both `git` and `source` are specified 33 | **pkger** will fetch both to the build directory. 34 | 35 | If `source` starts with a prefix like `http` or `https` the file that if points to will be downloaded. If the file is an 36 | archive like `.tar.gz` or `.tar.xz` or `.zip` it will be directly extracted to 37 | [`$PKGER_BLD_DIR`](./env.md#pkger-variables), otherwise the file will be copied to the directory untouched. 38 | 39 | ```yaml 40 | source: "" # remote source or file system location 41 | 42 | # can also specify multiple sources: 43 | 44 | source: 45 | - 'http://some.website.com/file.tar.gz' 46 | - some_dir # relative path will be prefixed with recipe directory 47 | - /some/absolute/path # can be a directory or a file 48 | 49 | git: https://github.com/vv9k/pkger.git # will default to branch = "master" 50 | 51 | # or specify a branch like this: 52 | git: 53 | url: https://github.com/vv9k/pkger.git 54 | branch: dev 55 | ``` 56 | 57 | [Environment variables](./env.md) are available for this fields so this is possible: 58 | ```yaml 59 | source: "https://github.com/vv9k/${RECIPE}/${RECIPE_VERSION}" 60 | ``` 61 | 62 | 63 | ### common 64 | 65 | Optional fields shared across all targets. 66 | 67 | ```yaml 68 | release: "1" # defaults to "0" 69 | 70 | epoch: "42" 71 | 72 | maintainer: "vv9k" 73 | 74 | # The website of the package being built 75 | url: https://github.com/vv9k/pkger 76 | 77 | arch: x86_64 # defaults to `noarch` on RPM and `all` on DEB, `x86_64` automatically converted to `amd64` on DEB... 78 | 79 | skip_default_deps: true # skip installing default dependencies, it might break the builds 80 | 81 | exclude: ["share", "info"] # directories to exclude from final package 82 | 83 | group: "" # acts as Group in RPM or Section in DEB build 84 | ``` 85 | 86 | 87 | ### dependencies 88 | 89 | Common fields that specify dependencies, conflicts and provides will be added to the spec of the final package. 90 | 91 | This fields can be specified as arrays: 92 | ```yaml 93 | depends: [] 94 | conflicts: [] 95 | provides: [] 96 | ``` 97 | Or specified per image as a map below. 98 | 99 | **pkger** will install all dependencies listed in `build_depends`, choosing an appropriate package manager for each 100 | supported distribution. Default dependencies like `gzip` or `git` might be installed depending on the target job type. 101 | 102 | ```yaml 103 | build_depends: 104 | # common dependencies shared across all images 105 | all: ["gcc", "pkg-config", "git"] 106 | 107 | # dependencies for custom images 108 | rocky: ["cargo", "openssl-devel"] 109 | debian: ["curl", "libssl-dev"] 110 | ``` 111 | 112 | To specify same dependencies for multiple images join the images by `+` sign like this: 113 | ```yaml 114 | rocky+fedora34: [ cargo, openssl-devel ] 115 | ubuntu20+debian: [ libssl-dev ] 116 | # you can later specify dependencies just for this images 117 | debian: [ curl ] 118 | ``` 119 | 120 | if running a simple build and there is a need to specify dependencies for the target add dependencies for one of this 121 | images: 122 | 123 | ```yaml 124 | pkger-rpm+pkger-apk+pkger-pkg: ["cargo"] 125 | pkger-deb: ["curl"] 126 | pkger-gzip: [] 127 | ``` 128 | 129 | A custom image, for example `rocky`, will also use dependecies defined for `pkger-rpm`. The same will apply for all rpm based images (or images that have their target specified to RPM in the [configuration](./configuration.md)) 130 | 131 | 132 | ### Patches 133 | 134 | To apply patches to the fetched source code specify them just like dependencies. Patches can be specified as just file 135 | name in which case **pkger** will look for the patch in the recipe directory, if the path is absolute it will be read 136 | directly from the file system and finally if the patch starts with an `http` or `https` prefix the patch will be fetched 137 | from remote source. 138 | 139 | ```yaml 140 | patches: 141 | - some-local.patch 142 | - /some/absolute/path/to.patch 143 | - https://someremotesource.com/other.patch 144 | - patch: with-strip-level.patch 145 | images: [ debian ] # specify the images that this patch should be aplied on 146 | strip: 2 # this specifies the number of directories to strip before applying the patch (known as -pN or --stripN option in UNIX patch tool 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/src/new.md: -------------------------------------------------------------------------------- 1 | # Generate recipes 2 | 3 | To generate a recipe declaratively from CLI use the `pkger new recipe` subcommand. By default it requires only the name 4 | of the package and creates a directory with `recipe.yml` in it. 5 | 6 | 7 | # Create images 8 | 9 | To create images use `pkger new image `. This will create a directory with a `Dockerfile` in the `images_dir` 10 | specified in the [configuration](./configuration.md). 11 | -------------------------------------------------------------------------------- /docs/src/output.md: -------------------------------------------------------------------------------- 1 | # Formatting output 2 | 3 | By default **pkger** will display basic output as hierhical log with level set to `INFO`. All log messages will be printed to stdout unless a `--log-dir` flag (or `log_dir` is specified in [configuration](./configuration.md)) is provided, in that case there will be a single global log file in the logging directory created on each run as well as a separate file for each task. 4 | 5 | To debug run with `-d` or `--debug` option. To surpress all output except for errors and warnings add `-q` or `--quiet`. To enable very verbose output add `-t` or `--trace option. 6 | -------------------------------------------------------------------------------- /docs/src/pkg.md: -------------------------------------------------------------------------------- 1 | # PKG fields 2 | 3 | Optional fields that will be used when building a PKG package. 4 | 5 | ```yaml 6 | pkg: 7 | # location of the script in `$PKGER_OUT_DIR` that contains pre/post install/upgrade/remove functions 8 | # to be included in the final pkg 9 | install: ".install" 10 | 11 | # A list of files to be backed up when package will be removed or upgraded 12 | backup: ["/etc/pkger.conf"] 13 | 14 | # A list of packages that this package replaces 15 | replaces: [] 16 | 17 | # This are dependencies that this package needs to offer full functionality. 18 | optdepends: 19 | # Each dependency should contain a short description in this format: 20 | - "libpng: PNG images support" 21 | - "alsa-lib: sound support" 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/src/recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | Each recipe is a directory containing at least a `recipe.yml` or `recipe.yaml` file located at `recipes_dir` specified 4 | in the [configuration](./configuration.md). 5 | 6 | The recipe is divided into 2 required (*metadata*, *build*) and 3 optional (*config*, *install*, *env*) parts. 7 | To read more on each topic select a subsection in the menu. 8 | 9 | Here's an example working recipe for **pkger**: 10 | 11 | ```yaml 12 | metadata: 13 | name: pkger 14 | description: pkger 15 | arch: x86_64 16 | license: MIT 17 | version: 0.1.0 18 | maintainer: "vv9k" 19 | url: "https://github.com/vv9k/pkger" 20 | git: "https://github.com/vv9k/pkger.git" 21 | provides: [ pkger ] 22 | depends: 23 | pkger-deb: [ libssl-dev ] 24 | pkger-rpm: [ openssl-devel ] 25 | build_depends: 26 | all: [ gcc, pkg-config ] 27 | pkger-deb: [ curl libssl-dev ] 28 | pkger-rpm: [ cargo ] 29 | pkger-pkg: [ cargo ] 30 | env: 31 | RUSTUP_URL: https://sh.rustup.rs 32 | configure: 33 | steps: 34 | - cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL 35 | deb: true 36 | - cmd: sh /tmp/install_rust.sh -y --default-toolchain stable 37 | deb: true 38 | build: 39 | steps: 40 | - cmd: cargo build --color=never 41 | rpm: true 42 | pkg: true 43 | - cmd: $HOME/.cargo/bin/cargo build --color=never 44 | deb: true 45 | install: 46 | steps: 47 | - cmd: dir -p usr/bin 48 | - cmd: install -m755 $PKGER_BLD_DIR/target/debug/pkger usr/bin/ 49 | 50 | ``` 51 | 52 | You can declare a new recipe with a subcommand. It will automatically create a directory in `recipes_dir` 53 | containing a `recipe.yml` with the generated YAML recipe: 54 | 55 | ```shell 56 | $ pkger new recipe [OPTIONS] 57 | ``` 58 | 59 | There is also a way to remove recipes. The `remove` subcommand erases the whole directory of a recipe if 60 | such exists: 61 | 62 | ```shell 63 | $ pkger remove recipes ... 64 | 65 | # or shorhand 66 | # or shorhand 'rm' for 'remove' and 'rcp' for 'recipes' 67 | $ pkger rm rcp ... 68 | ``` 69 | 70 | To see existing recipes use: 71 | ```shell 72 | $ pkger list recipes 73 | 74 | # or shorhand 'ls' for 'list' and 'rcp' for 'recipes' 75 | $ pkger ls rcp ... 76 | 77 | # for more detailed output 78 | $ pkger list -v recipes 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/src/rpm.md: -------------------------------------------------------------------------------- 1 | # RPM fields 2 | 3 | Optional fields that will be used when building RPM target. 4 | 5 | ```yaml 6 | rpm: 7 | vendor: "" 8 | icon: "" 9 | summary: "shorter description" # if not provided defaults to value of `description` 10 | config_noreplace: "%{_sysconfdir}/%{name}/%{name}.conf" 11 | 12 | pre_script: "" 13 | post_script: "" 14 | preun_script: "" 15 | postun_script: "" 16 | 17 | # Disable automatic dependency processing. Setting this to true has no effect. 18 | auto_req_prov: false 19 | 20 | # acts the same as other dependencies - can be passed as array 21 | #obsoletes: ["foo"] 22 | # or as a map 23 | obsoletes: 24 | rocky: ["foo"] 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/src/scripts.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | **pkger** has 3 defined build phases - *configure*, *build* and *install* of which only *build* is required to create a 4 | package. 5 | 6 | Each phase has field called `steps` that takes an array of steps to execute during a given phase. A step can be a simple 7 | string that will be executed in the default shell like `"echo 123"` or an entry that specifies on what targets it should 8 | be executed like: 9 | ```yaml 10 | - cmd: >- 11 | echo only on deb targets 12 | deb: true 13 | ``` 14 | 15 | To set a working directory during the script phase set the `working_dir` parameter like so: 16 | ```yaml 17 | working_dir: /tmp 18 | ``` 19 | [Environment variables](./env.md) are available for this field so this is possible: 20 | ```yaml 21 | working_dir: ${PKGER_BLD_DIR}/${RECIPE}-${RECIPE_VERSION}-${SOME_USER_DEFINED_VAR} 22 | ``` 23 | 24 | To use a different shell to execute each command set the `shell` parameter: 25 | ```yaml 26 | shell: "/bin/bash" # optionally change default `/bin/sh` 27 | ``` 28 | 29 | ## configure (Optional) 30 | 31 | Optional configuration steps. If provided the steps will be executed before the build phase. 32 | The working directory will be set to [`$PKGER_BLD_DIR`](./env.md#pkger-variables) 33 | 34 | ```yaml 35 | configure: 36 | shell: "/bin/bash" 37 | steps: 38 | - cmd: >- 39 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 40 | ``` 41 | 42 | ## build (Required) 43 | 44 | This is the phase where the package should be assembled/compiled/linked and so on. All steps executed during the build 45 | will have the working directory seto to [`$PKGER_BLD_DIR`](./env.md#pkger-variables). This directory will contain either 46 | extracted sources if `source` is specified in [metadata](./metadata.md#optional-fields) or a git repository if `git` 47 | was specified. 48 | 49 | ```yaml 50 | build: 51 | steps: 52 | - cmd: $HOME/.cargo/bin/cargo build --release . 53 | - images: [ debian ] 54 | cmd: echo 'hello from Debian' # will only be executed on image `debian` 55 | 56 | - cmd: echo 'will only run on images with target == `rpm`' 57 | rpm: true 58 | # same applies to other targets 59 | pkg: false 60 | apk: false 61 | deb: false 62 | gzip: falze 63 | 64 | 65 | - cmd: echo 'only on version 0.2.0' 66 | versions: [ 0.2.0 ] 67 | # fields can also be combined 68 | - cmd: echo 'only on debian version 0.2.0' 69 | versions: [ 0.2.0 ] 70 | images: [ debian ] 71 | ] 72 | ``` 73 | 74 | ## install (Optional) 75 | 76 | Optional steps that (if provided) will be executed after the build phase. Working directory of each step will be set to 77 | [`$PKGER_OUT_DIR`](./env.md#pkger-variables) so you can use relative paths with commands like install. Each file that 78 | ends up in [`$PKGER_OUT_DIR`](./env.md#pkger-variables) will be available in the final package unless explicitly 79 | excluded by `exclude` field in [metadata](./metadata.md#optional-fields). So in the example below, the file that is 80 | installed will be available as `/usr/bin/pkger` with permissions preserved. 81 | 82 | ```yaml 83 | install: 84 | steps: 85 | - cmd: >- 86 | install -m755 $PKGER_BLD_DIR/target/release/pkger usr/bin/pkger 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/src/signing.md: -------------------------------------------------------------------------------- 1 | # Signing 2 | To sign packages automatically using a GPG key add the following to your [configuration file](./configuration.md): 3 | 4 | ```yaml 5 | gpg_key: /absolute/path/to/the/private/key 6 | gpg_name: Packager Name # must be the same as the `Name` field on the key 7 | ``` 8 | 9 | When **pkger** detects the gpg key in the configuration it will prompt for a password to the key on each run. 10 | 11 | Currently, only *deb* and *rpm* targets support signing. 12 | 13 | -------------------------------------------------------------------------------- /docs/src/usage.md: -------------------------------------------------------------------------------- 1 | # Build a package 2 | 3 | Currently available targets are: **rpm**, **deb**, **pkg**, **apk**, **gzip**. 4 | 5 | ### Simple build 6 | 7 | To build a simple package using **pkger** use: 8 | ```shell 9 | pkger build --simple [TARGETS] -- [RECIPES] 10 | ``` 11 | 12 | When using a simple build following linux distributions will be used for build images: 13 | - rpm: `rockylinux/rockylinux:latest` 14 | - deb: `debian:latest` 15 | - pkg: `archlinux` 16 | - apk: `alpine:latest` 17 | - gzip: `debian:latest` 18 | 19 | To override the default images set `custom_simple_images` like this: 20 | ```yaml 21 | custom_simple_images: 22 | deb: ubuntu:18 23 | rpm: fedora:latest 24 | ``` 25 | 26 | ### Custom images build 27 | 28 | To use [custom images](./images.md) drop the `--simple` parameter and just use: 29 | ```shell 30 | pkger build [RECIPES] 31 | ``` 32 | 33 | For this to have any effect the recipes have to have image targets defined (more on that [here](./metadata.md#optional-fields)) 34 | 35 | ### Examples 36 | 37 | #### Build a recipe for all supported images: 38 | ```shell 39 | pkger build recipe 40 | 41 | # or shorthand 'b' for 'build' 42 | pkger b recipe 43 | ``` 44 | 45 | #### Build all recipes for all supported images 46 | ```shell 47 | pkger build --all 48 | ``` 49 | 50 | #### Build multiple recipes on specified custom images: 51 | ```shell 52 | pkger build -i custom-image1 custom-image2 -- recipe1 recipe2 53 | ``` 54 | 55 | #### Build simple RPM, DEB, PKG... packages: 56 | ```shell 57 | pkger build -s rpm -s deb -s pkg -s gzip -- recipe1 58 | ``` 59 | 60 | #### Build only RPM package: 61 | ```shell 62 | pkger build -s rpm -- recipe1 63 | ``` 64 | 65 | ### Output 66 | 67 | After successfully building a package **pkger** will put the output artifact to `output_dir` specified in 68 | [configuration](./configuration.md) joined by the image name that was used to build the package. 69 | Each image will have a separate directory with all of its output packages. 70 | -------------------------------------------------------------------------------- /example/conf.yml: -------------------------------------------------------------------------------- 1 | images_dir: "example/images" 2 | recipes_dir: "example/recipes" 3 | output_dir: "example/output" 4 | 5 | images: 6 | - name: arch 7 | target: pkg 8 | os: Arch Linux 9 | - name: rocky 10 | target: rpm 11 | os: Rocky 12 | - name: debian 13 | target: deb 14 | os: Debian 15 | -------------------------------------------------------------------------------- /example/images/arch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux 2 | RUN pacman -Syu --noconfirm 3 | -------------------------------------------------------------------------------- /example/images/debian/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | -------------------------------------------------------------------------------- /example/images/rocky/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/rockylinux/rockylinux:latest 2 | -------------------------------------------------------------------------------- /example/recipes/base-package/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | name: base-package 4 | version: 0.1.0 5 | description: pkger base package testing recipe inheritance 6 | arch: x86_64 7 | license: MIT 8 | images: [ rocky, debian ] 9 | build: 10 | working_dir: $PKGER_OUT_DIR 11 | steps: 12 | - cmd: echo 123 >> ${RECIPE}_${RECIPE_VERSION} 13 | -------------------------------------------------------------------------------- /example/recipes/child-package1/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | from: base-package 3 | metadata: 4 | name: child-package1 5 | version: 0.2.0 6 | description: pkger child package testing recipe inheritance from base-package 7 | install: 8 | shell: /bin/bash 9 | steps: 10 | - cmd: >- 11 | if [[ $(cat ${RECIPE}_${RECIPE_VERSION}) =~ 123 ]]; then exit 0; else 12 | echo "Test file ${RECIPE}_${RECIPE_VERSION} has invalid content"; exit 1; fi 13 | -------------------------------------------------------------------------------- /example/recipes/child-package2/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | from: base-package 3 | metadata: 4 | name: child-package2 5 | version: 0.3.0 6 | description: pkger child package2 testing recipe inheritance from base-package 7 | build: 8 | steps: 9 | - cmd: echo 123 10 | install: 11 | shell: /bin/bash 12 | steps: 13 | - cmd: >- 14 | if [ ! -f ${RECIPE}_${RECIPE_VERSION} ]; then exit 0; else 15 | echo "Test file ${RECIPE}_${RECIPE_VERSION} should not exist on child-package2"; exit 1; fi 16 | -------------------------------------------------------------------------------- /example/recipes/pkger-custom-images/recipe.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: pkger 3 | description: pkger 4 | arch: x86_64 5 | license: MIT 6 | version: 0.11.0 7 | url: "https://github.com/vv9k/pkger" 8 | git: "https://github.com/vv9k/pkger.git" 9 | maintainer: "vv9k" 10 | provides: 11 | - pkger 12 | all_images: true 13 | depends: 14 | debian: 15 | - libssl-dev 16 | rocky: 17 | - openssl-devel 18 | build_depends: 19 | all: [ gcc, pkg-config] 20 | rocky+arch: [ cargo ] 21 | rocky: [ openssl-devel ] 22 | debian: [ curl, libssl-dev] 23 | env: 24 | RUSTUP_URL: https://sh.rustup.rs 25 | configure: 26 | steps: 27 | - cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL 28 | images: ["debian"] 29 | - cmd: sh /tmp/install_rust.sh -y --default-toolchain stable 30 | images: ["debian"] 31 | build: 32 | steps: 33 | - cmd: cargo build --color=never 34 | images: ["rocky", "arch"] 35 | - cmd: $HOME/.cargo/bin/cargo build --color=never --release 36 | images: ["debian"] 37 | install: 38 | steps: 39 | - cmd: mkdir -p usr/bin 40 | - cmd: install -m755 $PKGER_BLD_DIR/target/release/pkger usr/bin/ 41 | 42 | -------------------------------------------------------------------------------- /example/recipes/pkger-prebuilt/recipe.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: pkger-prebuilt 3 | description: pkger 4 | arch: x86_64 5 | license: MIT 6 | version: 0.11.0 7 | maintainer: "vv9k" 8 | url: "https://github.com/vv9k/pkger" 9 | source: pkger-0.11.0-x86_64-unknown-linux.tar.gz 10 | provides: 11 | - pkger 12 | depends: 13 | pkger-deb: 14 | - libssl-dev 15 | pkger-rpm: 16 | - openssl-devel 17 | build: 18 | steps: [] 19 | install: 20 | steps: 21 | - cmd: | 22 | mkdir -p \ 23 | usr/bin \ 24 | usr/share/licenses/pkger \ 25 | usr/share/doc/pkger 26 | - cmd: install -m755 $PKGER_BLD_DIR/pkger/pkger usr/bin/ 27 | - cmd: install -m644 $PKGER_BLD_DIR/pkger/README.md usr/share/doc/pkger/ 28 | - cmd: install -m644 $PKGER_BLD_DIR/pkger/LICENSE usr/share/licenses/pkger/ 29 | 30 | -------------------------------------------------------------------------------- /example/recipes/pkger-simple/recipe.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: pkger 3 | description: pkger 4 | arch: x86_64 5 | license: MIT 6 | version: 0.11.0 7 | maintainer: "vv9k" 8 | url: "https://github.com/vv9k/pkger" 9 | git: "https://github.com/vv9k/pkger.git" 10 | provides: 11 | - pkger 12 | depends: 13 | pkger-deb: 14 | - libssl-dev 15 | pkger-rpm: 16 | - openssl-devel 17 | build_depends: 18 | all: 19 | - gcc 20 | - pkg-config 21 | pkger-pkg: 22 | - cargo 23 | pkger-deb: 24 | - curl 25 | - libssl-dev 26 | pkger-rpm: 27 | - curl 28 | - openssl-devel 29 | configure: 30 | steps: 31 | - cmd: curl -o /tmp/install_rust.sh https://sh.rustup.rs 32 | deb: true 33 | rpm: true 34 | - cmd: sh /tmp/install_rust.sh -y --default-toolchain stable 35 | deb: true 36 | rpm: true 37 | build: 38 | steps: 39 | - cmd: cargo build --color=never 40 | pkg: true 41 | - cmd: $HOME/.cargo/bin/cargo build --color=never --release 42 | deb: true 43 | rpm: true 44 | install: 45 | steps: 46 | - cmd: mkdir -p usr/bin 47 | - cmd: install -m755 $PKGER_BLD_DIR/target/release/pkger usr/bin/ 48 | 49 | -------------------------------------------------------------------------------- /example/recipes/test-common-dependencies/recipe.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: test-common-dependencies 3 | description: Test that dependencies for common images like `pkger-rpm` get shared to other RPM based distros 4 | arch: x86_64 5 | license: MIT 6 | version: 0.1.0 7 | all_images: true 8 | build_depends: 9 | pkger-rpm: [ bison ] 10 | pkger-deb: [ bison ] 11 | depends: 12 | pkger-rpm: [ openssl-devel ] 13 | pkger-deb: [ libssl-dev ] 14 | conflicts: 15 | pkger-rpm: [ httpd ] 16 | pkger-deb: [ apache2 ] 17 | rpm: 18 | obsoletes: 19 | pkger-rpm: [ bison1 ] 20 | build: 21 | shell: /bin/bash 22 | steps: 23 | - cmd: | 24 | bison --version 25 | if [ $? -eq 0 ]; then exit 0; else echo \ 26 | 'bison is not installed but should be part of the build_depends list'; exit 1; fi 27 | images: [ rocky ] 28 | - cmd: | 29 | bison --version 30 | if [ $? -eq 0 ]; then exit 0; else echo \ 31 | 'bison is not installed but should be part of the build_depends list'; exit 1; fi 32 | images: [ debian ] 33 | -------------------------------------------------------------------------------- /example/recipes/test-fail-non-existent-patch/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | name: test-fail-non-existent-patch 4 | version: 0.1.0 5 | description: This recipe should fail as it specifies a patch that doesn't exist 6 | arch: x86_64 7 | license: MIT 8 | patches: 9 | - dummy.patch 10 | build: 11 | steps: [] 12 | -------------------------------------------------------------------------------- /example/recipes/test-package/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | name: test-package 4 | version: 0.1.0 5 | description: pkger test package 6 | arch: x86_64 7 | license: MIT 8 | images: [ rocky, debian ] 9 | configure: 10 | steps: 11 | - cmd: mkdir -p $PKGER_OUT_DIR/$RECIPE_VERSION/$RECIPE 12 | build: 13 | working_dir: $PKGER_OUT_DIR/$RECIPE_VERSION/$RECIPE 14 | steps: 15 | - cmd: echo $PWD 16 | install: 17 | steps: 18 | - cmd: mkdir -p $PKGER_OUT_DIR/test/deb 19 | images: ["debian"] 20 | - cmd: mkdir -p $PKGER_OUT_DIR/test/rpm 21 | images: ["rocky"] 22 | - cmd: echo "123" > $PKGER_OUT_DIR/test/rpm/test_file 23 | images: ["rocky"] 24 | - cmd: echo "321" > $PKGER_OUT_DIR/test/deb/test_file 25 | images: ["debian"] 26 | -------------------------------------------------------------------------------- /example/recipes/test-patches/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | metadata: 3 | name: test-patches 4 | version: 0.1.0 5 | description: pkger test package 6 | arch: x86_64 7 | license: MIT 8 | source: 9 | - src 10 | - testrootfile 11 | patches: 12 | - patch: src.patch 13 | strip: 1 14 | - root.patch 15 | build: 16 | steps: 17 | - cmd: >- 18 | if [[ $(cat src/testfile) =~ "exxample-patch321-patched" ]]; then exit 0; else 19 | echo "Test file src/testfile has invalid content"; exit 1; fi 20 | - cmd: >- 21 | if [[ $(cat testrootfile) =~ "root-file-patched" ]]; then exit 0; else 22 | echo "Test file testrootfile has invalid content"; exit 1; fi 23 | -------------------------------------------------------------------------------- /example/recipes/test-patches/root.patch: -------------------------------------------------------------------------------- 1 | --- testrootfile 2022-12-02 11:07:20.563951424 +0100 2 | +++ testrootfile1 2022-12-02 11:11:45.062500002 +0100 3 | @@ -1 +1 @@ 4 | -root-file-pre-patch 5 | +root-file-patched 6 | -------------------------------------------------------------------------------- /example/recipes/test-patches/src.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/testfile b/src/testfile 2 | index f5a1474..f15fd5e 100644 3 | --- a/src/testfile 4 | +++ b/src/testfile 5 | @@ -1 +1 @@ 6 | -example-patch123 7 | +exxample-patch321-patched 8 | -------------------------------------------------------------------------------- /example/recipes/test-patches/src/testfile: -------------------------------------------------------------------------------- 1 | example-patch123 2 | -------------------------------------------------------------------------------- /example/recipes/test-patches/testrootfile: -------------------------------------------------------------------------------- 1 | root-file-pre-patch 2 | -------------------------------------------------------------------------------- /example/recipes/test-suite/recipe.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: test-suite 3 | description: pkger testing suite 4 | arch: x86_64 5 | license: MIT 6 | version: 7 | - 0.2.0 8 | - 0.3.0 9 | skip_default_deps: true 10 | source: 11 | - some_dir 12 | - some_file.txt 13 | - https://raw.githubusercontent.com/vv9k/pkger/master/LICENSE 14 | exclude: 15 | - share 16 | - info 17 | images: [ debian, rocky ] 18 | env: 19 | ENV_VAR_TEST: 'test.com:1010' 20 | configure: 21 | working_dir: /var/lib 22 | steps: 23 | - cmd: >- 24 | echo SHELL=$0; 25 | if [ "$0" = "/bin/sh" ]; then exit 0; else echo 'Shell is configure script 26 | is not set to properly'; exit 1; fi 27 | - cmd: >- 28 | echo PWD=$PWD; 29 | if [ "$PWD" = "/var/lib" ]; then exit 0; else echo 'Working directory of 30 | configure script is not set properly'; exit 1; fi 31 | build: 32 | shell: /bin/bash 33 | steps: 34 | - cmd: >- 35 | echo SHELL=$0; 36 | if [[ $0 == /bin/bash ]]; then exit 0; else echo 'Shell is configure 37 | script is not set to properly'; exit 1; fi 38 | - cmd: >- 39 | echo PWD=$PWD; 40 | if [[ $PWD == $PKGER_BLD_DIR ]]; then exit 0; else echo 'Working directory 41 | of build script is not set properly'; exit 1; fi 42 | - cmd: >- 43 | echo 'Testing environment variables'; 44 | echo ENV_VAR_TEST=$ENV_VAR_TEST; 45 | if [[ ! '$ENV_VAR_TEST' =~ /test[.]com[:]1010/ ]]; then exit 0; else echo 46 | 'Environment variable ENV_VAR_TEST is not set properly'; exit 1; fi 47 | - cmd: >- 48 | echo 'Testing pkger env variables'; 49 | echo PKGER_OS=$PKGER_OS; 50 | if [[ $PKGER_OS =~ debian|rocky ]]; then exit 0; else echo 'Environment 51 | variable PKGER_OS is not set properly'; exit 1; fi 52 | # This does not always work 53 | #- cmd: >- 54 | #echo PKGER_OS_VERSION=$PKGER_OS_VERSION; 55 | #if [[ $PKGER_OS_VERSION =~ 10|8 ]]; then exit 0; else echo 'Environment 56 | #variable PKGER_OS_VERSION is not set properly'; exit 1; fi 57 | - cmd: >- 58 | echo PKGER_BLD_DIR=$PKGER_BLD_DIR; 59 | if [[ $PKGER_BLD_DIR =~ /tmp/test-suite-build-[0-9]* ]]; then exit 0; else 60 | echo 'Environment variable PKGER_BLD_DIR is not set properly'; exit 1; fi 61 | - cmd: >- 62 | echo PKGER_OUT_DIR=$PKGER_OUT_DIR; 63 | if [[ $PKGER_OUT_DIR =~ /tmp/test-suite-out-[0-9]* ]]; then exit 0; else 64 | echo 'Environment variable PKGER_OUT_DIR is not set properly'; exit 1; fi 65 | - cmd: >- 66 | echo RECIPE=$RECIPE; 67 | if [[ $RECIPE =~ test-suite ]]; then exit 0; else 68 | echo 'Environment variable RECIPE is not set properly'; exit 1; fi 69 | 70 | - cmd: >- 71 | echo RECIPE_VERSION=$RECIPE_VERSION; 72 | if [[ $RECIPE_VERSION =~ 0.[23].0 ]]; then exit 0; else 73 | echo 'Environment variable RECIPE_VERSION is not set properly'; exit 1; fi 74 | - versions: [ 0.2.0 ] 75 | cmd: echo "from_0.2.0" > /tmp/only_version_0.2.0 76 | - versions: [ 0.3.0 ] 77 | cmd: echo "from_0.3.0" > /tmp/only_version_0.3.0 78 | - versions: [ 0.2.0 ] 79 | cmd: >- 80 | if [ ! -f '/tmp/only_version_0.2.0' ]; then echo 'File /tmp/only_version_0.2.0 should 81 | exist on build version 0.2.0'; exit 1; fi 82 | - versions: [ 0.2.0 ] 83 | cmd: >- 84 | if [ -f '/tmp/only_version_0.3.0' ]; then echo 'File /tmp/only_version_0.3.0 shouldnt 85 | exist on build version 0.2.0'; exit 1; fi 86 | - versions: [ 0.3.0 ] 87 | cmd: >- 88 | if [ ! -f '/tmp/only_version_0.3.0' ]; then echo 'File /tmp/only_version_0.3.0 should 89 | exist on build version 0.3.0'; exit 1; fi 90 | - versions: [ 0.3.0 ] 91 | cmd: >- 92 | if [ -f '/tmp/only_version_0.2.0' ]; then echo 'File /tmp/only_version_0.2.0 shouldnt 93 | exist on build version 0.3.0'; exit 1; fi 94 | - cmd: >- 95 | echo RECIPE_RELEASE=$RECIPE_RELEASE; 96 | if [[ $RECIPE_RELEASE =~ 0 ]]; then exit 0; else 97 | echo 'Environment variable RECIPE_RELEASE is not set properly'; exit 1; fi 98 | 99 | - cmd: echo 'Testing pkger command syntax' 100 | # test if pkger commands work correctly 101 | - images: ["rocky"] 102 | cmd: touch /tmp/only_rocky 103 | # assure the file exists on rocky 104 | - images: ["rocky"] 105 | cmd: >- 106 | if [ ! -f '/tmp/only_rocky' ]; then echo 'File /tmp/only_rocky should 107 | exist on image rocky'; exit 1; fi 108 | # assure the file doesn't exist on debian 109 | - images: ["debian"] 110 | cmd: >- 111 | if [ -f '/tmp/only_rocky' ]; then echo 'File /tmp/only_rocky shouldnt 112 | exist on image debian'; exit 1; fi 113 | - images: ["rocky", "debian"] 114 | cmd: touch /tmp/pkger_group 115 | # assure the file exists on both images 116 | - images: ["rocky", "debian"] 117 | cmd: >- 118 | if [ ! -f '/tmp/pkger_group' ]; then echo 'File /tmp/pkger_group should 119 | exist on image both rocky and debian'; exit 1; fi 120 | # assure the file exists on both images 121 | - cmd: ls -l $PKGER_BLD_DIR 122 | - cmd: >- 123 | if [[ $(cat $PKGER_BLD_DIR/some_file.txt) =~ hello! ]]; then exit 0; else 124 | echo "Test file $PKGER_BLD_DIR/some_file.txt has invalid content"; exit 1; fi 125 | - cmd: >- 126 | if [[ $(cat $PKGER_BLD_DIR/some_dir/some_file2.txt) =~ hello2! ]]; then exit 0; else 127 | echo "Test file $PKGER_BLD_DIR/some_dir/some_file2.txt has invalid content"; exit 1; fi 128 | - cmd: >- 129 | if [[ $(cat $PKGER_BLD_DIR/LICENSE) =~ "MIT License" ]]; then exit 0; else 130 | echo "Test file $PKGER_BLD_DIR/LICENSE has invalid content"; exit 1; fi 131 | install: 132 | steps: 133 | - cmd: >- 134 | echo $PWD; 135 | if [ "$PWD" = "$PKGER_OUT_DIR" ]; then exit 0; else echo 'Working 136 | directory of install script is not set properly'; exit 1; fi 137 | - cmd: mkdir -p share/test/123 info/dir/to/remove 138 | -------------------------------------------------------------------------------- /example/recipes/test-suite/some_dir/some_file2.txt: -------------------------------------------------------------------------------- 1 | hello2! 2 | -------------------------------------------------------------------------------- /example/recipes/test-suite/some_file.txt: -------------------------------------------------------------------------------- 1 | hello! 2 | -------------------------------------------------------------------------------- /libs/apkbuild/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apkbuild" 3 | description = "Crate for APKBUILD generation" 4 | version = "0.1.0" 5 | authors = ["Wojciech Kępka "] 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | pkgspec = { path = "../pkgspec" } 11 | pkgspec-core = { path = "../pkgspec-core" } 12 | paste = "1" 13 | -------------------------------------------------------------------------------- /libs/apkbuild/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /libs/apkbuild/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pkgspec::SpecStruct; 2 | use pkgspec_core::{Error, Manifest, Result}; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Eq, SpecStruct)] 7 | pub struct ApkBuild { 8 | #[skip] 9 | /// Name of this packages or names if split packages 10 | pkgname: String, 11 | /// The version of the software. The variable is not allowed to contain colons, 12 | /// forward slashes, hyphens or whitespace 13 | pkgver: String, 14 | /// Release number of the package 15 | pkgrel: String, 16 | /// Architectures on which the given package is available 17 | arch: Vec, 18 | /// A brief description of the package 19 | pkgdesc: String, 20 | /// The url pointing to the website of the package 21 | url: String, 22 | /// License(s) of the package 23 | license: Vec, 24 | /// A list of source files required to build the package 25 | source: Vec, 26 | /// If the package contains pre/post install scripts this field should contain the install 27 | /// variables. 28 | install: Option, 29 | 30 | /// Subpackages are made to split up the normal "make install" into separate packages. The most 31 | /// common subpackages we use are doc and dev 32 | subpackages: Vec, 33 | 34 | /// A list of patches to apply to the package 35 | patches: Vec, 36 | 37 | /// A list of "virtual provisions" that this package provides 38 | provides: Vec, 39 | /// A list of packages this package depends on to run 40 | depends: Vec, 41 | /// A list of packages this package depends on to build 42 | makedepends: Vec, 43 | 44 | /// Directory used during prepare/build/install phases 45 | builddir: String, 46 | 47 | /// Specifies the prepare script of the package 48 | prepare_func: Option, 49 | /// Specifies the check script of the package 50 | check_func: Option, 51 | /// Specifies the build script of the package 52 | build_func: Option, 53 | /// Specifies the package script of the package 54 | package_func: Option, 55 | } 56 | 57 | impl Manifest for ApkBuild { 58 | /// Renders this APKBUILD and saves it to the given path 59 | fn save_to(&self, path: impl AsRef) -> Result<()> { 60 | fs::write(path, self.render()?).map_err(Error::from) 61 | } 62 | 63 | /// Renders this APKBUILD 64 | fn render(&self) -> Result { 65 | use std::fmt::Write; 66 | let mut pkg = String::new(); 67 | 68 | macro_rules! format_value { 69 | ($key:expr, $value:ident) => { 70 | if $value.contains(|c: char| c.is_ascii_whitespace() || c == '$') { 71 | write!(pkg, "{}=\"{}\"\n", $key, &$value)?; 72 | } else { 73 | write!(pkg, "{}={}\n", $key, &$value)?; 74 | } 75 | }; 76 | } 77 | 78 | macro_rules! push_field { 79 | ($field:ident) => { 80 | let f = &self.$field; 81 | format_value!(stringify!($field), f); 82 | }; 83 | } 84 | 85 | macro_rules! push_if_some { 86 | ($field:ident) => { 87 | if let Some(value) = &self.$field { 88 | format_value!(stringify!($field), value); 89 | } 90 | }; 91 | } 92 | 93 | macro_rules! push_array { 94 | ($field:ident) => { 95 | if !self.$field.is_empty() { 96 | write!( 97 | pkg, 98 | "{}=\"{}\"\n", 99 | stringify!($field), 100 | self.$field.join(" ") 101 | )?; 102 | } 103 | }; 104 | } 105 | 106 | macro_rules! push_func { 107 | ($field:ident) => { 108 | write!(pkg, "\n{}() {{\n{}\n}}\n", stringify!($field), $field)?; 109 | }; 110 | } 111 | 112 | push_field!(pkgname); 113 | push_field!(pkgver); 114 | push_field!(pkgrel); 115 | push_field!(pkgdesc); 116 | push_field!(url); 117 | push_array!(arch); 118 | push_array!(license); 119 | push_array!(provides); 120 | push_array!(depends); 121 | push_array!(makedepends); 122 | push_if_some!(install); 123 | push_array!(subpackages); 124 | push_array!(source); 125 | push_array!(patches); 126 | if self.builddir.is_empty() { 127 | const BUILDDIR: &str = "$srcdir/"; 128 | format_value!("builddir", BUILDDIR); 129 | } else { 130 | push_field!(builddir); 131 | } 132 | 133 | if let Some(prepare) = &self.prepare_func { 134 | push_func!(prepare); 135 | } 136 | if let Some(build) = &self.build_func { 137 | push_func!(build); 138 | } 139 | if let Some(check) = &self.check_func { 140 | push_func!(check); 141 | } 142 | if let Some(package) = &self.package_func { 143 | push_func!(package); 144 | } 145 | 146 | Ok(pkg) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | 154 | #[test] 155 | fn builds_a_apkbuild() { 156 | let got = ApkBuild::builder() 157 | .pkgname("apkbuild") 158 | .pkgver("0.1.0") 159 | .pkgrel("1") 160 | .pkgdesc("short description...") 161 | .url("https://some.invalid.url") 162 | .add_license_entries(vec!["MIT"]) 163 | .add_depends_entries(vec!["rust", "cargo"]) 164 | .build_func(" echo test") 165 | .check_func(" true\n false") 166 | .build() 167 | .render(); 168 | 169 | let expect = r#"pkgname=apkbuild 170 | pkgver=0.1.0 171 | pkgrel=1 172 | pkgdesc="short description..." 173 | url=https://some.invalid.url 174 | license="MIT" 175 | depends="rust cargo" 176 | builddir="$srcdir/" 177 | 178 | build() { 179 | echo test 180 | } 181 | 182 | check() { 183 | true 184 | false 185 | } 186 | "#; 187 | 188 | assert_eq!(expect, got.unwrap()); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /libs/debbuild/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "debbuild" 3 | description = "Crate for DEB/control file generation" 4 | version = "0.3.1" 5 | authors = ["Wojciech Kępka "] 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | paste = "1" 11 | pkgspec = { path = "../pkgspec" } 12 | pkgspec-core = { path = "../pkgspec-core" } 13 | -------------------------------------------------------------------------------- /libs/debbuild/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /libs/debbuild/README.md: -------------------------------------------------------------------------------- 1 | # deb-control-rs 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | 5 | > Crate for DEB/control file generation in Rust 6 | 7 | ## Usage 8 | 9 | This crate provides a simple builder interface for DEB/control files generation. There are two types of builders: binary and source. To access them use the associated functions on `DebControlBuilder`, for example: 10 | ```rust 11 | use deb_control::DebControlBuilder; 12 | 13 | DebControlBuilder::binary_package_builder(); 14 | // or 15 | DebControlBuilder::source_package_builder(); 16 | ``` 17 | 18 | Here's an example of building a binary DEB/control from scratch: 19 | ```rust 20 | use deb_control::DebControlBuilder; 21 | 22 | fn main() -> Result<(), std::boxed::Box> { 23 | let control = DebControlBuilder::binary_package_builder("debcontrol") 24 | .source("package.tar.gz") 25 | .version("1") 26 | .architecture("any") 27 | .maintainer("vv9k") 28 | .description("crate for DEB/control file generation") 29 | .essential(true) 30 | .section("devel") 31 | .homepage("https://some.invalid.url") 32 | .built_using("rustc") 33 | .add_pre_depends_entries(vec!["rustc", "cargo"]) 34 | .add_depends_entries(vec!["rustc", "cargo"]) 35 | .add_conflicts_entries(vec!["rustc", "cargo"]) 36 | .add_provides_entries(vec!["rustc", "cargo"]) 37 | .add_replaces_entries(vec!["rustc", "cargo"]) 38 | .add_enchances_entries(vec!["rustc", "cargo"]) 39 | .add_provides_entries(vec!["debcontrol"]) 40 | .build(); 41 | 42 | // you can later render it to a string like so: 43 | let _rendered = control.render()?; 44 | 45 | // or save it directly to a file 46 | control.save_to("/tmp/CONTROL")?; 47 | 48 | Ok(()) 49 | } 50 | 51 | ``` 52 | 53 | 54 | ## License 55 | [MIT](./LICENSE) 56 | -------------------------------------------------------------------------------- /libs/debbuild/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod binary; 2 | pub mod source; 3 | 4 | use binary::{BinaryDebControl, BinaryDebControlBuilder}; 5 | use source::{SourceDebControl, SourceDebControlBuilder}; 6 | 7 | pub struct DebControlBuilder {} 8 | impl DebControlBuilder { 9 | pub fn source_package_builder(name: S) -> SourceDebControlBuilder 10 | where 11 | S: Into, 12 | { 13 | SourceDebControl::builder().package(name) 14 | } 15 | 16 | pub fn binary_package_builder(name: S) -> BinaryDebControlBuilder 17 | where 18 | S: Into, 19 | { 20 | BinaryDebControl::builder().package(name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/pkgbuild/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkgbuild" 3 | description = "Crate for PKGBUILD generation" 4 | version = "0.1.1" 5 | authors = ["Wojciech Kępka "] 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | pkgspec = { path = "../pkgspec" } 11 | pkgspec-core = { path = "../pkgspec-core" } 12 | paste = "1" 13 | -------------------------------------------------------------------------------- /libs/pkgbuild/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /libs/pkgspec-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkgspec-core" 3 | version = "0.1.0" 4 | authors = ["Wojciech Kępka "] 5 | edition = "2021" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | thiserror = "1" 10 | -------------------------------------------------------------------------------- /libs/pkgspec-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use thiserror::Error as ThisError; 3 | 4 | #[derive(ThisError, Debug)] 5 | pub enum Error { 6 | #[error(transparent)] 7 | WriteError(#[from] std::io::Error), 8 | #[error(transparent)] 9 | FormatError(#[from] std::fmt::Error), 10 | } 11 | 12 | pub type Result = std::result::Result; 13 | 14 | pub trait Manifest { 15 | fn save_to(&self, path: impl AsRef) -> Result<()>; 16 | fn render(&self) -> Result; 17 | } 18 | -------------------------------------------------------------------------------- /libs/pkgspec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkgspec" 3 | description = "Crate with derives for auto generation of methods for spec files like DEB and RPM" 4 | version = "0.2.0" 5 | authors = ["Wojciech Kępka "] 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | paste = "1" 14 | proc-macro2 = "1" 15 | syn = "1" 16 | quote = "1" 17 | -------------------------------------------------------------------------------- /libs/pkgspec/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /libs/pkgspec/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod parse; 2 | mod spec_impl; 3 | 4 | use parse::Spec; 5 | 6 | use proc_macro::TokenStream; 7 | use quote::quote; 8 | use syn::parse_macro_input; 9 | 10 | #[proc_macro_derive(SpecStruct, attributes(skip, spec_error))] 11 | pub fn spec_struct(input: TokenStream) -> TokenStream { 12 | let input = parse_macro_input!(input as Spec); 13 | 14 | let mut code = quote! { 15 | use paste::paste; 16 | 17 | macro_rules! calculated_doc { 18 | ( 19 | $( 20 | #[doc = $doc:expr] 21 | $thing:item 22 | )* 23 | ) => ( 24 | $( 25 | #[doc = $doc] 26 | $thing 27 | )* 28 | ); 29 | } 30 | }; 31 | 32 | input.add_spec_struct_impl(&mut code); 33 | input.add_spec_builder_impl(&mut code); 34 | input.spec_builder_impl_from_fields(&mut code); 35 | 36 | TokenStream::from(code) 37 | } 38 | -------------------------------------------------------------------------------- /libs/pkgspec/src/parse.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::quote; 3 | use syn::{ 4 | parse::Parse, parse::ParseStream, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, 5 | Result, Type, 6 | }; 7 | 8 | pub struct SpecStruct { 9 | pub attrs: Vec, 10 | pub input: DeriveInput, 11 | } 12 | 13 | pub struct SpecField { 14 | pub _type: Type, 15 | pub name: Ident, 16 | pub docs: TokenStream2, 17 | } 18 | 19 | pub struct Spec { 20 | pub struct_ident: Ident, 21 | pub fields: Vec, 22 | } 23 | 24 | impl SpecStruct { 25 | fn parse_spec_fields(self) -> Vec { 26 | match self.input.data { 27 | Data::Struct(DataStruct { 28 | fields: Fields::Named(fields), 29 | .. 30 | }) => { 31 | let mut spec_fields = Vec::new(); 32 | for field in fields.named.into_iter() { 33 | let mut docs = TokenStream2::new(); 34 | 35 | for attr in &field.attrs { 36 | if attr.path.is_ident("skip") { 37 | continue; 38 | } 39 | 40 | docs.extend(quote! { #attr }); 41 | } 42 | 43 | spec_fields.push(SpecField { 44 | _type: field.ty, 45 | name: field.ident.unwrap(), 46 | docs, 47 | }); 48 | } 49 | 50 | spec_fields 51 | } 52 | _ => panic!("expected a struct with named fields"), 53 | } 54 | } 55 | } 56 | 57 | impl Parse for SpecStruct { 58 | fn parse(input: ParseStream) -> Result { 59 | Ok(SpecStruct { 60 | attrs: input.call(Attribute::parse_outer)?, 61 | input: input.parse()?, 62 | }) 63 | } 64 | } 65 | 66 | impl Parse for Spec { 67 | fn parse(input: ParseStream) -> Result { 68 | let spec_struct: SpecStruct = input.parse()?; 69 | let struct_ident = spec_struct.input.ident.clone(); 70 | let fields = spec_struct.parse_spec_fields(); 71 | 72 | Ok(Spec { 73 | struct_ident, 74 | fields, 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /libs/pkgspec/src/spec_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::parse::Spec; 2 | 3 | use proc_macro2::TokenStream as TokenStream2; 4 | use quote::quote; 5 | use syn::{Type, TypeParam}; 6 | 7 | impl Spec { 8 | pub fn add_spec_struct_impl(&self, tokens: &mut TokenStream2) { 9 | let struct_ident = &self.struct_ident; 10 | 11 | tokens.extend(quote! { 12 | impl #struct_ident { 13 | paste!{ 14 | calculated_doc!( 15 | #[doc = concat!("Returns the builder instance used for configurable initialization of [`", stringify!(#struct_ident), "`](", stringify!(#struct_ident), ")")] 16 | pub fn builder() -> [ <#struct_ident Builder> ] { 17 | [ <#struct_ident Builder> ] { inner: #struct_ident::default() } 18 | } 19 | ); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | pub fn add_spec_builder_impl(&self, tokens: &mut TokenStream2) { 26 | let struct_ident = &self.struct_ident; 27 | 28 | tokens.extend(quote! { 29 | paste! { 30 | calculated_doc!{ 31 | #[doc = concat!("A builder struct for [`", stringify!(#struct_ident), "`](", stringify!(#struct_ident), ")")] 32 | #[derive(Default, Debug)] 33 | pub struct [ <#struct_ident Builder> ] { 34 | inner: #struct_ident 35 | } 36 | } 37 | 38 | 39 | impl [ <#struct_ident Builder> ] { 40 | calculated_doc!{ 41 | #[doc = concat!("Finishes the building process returning the [`", stringify!(#struct_ident), "`](", stringify!(#struct_ident), ")")] 42 | pub fn build(self) -> #struct_ident { 43 | self.inner 44 | } 45 | } 46 | } 47 | 48 | } 49 | }); 50 | } 51 | 52 | pub fn spec_builder_impl_from_fields(&self, tokens: &mut TokenStream2) { 53 | let struct_ident = &self.struct_ident; 54 | 55 | self.fields.iter().for_each(|field| { 56 | let field_ty = &field._type; 57 | let field_ty_name = quote! { #field_ty }.to_string(); 58 | let field_name = &field.name; 59 | let docs = &field.docs; 60 | 61 | let mut ty_elems = field_ty_name.split(' '); 62 | match ty_elems.next() { 63 | Some("String") => { 64 | tokens.extend(quote! { 65 | paste! { 66 | impl [ <#struct_ident Builder> ] { 67 | #docs 68 | pub fn #field_name(mut self, #field_name: S) -> Self 69 | where 70 | S: Into, 71 | { 72 | self.inner.#field_name = #field_name.into(); 73 | self 74 | } 75 | }}}); 76 | } 77 | Some("bool") => { 78 | tokens.extend(quote! { 79 | paste! { 80 | impl [ <#struct_ident Builder> ] { 81 | #docs 82 | pub fn #field_name(mut self, #field_name: bool) -> Self 83 | { 84 | self.inner.#field_name = #field_name; 85 | self 86 | } 87 | }}}); 88 | } 89 | Some("Option") => { 90 | let _ = ty_elems.next(); 91 | let ty = ty_elems.next().expect("type"); 92 | let ty_initial = ty.chars().next().expect("initial type character"); 93 | let ty = syn::parse_str::(ty).unwrap(); 94 | let ty_initial = syn::parse_str::(&ty_initial.to_string()).unwrap(); 95 | tokens.extend(quote! { 96 | paste! { 97 | impl [ <#struct_ident Builder> ] { 98 | #docs 99 | pub fn #field_name<#ty_initial>(mut self, #field_name: #ty_initial) -> Self 100 | where 101 | #ty_initial: Into<#ty> 102 | { 103 | self.inner.#field_name = Some(#field_name.into()); 104 | self 105 | } 106 | }}}); 107 | } 108 | Some("Vec") => { 109 | tokens.extend(quote! { 110 | paste! { 111 | impl [ <#struct_ident Builder> ] { 112 | #docs 113 | pub fn [](mut self, entries: I) -> Self 114 | where 115 | I: IntoIterator, 116 | S: Into, 117 | { 118 | entries.into_iter().for_each(|entry| self.inner.#field_name.push(entry.into())); 119 | self 120 | } 121 | }}}); 122 | } 123 | _ => {} 124 | } 125 | }); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /libs/rpmspec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpmspec" 3 | description = "Crate for RPM spec generation" 4 | version = "0.3.0" 5 | authors = ["Wojciech Kępka "] 6 | edition = "2021" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | pkgspec = { path = "../pkgspec" } 11 | pkgspec-core = { path = "../pkgspec-core" } 12 | paste = "1" 13 | -------------------------------------------------------------------------------- /libs/rpmspec/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright 2021-2022 Wojciech Kępka 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.T 9 | -------------------------------------------------------------------------------- /libs/rpmspec/README.md: -------------------------------------------------------------------------------- 1 | # rpmspec-rs 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | 5 | > Crate for RPM spec generation in Rust 6 | 7 | ## Usage 8 | 9 | This crate provides a simple builder interface for RPM spec files generation. 10 | 11 | Here's an example of building a spec file from scratch (some fields are ommited): 12 | 13 | ```rust 14 | use rpmspec::RpmSpec; 15 | 16 | fn main() -> Result<(), std::boxed::Box> { 17 | let spec = RpmSpec::builder() 18 | .name("rpmspec") 19 | .license("MIT") 20 | .summary("short summary") 21 | .description("very long summary...") 22 | .build_script(BUILD) 23 | .install_script(INSTALL) 24 | .prep_script(r#"cat /etc/os-release"#) 25 | .check_script("uptime") 26 | .url("https://some.invalid.url") 27 | .version("0.1.0") 28 | .release("1") 29 | .epoch("42") 30 | .vendor("vv9k") 31 | .packager("vv9k") 32 | .copyright("2021 test") 33 | .build_arch("noarch") 34 | .exclude_arch("x86_64") 35 | .group("group") 36 | .icon("rpm.xpm") 37 | .build_root("/root/bld") 38 | .add_patches_entries(vec!["patch.1", "patch.2"]) 39 | .add_sources_entries(vec!["source.tar.gz", "source-2.tar.xz"]) 40 | .add_files_entries(vec!["/bin/test.bin", "/docs/README"]) 41 | .add_doc_files_entries(vec!["README"]) 42 | .add_license_files_entries(vec!["LICENSE"]) 43 | .add_provides_entries(vec!["rpmspec"]) 44 | .add_requires_entries(vec!["rust"]) 45 | .add_build_requires_entries(vec!["rust", "cargo"]) 46 | .add_obsoletes_entries(vec!["rpmspec-old"]) 47 | .add_conflicts_entries(vec!["rpmspec2"]) 48 | .config_noreplace("%{_sysconfdir}/%{name}/%{name}.conf") 49 | .pre_script("echo") 50 | .post_script("false") 51 | .preun_script("echo 123") 52 | .postun_script("true") 53 | .add_macro("githash", None::<&str>, "0ab32f") 54 | .add_macro("python", Some("-c"), "import os") 55 | .build(); 56 | 57 | // you can later render it to a string like so: 58 | let _rendered = spec.render()?; 59 | 60 | // or save it directly to a file 61 | spec.save_to("/tmp/RPMSPEC")?; 62 | 63 | Ok(()) 64 | } 65 | 66 | ``` 67 | 68 | ## License 69 | [MIT](./LICENSE) 70 | -------------------------------------------------------------------------------- /pkger-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkger" 3 | version = "0.11.0" 4 | description = "Package building tool utilizing Docker" 5 | authors = ["Wojciech Kępka "] 6 | repository = "https://github.com/vv9k/pkger" 7 | homepage = "https://github.com/vv9k/pkger" 8 | keywords = ["unix", "linux", "deb", "rpm", "docker"] 9 | categories = ["command-line-utilities"] 10 | license = "MIT" 11 | readme = "README.md" 12 | edition = "2021" 13 | 14 | [dependencies] 15 | pkger-core = { path = "../pkger-core" } 16 | 17 | clap = { version = "4", features = ["derive"] } 18 | clap_complete = "4" 19 | 20 | chrono = "0.4" 21 | colored = "2" 22 | ctrlc = "3" 23 | rpassword = "5" 24 | 25 | regex = "1" 26 | lazy_static = "1" 27 | 28 | dirs = "3" 29 | tempdir = "0.3" 30 | 31 | serde = {version = "1.0", features = ["derive"]} 32 | serde_yaml = "0.8" 33 | 34 | async-rwlock = "1" 35 | futures = "0.3" 36 | tokio = {version = "1", features = ["macros", "rt-multi-thread"]} 37 | 38 | uuid = { version = "0.8", features = ["serde", "v4"] } 39 | pretty_env_logger = "*" 40 | -------------------------------------------------------------------------------- /pkger-cli/src/completions.rs: -------------------------------------------------------------------------------- 1 | use crate::opts::{CompletionsOpts, Opts, APP_NAME}; 2 | use crate::Error; 3 | 4 | use clap::{CommandFactory, Parser}; 5 | use std::io; 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone, Copy, Debug, Parser)] 9 | #[allow(clippy::enum_variant_names)] 10 | pub enum Shell { 11 | Bash, 12 | Elvish, 13 | Fish, 14 | PowerShell, 15 | Zsh, 16 | } 17 | 18 | impl FromStr for Shell { 19 | type Err = Error; 20 | fn from_str(s: &str) -> Result { 21 | match &s.to_lowercase()[..] { 22 | "bash" => Ok(Shell::Bash), 23 | "elvish" => Ok(Shell::Elvish), 24 | "fish" => Ok(Shell::Fish), 25 | "powershell" => Ok(Shell::PowerShell), 26 | "zsh" => Ok(Shell::Zsh), 27 | _ => Err(Error::msg(format!("invalid shell `{}`", s))), 28 | } 29 | } 30 | } 31 | 32 | pub fn print(opts: &CompletionsOpts) { 33 | use clap_complete::{ 34 | generate, 35 | shells::{Bash, Elvish, Fish, PowerShell, Zsh}, 36 | }; 37 | 38 | let mut app = Opts::command(); 39 | 40 | match opts.shell { 41 | Shell::Bash => generate(Bash, &mut app, APP_NAME, &mut io::stdout()), 42 | Shell::Elvish => generate(Elvish, &mut app, APP_NAME, &mut io::stdout()), 43 | Shell::Fish => generate(Fish, &mut app, APP_NAME, &mut io::stdout()), 44 | Shell::PowerShell => generate(PowerShell, &mut app, APP_NAME, &mut io::stdout()), 45 | Shell::Zsh => generate(Zsh, &mut app, APP_NAME, &mut io::stdout()), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkger-cli/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use pkger_core::recipe::{deserialize_images, BuildTarget, ImageTarget}; 3 | use pkger_core::ssh::SshConfig; 4 | use pkger_core::ErrContext; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use std::fs; 8 | use std::path::{Path, PathBuf}; 9 | 10 | #[derive(Debug, Deserialize, Serialize)] 11 | pub struct Configuration { 12 | pub recipes_dir: PathBuf, 13 | pub output_dir: PathBuf, 14 | pub images_dir: Option, 15 | pub log_dir: Option, 16 | pub runtime_uri: Option, 17 | pub gpg_key: Option, 18 | pub gpg_name: Option, 19 | pub ssh: Option, 20 | #[serde(deserialize_with = "deserialize_images")] 21 | pub images: Vec, 22 | #[serde(skip_serializing)] 23 | #[serde(skip_deserializing)] 24 | pub path: PathBuf, 25 | pub custom_simple_images: Option, 26 | #[serde(default)] 27 | #[serde(skip_serializing_if = "default")] 28 | pub no_color: bool, 29 | } 30 | 31 | fn default(t: &T) -> bool { 32 | *t == Default::default() 33 | } 34 | 35 | impl Configuration { 36 | pub fn load>(path: P) -> Result { 37 | let path = path.as_ref(); 38 | serde_yaml::from_slice(&fs::read(path).context("failed to read configuration file")?) 39 | .context("failed to deserialize configuration file") 40 | .map(|mut cfg: Configuration| { 41 | cfg.path = path.to_path_buf(); 42 | cfg 43 | }) 44 | } 45 | 46 | pub fn save(&self) -> Result<()> { 47 | fs::write( 48 | &self.path, 49 | &serde_yaml::to_string(&self).context("failed to serialize configuration file")?, 50 | ) 51 | .context("failed to save configuration file") 52 | .map(|_| ()) 53 | } 54 | } 55 | 56 | #[derive(Debug, Deserialize, Serialize)] 57 | pub struct CustomImagesDefinition { 58 | pub rpm: Option, 59 | pub deb: Option, 60 | pub pkg: Option, 61 | pub apk: Option, 62 | pub gzip: Option, 63 | } 64 | 65 | impl CustomImagesDefinition { 66 | pub fn name_for_target(&self, target: BuildTarget) -> Option<&str> { 67 | match target { 68 | BuildTarget::Apk => self.apk.as_deref(), 69 | BuildTarget::Deb => self.deb.as_deref(), 70 | BuildTarget::Pkg => self.pkg.as_deref(), 71 | BuildTarget::Rpm => self.rpm.as_deref(), 72 | BuildTarget::Gzip => self.gzip.as_deref(), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkger-cli/src/gen.rs: -------------------------------------------------------------------------------- 1 | use crate::opts::GenRecipeOpts; 2 | use pkger_core::log::{debug, trace, warning, BoxedCollector}; 3 | use pkger_core::recipe::{DebRep, MetadataRep, PkgRep, RecipeRep, RpmRep}; 4 | 5 | use serde_yaml::{Mapping, Value as YamlValue}; 6 | 7 | pub fn recipe(opts: Box, logger: &mut BoxedCollector) -> RecipeRep { 8 | debug!(logger => "generating recipe"); 9 | trace!(logger => "{:?}", opts); 10 | 11 | let git = if let Some(url) = opts.git_url { 12 | let mut git_src = Mapping::new(); 13 | git_src.insert(YamlValue::from("url"), YamlValue::from(url)); 14 | if let Some(branch) = opts.git_branch { 15 | git_src.insert(YamlValue::from("branch"), YamlValue::from(branch)); 16 | } 17 | YamlValue::Mapping(git_src) 18 | } else { 19 | YamlValue::Null 20 | }; 21 | 22 | let mut env = Mapping::new(); 23 | if let Some(env_str) = opts.env { 24 | for kv in env_str.split(',') { 25 | let mut kv_split = kv.split('='); 26 | if let Some(k) = kv_split.next() { 27 | if let Some(v) = kv_split.next() { 28 | if let Some(entry) = env.insert(YamlValue::from(k), YamlValue::from(v)) { 29 | warning!(logger => "key '{}' already exists, old: {}, new: {}", k, entry.as_str().unwrap_or_default(), v); 30 | } 31 | } else { 32 | warning!(logger => "env entry '{}' missing a `=`", kv); 33 | } 34 | } else { 35 | warning!(logger => "env entry '{}' missing a key or `=`", kv); 36 | } 37 | } 38 | } 39 | 40 | macro_rules! vec_as_deps { 41 | ($it:expr) => {{ 42 | let vec = $it.into_iter().map(YamlValue::from).collect::>(); 43 | if vec.is_empty() { 44 | YamlValue::Null 45 | } else { 46 | YamlValue::Sequence(vec) 47 | } 48 | }}; 49 | } 50 | 51 | let deb = DebRep { 52 | priority: opts.priority, 53 | built_using: opts.built_using, 54 | essential: opts.essential, 55 | 56 | pre_depends: vec_as_deps!(opts.pre_depends), 57 | recommends: vec_as_deps!(opts.recommends), 58 | suggests: vec_as_deps!(opts.suggests), 59 | breaks: vec_as_deps!(opts.breaks), 60 | replaces: vec_as_deps!(opts.replaces.clone()), 61 | enhances: vec_as_deps!(opts.enchances), 62 | 63 | postinst_script: None, 64 | }; 65 | 66 | let rpm = RpmRep { 67 | obsoletes: vec_as_deps!(opts.obsoletes), 68 | vendor: opts.vendor, 69 | icon: opts.icon, 70 | summary: opts.summary, 71 | auto_req_prov: None, 72 | pre_script: None, 73 | post_script: None, 74 | preun_script: None, 75 | postun_script: None, 76 | config_noreplace: opts.config_noreplace, 77 | }; 78 | 79 | let pkg = PkgRep { 80 | install: opts.install_script, 81 | backup: opts.backup_files.unwrap_or_default(), 82 | replaces: vec_as_deps!(opts.replaces), 83 | optdepends: opts.optdepends.unwrap_or_default(), 84 | }; 85 | 86 | let metadata = MetadataRep { 87 | name: Some(opts.name), 88 | version: serde_yaml::to_value(opts.version.unwrap_or_else(|| "1.0.0".to_string())) 89 | .unwrap_or_default(), 90 | description: opts.description.or_else(|| Some("missing".to_string())), 91 | license: opts.license.or_else(|| Some("missing".to_string())), 92 | all_images: None, 93 | images: vec![], 94 | 95 | maintainer: opts.maintainer, 96 | url: opts.url, 97 | arch: opts.arch, 98 | source: serde_yaml::to_value(opts.source).unwrap_or_default(), 99 | git, 100 | skip_default_deps: opts.skip_default_deps, 101 | exclude: opts.exclude, 102 | group: opts.group, 103 | release: opts.release, 104 | epoch: opts.epoch, 105 | 106 | build_depends: vec_as_deps!(opts.build_depends), 107 | depends: vec_as_deps!(opts.depends), 108 | conflicts: vec_as_deps!(opts.conflicts), 109 | provides: vec_as_deps!(opts.provides), 110 | patches: vec_as_deps!(opts.patches), 111 | 112 | deb: Some(deb), 113 | rpm: Some(rpm), 114 | pkg: Some(pkg), 115 | apk: None, 116 | }; 117 | 118 | RecipeRep { 119 | from: None, 120 | metadata: Some(metadata), 121 | env: if env.is_empty() { None } else { Some(env) }, 122 | configure: None, 123 | build: Default::default(), 124 | install: None, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /pkger-cli/src/job.rs: -------------------------------------------------------------------------------- 1 | use pkger_core::build::{self, Context}; 2 | use pkger_core::log::BoxedCollector; 3 | use pkger_core::runtime; 4 | 5 | use std::time::{Duration, Instant}; 6 | 7 | pub enum JobResult { 8 | Success { 9 | id: String, 10 | duration: Duration, 11 | output: String, 12 | }, 13 | Failure { 14 | id: String, 15 | duration: Duration, 16 | reason: String, 17 | }, 18 | } 19 | 20 | impl JobResult { 21 | pub fn success(id: I, duration: Duration, output: O) -> Self 22 | where 23 | I: Into, 24 | O: Into, 25 | { 26 | Self::Success { 27 | id: id.into(), 28 | duration, 29 | output: output.into(), 30 | } 31 | } 32 | 33 | pub fn failure(id: I, duration: Duration, err: E) -> Self 34 | where 35 | I: Into, 36 | E: Into, 37 | { 38 | Self::Failure { 39 | id: id.into(), 40 | duration, 41 | reason: err.into(), 42 | } 43 | } 44 | } 45 | 46 | pub enum JobCtx { 47 | Build(Context), 48 | } 49 | 50 | impl JobCtx { 51 | pub async fn run(self, mut logger: BoxedCollector) -> JobResult { 52 | let start = Instant::now(); 53 | match self { 54 | JobCtx::Build(mut ctx) => match build::run(&mut ctx, &mut logger).await { 55 | Err(e) => { 56 | let duration = start.elapsed(); 57 | let reason = if ctx.is_docker() { 58 | match e.downcast::() { 59 | Ok(err) => match err { 60 | runtime::docker_api::Error::Fault { code: _, message } => message, 61 | e => e.to_string(), 62 | }, 63 | Err(e) => format!("{:?}", e), 64 | } 65 | } else { 66 | match e.downcast::() { 67 | Ok(err) => match err { 68 | runtime::podman_api::Error::Fault { code: _, message } => message, 69 | e => e.to_string(), 70 | }, 71 | Err(e) => format!("{:?}", e), 72 | } 73 | }; 74 | JobResult::failure(ctx.id(), duration, reason) 75 | } 76 | Ok(output) => JobResult::success( 77 | ctx.id(), 78 | start.elapsed(), 79 | output.to_string_lossy().to_string(), 80 | ), 81 | }, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkger-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate pkger_core; 3 | 4 | use std::fs; 5 | use std::process; 6 | use std::time::SystemTime; 7 | 8 | use app::Application; 9 | use config::Configuration; 10 | use opts::Opts; 11 | use pkger_core::log::{self, error}; 12 | use pkger_core::{ErrContext, Error, Result}; 13 | 14 | mod app; 15 | mod completions; 16 | mod config; 17 | mod gen; 18 | mod job; 19 | mod metadata; 20 | mod opts; 21 | mod table; 22 | 23 | static DEFAULT_CONFIG_FILE: &str = ".pkger.yml"; 24 | 25 | macro_rules! exit { 26 | ($($args:tt)*) => {{ 27 | error!($($args)*); 28 | process::exit(1); 29 | }}; 30 | 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> Result<()> { 35 | let opts = Opts::from_args(); 36 | 37 | pretty_env_logger::init(); 38 | 39 | let timestamp = SystemTime::now() 40 | .duration_since(SystemTime::UNIX_EPOCH) 41 | .unwrap_or_default() 42 | .as_secs(); 43 | 44 | if let opts::Command::Init(init_opts) = opts.command { 45 | let config_dir = dirs::config_dir().context("missing config directory")?; 46 | let pkger_dir = config_dir.join("pkger"); 47 | let recipes_dir = init_opts 48 | .recipes 49 | .unwrap_or_else(|| pkger_dir.join("recipes")); 50 | let output_dir = init_opts.output.unwrap_or_else(|| pkger_dir.join("output")); 51 | let images_dir = init_opts.images.unwrap_or_else(|| pkger_dir.join("images")); 52 | let config_path = init_opts 53 | .config 54 | .unwrap_or_else(|| config_dir.join(DEFAULT_CONFIG_FILE)); 55 | 56 | if !images_dir.exists() { 57 | println!("creating images directory ~> `{}`", images_dir.display()); 58 | fs::create_dir_all(&images_dir).context("failed to create images dir")?; 59 | } 60 | if !output_dir.exists() { 61 | println!("creating output directory ~> `{}`", output_dir.display()); 62 | fs::create_dir_all(&output_dir).context("failed to create output dir")?; 63 | } 64 | if !recipes_dir.exists() { 65 | println!("creating recipes directory ~> `{}`", recipes_dir.display()); 66 | fs::create_dir_all(&recipes_dir).context("failed to create recipes dir")?; 67 | } 68 | 69 | let cfg = Configuration { 70 | recipes_dir, 71 | output_dir, 72 | images_dir: Some(images_dir), 73 | log_dir: None, 74 | runtime_uri: opts.runtime_uri, 75 | gpg_key: init_opts.gpg_key, 76 | gpg_name: init_opts.gpg_name, 77 | ssh: None, 78 | images: vec![], 79 | path: config_path, 80 | custom_simple_images: None, 81 | no_color: false, 82 | }; 83 | 84 | if cfg.path.exists() { 85 | let mut line = String::new(); 86 | loop { 87 | println!("configuration file already exists, overwrite? y/n"); 88 | std::io::stdin() 89 | .read_line(&mut line) 90 | .context("failed to read input from user")?; 91 | match line.trim() { 92 | "y" => break, 93 | "n" => { 94 | println!("exiting..."); 95 | process::exit(1) 96 | } 97 | _ => continue, 98 | } 99 | } 100 | } 101 | println!("saving configuration ~> `{}`", cfg.path.display()); 102 | cfg.save()?; 103 | process::exit(0); 104 | } 105 | 106 | // config 107 | let config_path = opts 108 | .config 109 | .clone() 110 | .unwrap_or_else(|| match dirs::config_dir() { 111 | Some(config_dir) => config_dir 112 | .join(DEFAULT_CONFIG_FILE) 113 | .to_string_lossy() 114 | .to_string(), 115 | None => DEFAULT_CONFIG_FILE.to_string(), 116 | }); 117 | let result = Configuration::load(&config_path).context("failed to load configuration file"); 118 | if let Err(e) = &result { 119 | exit!("execution failed, reason: {:?}", e); 120 | } 121 | let config = result.unwrap(); 122 | 123 | let mut logger_config = if let Some(p) = &opts.log_dir { 124 | log::Config::file(p.join(format!("pkger-{}.log", timestamp))) 125 | } else if let Some(p) = &config.log_dir { 126 | log::Config::file(p.join(format!("pkger-{}.log", timestamp))) 127 | } else { 128 | log::Config::stdout() 129 | }; 130 | 131 | let disable_color = opts.no_color || config.no_color; 132 | if disable_color { 133 | logger_config = logger_config.no_color(true); 134 | if let Ok(mut log) = log::GLOBAL_OUTPUT_COLLECTOR.try_write() { 135 | log.set_override(false); 136 | } 137 | } 138 | 139 | let mut logger = match logger_config 140 | .as_collector() 141 | .context("failed to initialize global output collector") 142 | { 143 | Ok(config) => config, 144 | Err(e) => exit!("execution failed, reason: {:?}", e), 145 | }; 146 | 147 | if opts.trace { 148 | logger.set_level(log::Level::Trace); 149 | } else if opts.debug { 150 | logger.set_level(log::Level::Debug); 151 | } else if opts.quiet { 152 | logger.set_level(log::Level::Warn); 153 | } 154 | 155 | trace!(logger => "{:#?}", opts); 156 | trace!(logger => "{:#?}", config); 157 | 158 | let mut app = match Application::new(config, &opts, &mut logger) 159 | .await 160 | .context("failed to initialize pkger") 161 | { 162 | Ok(app) => app, 163 | Err(e) => exit!("execution failed, reason: {:?}", e), 164 | }; 165 | 166 | if let Err(e) = app.process_opts(opts, &mut logger).await { 167 | exit!("execution failed, reason: {:?}", e); 168 | } 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /pkger-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkger-core" 3 | version = "0.11.0" 4 | edition = "2021" 5 | authors = ["Wojciech Kępka "] 6 | license = "MIT" 7 | 8 | [dependencies] 9 | debbuild = { path = "../libs/debbuild" } 10 | rpmspec = { path = "../libs/rpmspec" } 11 | pkgbuild = { path = "../libs/pkgbuild" } 12 | apkbuild = { path = "../libs/apkbuild" } 13 | pkgspec-core = { path = "../libs/pkgspec-core" } 14 | 15 | #docker-api = { git = "https://github.com/vv9k/docker-api-rs" } 16 | docker-api = "0.12" 17 | podman-api = { git = "https://github.com/vv9k/podman-api-rs" } 18 | #podman-api = { path = "../../podman-api-rs" } 19 | #podman-api = "0.8" 20 | 21 | anyhow = "1" 22 | 23 | tar = "0.4" 24 | flate2 = "1" 25 | 26 | chrono = "0.4" 27 | 28 | colored = "2" 29 | 30 | async-trait = "0.1" 31 | async-rwlock = "1" 32 | futures = "0.3" 33 | 34 | serde = {version = "1.0", features = ["derive"]} 35 | serde_cbor = "0.11" 36 | serde_yaml = "0.8" 37 | merge-yaml-hash = "0.2" 38 | 39 | tempdir = "0.3" 40 | 41 | uuid = { version = "0.8", features = ["serde", "v4"] } 42 | lazy_static = "1" 43 | 44 | git2 = "0.14" 45 | tokio = "1" 46 | 47 | http = "0.2" 48 | ipnet = "2" 49 | -------------------------------------------------------------------------------- /pkger-core/src/archive.rs: -------------------------------------------------------------------------------- 1 | //! Helper functions that don't fit anywhere else 2 | 3 | pub use flate2; 4 | pub use tar; 5 | 6 | use crate::log::{debug, trace, BoxedCollector}; 7 | use crate::{ErrContext, Result}; 8 | 9 | use flate2::write::GzEncoder; 10 | use flate2::Compression; 11 | use std::fs::File; 12 | use std::io; 13 | use std::io::prelude::*; 14 | use std::path::Path; 15 | 16 | /// Unpacks a given tar archive to the path specified by `output_dir` 17 | pub fn unpack_tarball>( 18 | archive: &mut tar::Archive, 19 | output_dir: P, 20 | logger: &mut BoxedCollector, 21 | ) -> Result<()> { 22 | let output_dir = output_dir.as_ref(); 23 | debug!(logger => "unpacking archive to {}", output_dir.display()); 24 | 25 | for entry in archive.entries()? { 26 | let mut entry = entry?; 27 | if let tar::EntryType::Regular = entry.header().entry_type() { 28 | let path = entry.header().path()?.to_path_buf(); 29 | trace!(logger => "unpacking {}", path.display()); 30 | let name = path.file_name().unwrap_or_default(); 31 | 32 | entry.unpack(output_dir.join(name))?; 33 | } 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | /// Save the given tar archive as gzip encoded tar to path specified by `output_dir` with the 40 | /// filename set to `name`. 41 | pub fn save_tar_gz( 42 | archive: tar::Archive, 43 | name: &str, 44 | output_dir: &Path, 45 | logger: &mut BoxedCollector, 46 | ) -> Result<()> { 47 | let path = output_dir.join(name); 48 | debug!(logger => "creating a gzipped tar archive, name: {}, path: {}", name, output_dir.display()); 49 | 50 | let f = File::create(path.as_path())?; 51 | let mut e = GzEncoder::new(f, Compression::default()); 52 | let mut archive = archive.into_inner(); 53 | let mut bytes = Vec::new(); 54 | archive.read_to_end(&mut bytes)?; 55 | 56 | e.write_all(&bytes)?; 57 | 58 | e.finish()?; 59 | 60 | Ok(()) 61 | } 62 | 63 | /// Creates a tar archive from an iterator of entries consisting of a path and the content of the 64 | /// entry corresponding to the path. 65 | pub fn create_tarball<'archive, E, P>(entries: E, logger: &mut BoxedCollector) -> Result> 66 | where 67 | E: Iterator, 68 | P: AsRef, 69 | { 70 | debug!(logger => "creating a tar archive"); 71 | 72 | let archive_buf = Vec::new(); 73 | let mut archive = tar::Builder::new(archive_buf); 74 | 75 | for entry in entries { 76 | let path = entry.0.as_ref(); 77 | let size = entry.1.len() as u64; 78 | trace!(logger => "adding '{}' to archive, size: {}", path.display(), size); 79 | let mut header = tar::Header::new_gnu(); 80 | header.set_size(size); 81 | header.set_cksum(); 82 | archive.append_data(&mut header, path, entry.1)?; 83 | } 84 | 85 | archive.finish()?; 86 | 87 | archive.into_inner().context("failed to create tar archive") 88 | } 89 | -------------------------------------------------------------------------------- /pkger-core/src/build/container.rs: -------------------------------------------------------------------------------- 1 | use crate::build; 2 | use crate::image::ImageState; 3 | use crate::log::{debug, info, trace, BoxedCollector}; 4 | use crate::runtime::container::{fix_name, Container, CreateOpts, ExecOpts, Output}; 5 | use crate::runtime::{DockerContainer, PodmanContainer, RuntimeConnector}; 6 | use crate::ssh; 7 | use crate::{err, ErrContext, Error, Result}; 8 | 9 | use crate::recipe::Env; 10 | use std::path::Path; 11 | 12 | pub static SESSION_LABEL_KEY: &str = "pkger.session"; 13 | 14 | // https://github.com/rust-lang/rust-clippy/issues/7271 15 | #[allow(clippy::needless_lifetimes)] 16 | /// Creates and starts a container from the given ImageState 17 | pub async fn spawn<'ctx>( 18 | ctx: &'ctx build::Context, 19 | image_state: &ImageState, 20 | logger: &mut BoxedCollector, 21 | ) -> Result> { 22 | info!(logger => "initializing container context"); 23 | trace!(logger => "{:?}", image_state); 24 | 25 | if !ctx.recipe.metadata.version.has_version(&ctx.build_version) { 26 | return err!("invalid recipe version {}", ctx.build_version); 27 | } 28 | 29 | let mut volumes = Vec::new(); 30 | 31 | let mut env = ctx.recipe.env.clone(); 32 | env.insert("PKGER_BLD_DIR", ctx.container_bld_dir.to_string_lossy()); 33 | env.insert("PKGER_OUT_DIR", ctx.container_out_dir.to_string_lossy()); 34 | env.insert("PKGER_OS", image_state.os.name()); 35 | env.insert("PKGER_OS_VERSION", image_state.os.version()); 36 | env.insert("RECIPE", &ctx.recipe.metadata.name); 37 | env.insert("RECIPE_VERSION", &ctx.build_version); 38 | env.insert("RECIPE_RELEASE", ctx.recipe.metadata.release()); 39 | 40 | if let Some(ssh) = &ctx.ssh { 41 | if ssh.forward_agent { 42 | const CONTAINER_PATH: &str = "/ssh-agent"; 43 | let host_path = ssh::auth_sock()?; 44 | volumes.push(format!("{}:{}", host_path, CONTAINER_PATH)); 45 | env.insert(ssh::SOCK_ENV, CONTAINER_PATH); 46 | } 47 | 48 | if ssh.disable_key_verification { 49 | env.insert("GIT_SSH_COMMAND", "ssh -o StrictHostKeyChecking=no"); 50 | } 51 | } 52 | 53 | trace!("{:?}", env); 54 | 55 | let session_label = ctx.session_id.to_string(); 56 | 57 | let opts = CreateOpts::new(&image_state.id) 58 | .name(&fix_name(&ctx.id)) 59 | .cmd(["sleep infinity"]) 60 | .entrypoint(["/bin/sh", "-c"]) 61 | .labels([(SESSION_LABEL_KEY, session_label.as_str())]) 62 | .volumes(volumes) 63 | .env(env.clone()) 64 | .working_dir(ctx.container_bld_dir.to_string_lossy()); 65 | 66 | let mut ctx = Context::new(ctx, opts); 67 | ctx.set_env(env); 68 | ctx.container.spawn(&ctx.opts, logger).await?; 69 | Ok(ctx) 70 | } 71 | 72 | pub struct Context<'job> { 73 | pub container: Box, 74 | pub opts: CreateOpts, 75 | pub build: &'job build::Context, 76 | pub vars: Env, 77 | } 78 | 79 | impl<'job> Context<'job> { 80 | pub fn new(build: &'job build::Context, opts: CreateOpts) -> Context<'_> { 81 | Context { 82 | container: match &build.runtime { 83 | RuntimeConnector::Docker(docker) => Box::new(DockerContainer::new(docker.clone())), 84 | RuntimeConnector::Podman(podman) => Box::new(PodmanContainer::new(podman.clone())), 85 | }, 86 | opts, 87 | build, 88 | vars: Env::new(), 89 | } 90 | } 91 | 92 | pub fn set_env(&mut self, env: Env) { 93 | self.vars = env; 94 | } 95 | 96 | pub async fn checked_exec( 97 | &self, 98 | opts: &ExecOpts<'_>, 99 | logger: &mut BoxedCollector, 100 | ) -> Result> { 101 | debug!(logger => "running checked exec"); 102 | let out = self.container.exec(opts, logger).await?; 103 | if out.exit_code != 0 { 104 | err!( 105 | "command failed with exit code {}\nError:\n{}", 106 | out.exit_code, 107 | out.stderr.join("\n") 108 | ) 109 | } else { 110 | Ok(out) 111 | } 112 | } 113 | 114 | pub async fn script_exec( 115 | &self, 116 | script: impl IntoIterator, Option<&'static str>)>, 117 | logger: &mut BoxedCollector, 118 | ) -> Result<()> { 119 | debug!(logger => "executing script"); 120 | for (opts, context) in script.into_iter() { 121 | let mut res = self.checked_exec(&opts, logger).await.map(|_| ()); 122 | if let Some(context) = context { 123 | res = res.context(context); 124 | } 125 | 126 | #[allow(clippy::question_mark)] 127 | if res.is_err() { 128 | return res; 129 | } 130 | } 131 | Ok(()) 132 | } 133 | 134 | pub async fn create_dirs>( 135 | &self, 136 | dirs: &[P], 137 | logger: &mut BoxedCollector, 138 | ) -> Result<()> { 139 | let dirs_joined = 140 | dirs.iter() 141 | .map(P::as_ref) 142 | .fold(String::new(), |mut dirs_joined, path| { 143 | dirs_joined.push(' '); 144 | dirs_joined.push_str(&path.to_string_lossy()); 145 | dirs_joined 146 | }); 147 | let dirs_joined = dirs_joined.trim(); 148 | info!(logger => "creating directories"); 149 | debug!(logger => "Directories: {}", dirs_joined); 150 | 151 | self.checked_exec( 152 | &ExecOpts::new().cmd(&format!("mkdir -p {}", dirs_joined)), 153 | logger, 154 | ) 155 | .await 156 | .map(|_| ()) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /pkger-core/src/build/deps.rs: -------------------------------------------------------------------------------- 1 | use crate::image::Image; 2 | use crate::recipe::{BuildTarget, Dependencies, Recipe}; 3 | 4 | use std::collections::HashSet; 5 | 6 | pub fn recipe_and_default<'ctx>( 7 | deps: Option<&'ctx Dependencies>, 8 | recipe_: &Recipe, 9 | build_target: BuildTarget, 10 | state_image: &str, 11 | enable_gpg: bool, 12 | ) -> HashSet<&'ctx str> { 13 | let mut deps_out = default(&build_target, recipe_, enable_gpg); 14 | let recipe = recipe(deps, build_target, state_image); 15 | deps_out.extend(recipe); 16 | deps_out 17 | } 18 | 19 | pub fn recipe<'ctx>( 20 | deps: Option<&'ctx Dependencies>, 21 | build_target: BuildTarget, 22 | state_image: &str, 23 | ) -> HashSet<&'ctx str> { 24 | let mut deps_out = HashSet::new(); 25 | if let Some(deps) = &deps { 26 | deps_out.extend(deps.resolve_names(state_image)); 27 | let simple = Image::simple(build_target).name; 28 | deps_out.extend(deps.resolve_names(simple)); 29 | } 30 | deps_out 31 | } 32 | 33 | fn default(target: &BuildTarget, recipe: &Recipe, enable_gpg: bool) -> HashSet<&'static str> { 34 | let mut deps = HashSet::new(); 35 | deps.insert("tar"); 36 | match target { 37 | BuildTarget::Rpm => { 38 | deps.insert("rpm-build"); 39 | deps.insert("util-linux"); // for setarch 40 | 41 | if enable_gpg { 42 | deps.insert("gnupg2"); 43 | deps.insert("rpm-sign"); 44 | } 45 | } 46 | BuildTarget::Deb => { 47 | deps.insert("dpkg"); 48 | 49 | if enable_gpg { 50 | deps.insert("gnupg2"); 51 | deps.insert("dpkg-sig"); 52 | } 53 | } 54 | BuildTarget::Gzip => { 55 | deps.insert("gzip"); 56 | } 57 | BuildTarget::Pkg => { 58 | deps.insert("base-devel"); 59 | } 60 | BuildTarget::Apk => { 61 | deps.insert("alpine-sdk"); 62 | deps.insert("sudo"); 63 | deps.insert("bash"); 64 | } 65 | } 66 | 67 | let mut is_http = false; 68 | let mut is_zip = false; 69 | 70 | for src in &recipe.metadata.source { 71 | if src.starts_with("http") { 72 | is_http = true; 73 | } 74 | if src.ends_with(".zip") { 75 | is_zip = true; 76 | } 77 | } 78 | if is_http { 79 | deps.insert("curl"); 80 | } 81 | if is_zip { 82 | deps.insert("zip"); 83 | } 84 | 85 | if recipe.metadata.patches.is_some() { 86 | deps.insert("patch"); 87 | } 88 | 89 | deps 90 | } 91 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/apk.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::build::package::{Manifest, Package}; 3 | use crate::image::ImageState; 4 | use crate::log::{debug, info, trace, BoxedCollector}; 5 | use crate::runtime::container::ExecOpts; 6 | use crate::{ErrContext, Result}; 7 | 8 | use async_trait::async_trait; 9 | use std::path::{Path, PathBuf}; 10 | 11 | pub struct Apk; 12 | 13 | #[async_trait] 14 | impl Package for Apk { 15 | fn name(ctx: &Context<'_>, extension: bool) -> String { 16 | format!( 17 | "{}-{}-r{}{}", 18 | &ctx.build.recipe.metadata.name, 19 | &ctx.build.build_version, 20 | &ctx.build.recipe.metadata.release(), 21 | if extension { ".apk" } else { "" }, 22 | ) 23 | } 24 | 25 | /// Creates a final APK package and saves it to `output_dir` 26 | async fn build( 27 | ctx: &Context<'_>, 28 | image_state: &ImageState, 29 | output_dir: &Path, 30 | logger: &mut BoxedCollector, 31 | ) -> Result { 32 | let package_name = Self::name(ctx, false); 33 | 34 | info!(logger => "building APK package {}", package_name); 35 | 36 | let tmp_dir: PathBuf = ["/tmp", &package_name].into_iter().collect(); 37 | let src_dir = tmp_dir.join("src"); 38 | let bld_dir = tmp_dir.join("bld"); 39 | 40 | let source_tar_name = [&package_name, ".tar.gz"].join(""); 41 | let source_tar_path = bld_dir.join(&source_tar_name); 42 | 43 | let dirs = [tmp_dir.as_path(), bld_dir.as_path(), src_dir.as_path()]; 44 | 45 | ctx.create_dirs(&dirs[..], logger) 46 | .await 47 | .context("failed to create dirs")?; 48 | 49 | trace!(logger => "copy source files to temporary location"); 50 | ctx.checked_exec( 51 | &ExecOpts::default() 52 | .cmd(&format!("cp -rv . {}", src_dir.display())) 53 | .working_dir(&ctx.build.container_out_dir), 54 | logger, 55 | ) 56 | .await 57 | .context("failed to copy source files to temp directory")?; 58 | 59 | trace!(logger => "prepare archived source files"); 60 | ctx.checked_exec( 61 | &ExecOpts::default() 62 | .cmd(&format!("tar -zcvf {} .", source_tar_path.display())) 63 | .working_dir(src_dir.as_path()), 64 | logger, 65 | ) 66 | .await?; 67 | 68 | let sources = vec![source_tar_name]; 69 | static BUILD_USER: &str = "builduser"; 70 | 71 | let apkbuild = ctx 72 | .build 73 | .recipe 74 | .as_apkbuild( 75 | &image_state.image, 76 | &sources, 77 | &bld_dir, 78 | &ctx.build.build_version, 79 | *ctx.build.target.build_target(), 80 | logger, 81 | ) 82 | .render() 83 | .context("rendering apkbuild failed")?; 84 | debug!(logger => "{}", apkbuild); 85 | 86 | ctx.container 87 | .upload_files( 88 | vec![(PathBuf::from("APKBUILD").as_path(), apkbuild.as_bytes())], 89 | &bld_dir, 90 | logger, 91 | ) 92 | .await 93 | .context("failed to upload APKBUILD to container")?; 94 | 95 | trace!(logger => "create build user"); 96 | 97 | let home_dir: PathBuf = ["/home", BUILD_USER].into_iter().collect(); 98 | let abuild_dir = home_dir.join(".abuild"); 99 | 100 | ctx.script_exec( 101 | [ 102 | ( 103 | ExecOpts::new().cmd(&format!("adduser -D {}", BUILD_USER)), 104 | Some("failed to create a build user"), 105 | ), 106 | ( 107 | ExecOpts::new().cmd(&format!("passwd -d {}", BUILD_USER)), 108 | Some("failed to set password of build user"), 109 | ), 110 | ( 111 | ExecOpts::new().cmd(&format!("mkdir {}", abuild_dir.display())), 112 | None, 113 | ), 114 | ], 115 | logger, 116 | ) 117 | .await?; 118 | 119 | const SIGNING_KEY: &str = "apk-signing-key"; 120 | let key_path = abuild_dir.join(SIGNING_KEY); 121 | let uploaded_key = if let Some(key_location) = ctx 122 | .build 123 | .recipe 124 | .metadata 125 | .apk 126 | .as_ref() 127 | .and_then(|apk| apk.private_key.as_deref()) 128 | { 129 | if let Ok(key) = std::fs::read(key_location) { 130 | info!("uploading signing key"); 131 | trace!(logger => "key location: {}", key_location.display()); 132 | ctx.container 133 | .upload_files( 134 | [(PathBuf::from(SIGNING_KEY).as_path(), key.as_slice())].to_vec(), 135 | &abuild_dir, 136 | logger, 137 | ) 138 | .await 139 | .context("failed to upload signing key")?; 140 | ctx.checked_exec( 141 | &ExecOpts::new().cmd(&format!("chmod 600 {}", key_path.display())), 142 | logger, 143 | ) 144 | .await 145 | .context("failed to change mode of signing key")?; 146 | true 147 | } else { 148 | false 149 | } 150 | } else { 151 | false 152 | }; 153 | 154 | ctx.script_exec( 155 | [ 156 | ( 157 | ExecOpts::new().cmd(&format!( 158 | "chown -Rv {0}:{0} {1} {2}", 159 | BUILD_USER, 160 | bld_dir.display(), 161 | abuild_dir.display() 162 | )), 163 | Some("failed to change ownership of the build directory"), 164 | ), 165 | ( 166 | ExecOpts::new() 167 | .cmd("chmod 644 APKBUILD") 168 | .working_dir(&bld_dir), 169 | Some("failed to change mode of APKBUILD"), 170 | ), 171 | ], 172 | logger, 173 | ) 174 | .await?; 175 | 176 | if !uploaded_key { 177 | ctx.checked_exec( 178 | &ExecOpts::new() 179 | .cmd("abuild-keygen -an") 180 | .working_dir(&bld_dir) 181 | .user(BUILD_USER), 182 | logger, 183 | ) 184 | .await?; 185 | } else { 186 | ctx.checked_exec( 187 | &ExecOpts::new() 188 | .cmd(&format!( 189 | "echo PACKAGER_PRIVKEY=\"{}\" >> abuild.conf", 190 | key_path.display() 191 | )) 192 | .working_dir(&abuild_dir) 193 | .user(BUILD_USER), 194 | logger, 195 | ) 196 | .await?; 197 | } 198 | 199 | ctx.script_exec( 200 | [ 201 | ( 202 | ExecOpts::new() 203 | .cmd("abuild checksum") 204 | .working_dir(&bld_dir) 205 | .user(BUILD_USER), 206 | Some("failed to calculate checksum"), 207 | ), 208 | ( 209 | ExecOpts::new() 210 | .cmd("abuild") 211 | .working_dir(&bld_dir) 212 | .user(BUILD_USER), 213 | Some("failed to run abuild"), 214 | ), 215 | ], 216 | logger, 217 | ) 218 | .await?; 219 | 220 | let apk = format!("{}.apk", package_name); 221 | let mut apk_path = home_dir.clone(); 222 | apk_path.push("packages"); 223 | apk_path.push(&package_name); 224 | apk_path.push(ctx.build.recipe.metadata.arch.apk_name()); 225 | apk_path.push(&apk); 226 | 227 | ctx.container 228 | .download_files(&apk_path, output_dir, logger) 229 | .await 230 | .map(|_| output_dir.join(apk)) 231 | .context("failed to download finished package") 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/deb.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::build::package::sign::{import_gpg_key, upload_gpg_key}; 3 | use crate::build::package::{Manifest, Package}; 4 | use crate::image::ImageState; 5 | use crate::log::{debug, info, trace, BoxedCollector}; 6 | use crate::runtime::container::ExecOpts; 7 | use crate::{ErrContext, Result}; 8 | 9 | use async_trait::async_trait; 10 | use std::path::{Path, PathBuf}; 11 | 12 | pub struct Deb; 13 | 14 | #[async_trait] 15 | impl Package for Deb { 16 | fn name(ctx: &Context<'_>, extension: bool) -> String { 17 | format!( 18 | "{}-{}-{}.{}{}", 19 | &ctx.build.recipe.metadata.name, 20 | &ctx.build.build_version, 21 | ctx.build.recipe.metadata.release(), 22 | ctx.build.recipe.metadata.arch.deb_name(), 23 | if extension { ".deb" } else { "" }, 24 | ) 25 | } 26 | 27 | /// Creates a final DEB package and saves it to `output_dir` 28 | async fn build( 29 | ctx: &Context<'_>, 30 | image_state: &ImageState, 31 | output_dir: &Path, 32 | logger: &mut BoxedCollector, 33 | ) -> Result { 34 | let package_name = Self::name(ctx, false); 35 | 36 | info!(logger => "building DEB package {}", package_name); 37 | 38 | let debbld_dir = PathBuf::from("/root/debbuild"); 39 | let tmp_dir = debbld_dir.join("tmp"); 40 | let base_dir = debbld_dir.join(&package_name); 41 | let deb_dir = base_dir.join("DEBIAN"); 42 | let dirs = [deb_dir.as_path(), tmp_dir.as_path()]; 43 | 44 | ctx.create_dirs(&dirs[..], logger) 45 | .await 46 | .context("failed to create dirs")?; 47 | 48 | let size_out = ctx 49 | .checked_exec( 50 | &ExecOpts::default() 51 | .cmd("du -s .") 52 | .working_dir(&ctx.build.container_out_dir), 53 | logger, 54 | ) 55 | .await 56 | .context("failed to check size of package files")? 57 | .stdout 58 | .join(""); 59 | let size = size_out.split_ascii_whitespace().next(); 60 | 61 | let control = ctx 62 | .build 63 | .recipe 64 | .as_deb_control( 65 | &image_state.image, 66 | size, 67 | &ctx.build.build_version, 68 | *ctx.build.target.build_target(), 69 | logger, 70 | ) 71 | .render() 72 | .context("rendering apkbuild failed")?; 73 | debug!(logger => "{}", control); 74 | 75 | // Upload install scripts 76 | if let Some(deb) = &ctx.build.recipe.metadata.deb { 77 | let mut scripts = vec![]; 78 | let postinst_path = PathBuf::from("./postinst"); 79 | if let Some(postinst) = &deb.postinst_script { 80 | scripts.push((postinst_path.as_path(), postinst.as_bytes())); 81 | } 82 | if !scripts.is_empty() { 83 | let scripts_paths: String = scripts 84 | .iter() 85 | .map(|s| s.0.to_string_lossy()) 86 | .collect::>() 87 | .join(" "); 88 | 89 | ctx.container 90 | .upload_files(scripts, &deb_dir, logger) 91 | .await 92 | .context("failed to upload install scripts to container")?; 93 | 94 | ctx.checked_exec( 95 | &ExecOpts::default() 96 | .cmd(&format!("chmod 0755 {}", scripts_paths)) 97 | .working_dir(&deb_dir), 98 | logger, 99 | ) 100 | .await 101 | .context("failed to change ownership of build scripts")?; 102 | } 103 | } 104 | 105 | ctx.container 106 | .upload_files( 107 | vec![(PathBuf::from("./control").as_path(), control.as_bytes())], 108 | &deb_dir, 109 | logger, 110 | ) 111 | .await 112 | .context("failed to upload control file to container")?; 113 | 114 | trace!(logger => "copy source files to build dir"); 115 | ctx.checked_exec( 116 | &ExecOpts::default() 117 | .cmd(&format!("cp -rv . {}", base_dir.display())) 118 | .working_dir(&ctx.build.container_out_dir), 119 | logger, 120 | ) 121 | .await 122 | .context("failed to copy source files to build directory")?; 123 | 124 | let dpkg_deb_opts = if image_state.os.version().parse::().unwrap_or_default() < 10 { 125 | "--build" 126 | } else { 127 | "--build --root-owner-group" 128 | }; 129 | 130 | ctx.checked_exec( 131 | &ExecOpts::default().cmd(&format!( 132 | "dpkg-deb {} {}", 133 | dpkg_deb_opts, 134 | base_dir.display() 135 | )), 136 | logger, 137 | ) 138 | .await 139 | .context("failed to build deb package")?; 140 | 141 | let deb_name = [&package_name, ".deb"].join(""); 142 | let package_file = debbld_dir.join(&deb_name); 143 | 144 | sign_package(ctx, &package_file, logger).await?; 145 | 146 | ctx.container 147 | .download_files(&package_file, output_dir, logger) 148 | .await 149 | .map(|_| output_dir.join(deb_name)) 150 | .context("failed to download finished package") 151 | } 152 | } 153 | 154 | pub async fn sign_package( 155 | ctx: &Context<'_>, 156 | package: &Path, 157 | logger: &mut BoxedCollector, 158 | ) -> Result<()> { 159 | info!(logger => "singing package {}", package.display()); 160 | let gpg_key = if let Some(key) = &ctx.build.gpg_key { 161 | key 162 | } else { 163 | return Ok(()); 164 | }; 165 | 166 | let key_file = upload_gpg_key(ctx, gpg_key, &ctx.build.container_tmp_dir, logger) 167 | .await 168 | .context("failed to upload gpg key to container")?; 169 | 170 | import_gpg_key(ctx, gpg_key, &key_file, logger) 171 | .await 172 | .context("failed to import gpg key")?; 173 | 174 | trace!(logger => "get key id"); 175 | let key_id = ctx 176 | .checked_exec( 177 | &ExecOpts::default().cmd("gpg --list-keys --with-colons"), 178 | logger, 179 | ) 180 | .await 181 | .map(|out| { 182 | let stdout = out.stdout.join(""); 183 | for line in stdout.split('\n') { 184 | if !line.contains(gpg_key.name()) { 185 | continue; 186 | } 187 | 188 | return line.split(':').nth(7).map(ToString::to_string); 189 | } 190 | None 191 | }) 192 | .context("failed to get gpg key id")? 193 | .unwrap_or_default(); 194 | 195 | trace!(logger => "add signature"); 196 | ctx.checked_exec( 197 | &ExecOpts::default().cmd(&format!( 198 | r#"dpkg-sig -k {} -g "--pinentry-mode=loopback --passphrase {}" --sign {} {}"#, 199 | key_id, 200 | gpg_key.pass(), 201 | gpg_key.name().to_lowercase(), 202 | package.display() 203 | )), 204 | logger, 205 | ) 206 | .await 207 | .map(|_| ()) 208 | } 209 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/gzip.rs: -------------------------------------------------------------------------------- 1 | use crate::archive::{save_tar_gz, tar}; 2 | use crate::build::container::Context; 3 | use crate::build::package::Package; 4 | use crate::image::ImageState; 5 | use crate::log::{info, BoxedCollector}; 6 | use crate::{ErrContext, Result}; 7 | 8 | use async_trait::async_trait; 9 | use std::path::{Path, PathBuf}; 10 | 11 | pub struct Gzip; 12 | 13 | #[async_trait] 14 | impl Package for Gzip { 15 | fn name(ctx: &Context<'_>, extension: bool) -> String { 16 | format!( 17 | "{}-{}.{}", 18 | &ctx.build.recipe.metadata.name, 19 | &ctx.build.build_version, 20 | if extension { ".tar.gz" } else { "" }, 21 | ) 22 | } 23 | 24 | /// Creates a final GZIP package and saves it to `output_dir` returning the path of the final 25 | /// archive as String. 26 | async fn build( 27 | ctx: &Context<'_>, 28 | _: &ImageState, 29 | output_dir: &Path, 30 | logger: &mut BoxedCollector, 31 | ) -> Result { 32 | let archive_name = Self::name(ctx, true); 33 | info!(logger => "building GZIP package {}" ,archive_name); 34 | let package = ctx 35 | .container 36 | .copy_from(&ctx.build.container_out_dir, logger) 37 | .await?; 38 | 39 | let archive = tar::Archive::new(&package[..]); 40 | 41 | save_tar_gz(archive, &archive_name, output_dir, logger) 42 | .context("failed to save package as tar.gz") 43 | .map(|_| output_dir.join(archive_name)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::image::ImageState; 3 | use crate::log::BoxedCollector; 4 | use crate::recipe::BuildTarget; 5 | use crate::Result; 6 | 7 | use pkgspec_core::Manifest; 8 | 9 | pub mod apk; 10 | pub mod deb; 11 | pub mod gzip; 12 | pub mod pkg; 13 | pub mod rpm; 14 | mod sign; 15 | 16 | use async_trait::async_trait; 17 | use std::path::{Path, PathBuf}; 18 | 19 | #[async_trait] 20 | pub trait Package { 21 | fn name(ctx: &Context<'_>, extension: bool) -> String; 22 | async fn build( 23 | ctx: &Context<'_>, 24 | image_state: &ImageState, 25 | output_dir: &Path, 26 | logger: &mut BoxedCollector, 27 | ) -> Result; 28 | } 29 | 30 | pub async fn build( 31 | ctx: &Context<'_>, 32 | image_state: &ImageState, 33 | output_dir: &Path, 34 | output: &mut BoxedCollector, 35 | ) -> Result { 36 | match ctx.build.target.build_target() { 37 | BuildTarget::Gzip => gzip::Gzip::build(ctx, image_state, output_dir, output).await, 38 | BuildTarget::Rpm => rpm::Rpm::build(ctx, image_state, output_dir, output).await, 39 | BuildTarget::Deb => deb::Deb::build(ctx, image_state, output_dir, output).await, 40 | BuildTarget::Pkg => pkg::Pkg::build(ctx, image_state, output_dir, output).await, 41 | BuildTarget::Apk => apk::Apk::build(ctx, image_state, output_dir, output).await, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/pkg.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::build::package::{Manifest, Package}; 3 | use crate::image::ImageState; 4 | use crate::log::{debug, info, trace, BoxedCollector}; 5 | use crate::runtime::container::ExecOpts; 6 | use crate::{ErrContext, Result}; 7 | 8 | use async_trait::async_trait; 9 | use std::path::{Path, PathBuf}; 10 | 11 | pub struct Pkg; 12 | 13 | #[async_trait] 14 | impl Package for Pkg { 15 | fn name(ctx: &Context<'_>, extension: bool) -> String { 16 | format!( 17 | "{}-{}-{}-{}{}", 18 | &ctx.build.recipe.metadata.name, 19 | &ctx.build.build_version, 20 | &ctx.build.recipe.metadata.release(), 21 | ctx.build.recipe.metadata.arch.pkg_name(), 22 | if extension { ".pkg" } else { "" }, 23 | ) 24 | } 25 | 26 | /// Creates a final PKG package and saves it to `output_dir` 27 | async fn build( 28 | ctx: &Context<'_>, 29 | image_state: &ImageState, 30 | output_dir: &Path, 31 | logger: &mut BoxedCollector, 32 | ) -> Result { 33 | let package_name = Self::name(ctx, false); 34 | 35 | info!(logger => "building PKG package {}", package_name); 36 | 37 | let tmp_dir = PathBuf::from(format!("/tmp/{}", package_name)); 38 | let src_dir = tmp_dir.join("src"); 39 | let bld_dir = tmp_dir.join("bld"); 40 | 41 | let source_tar_name = [&package_name, ".tar.gz"].join(""); 42 | let source_tar_path = bld_dir.join(source_tar_name); 43 | 44 | let dirs = [tmp_dir.as_path(), bld_dir.as_path(), src_dir.as_path()]; 45 | 46 | ctx.create_dirs(&dirs[..], logger) 47 | .await 48 | .context("failed to create dirs")?; 49 | 50 | trace!(logger => "copy source files to temporary location"); 51 | ctx.checked_exec( 52 | &ExecOpts::default() 53 | .cmd(&format!("cp -rv . {}", src_dir.display())) 54 | .working_dir(&ctx.build.container_out_dir), 55 | logger, 56 | ) 57 | .await 58 | .context("failed to copy source files to temp directory")?; 59 | 60 | trace!(logger => "prepare archived source files"); 61 | ctx.checked_exec( 62 | &ExecOpts::default() 63 | .cmd(&format!("tar -zcvf {} .", source_tar_path.display())) 64 | .working_dir(src_dir.as_path()), 65 | logger, 66 | ) 67 | .await?; 68 | 69 | trace!(logger => "calculate source MD5 checksum"); 70 | let sum = ctx 71 | .checked_exec( 72 | &ExecOpts::default().cmd(&format!("md5sum {}", source_tar_path.display())), 73 | logger, 74 | ) 75 | .await 76 | .map(|out| out.stdout.join(""))?; 77 | let sum = sum 78 | .split_ascii_whitespace() 79 | .next() 80 | .map(|s| s.to_string()) 81 | .context("failed to calculate MD5 checksum of source")?; 82 | 83 | let sources = vec![source_tar_path.to_string_lossy().to_string()]; 84 | let checksums = vec![sum]; 85 | static BUILD_USER: &str = "builduser"; 86 | 87 | let pkgbuild = ctx 88 | .build 89 | .recipe 90 | .as_pkgbuild( 91 | &image_state.image, 92 | &sources, 93 | &checksums, 94 | &ctx.build.build_version, 95 | *ctx.build.target.build_target(), 96 | logger, 97 | ) 98 | .render() 99 | .context("rendering apkbuild failed")?; 100 | debug!(logger => "{}", pkgbuild); 101 | 102 | ctx.container 103 | .upload_files( 104 | vec![(PathBuf::from("PKGBUILD").as_path(), pkgbuild.as_bytes())], 105 | &bld_dir, 106 | logger, 107 | ) 108 | .await 109 | .context("failed to upload PKGBUILD to container")?; 110 | 111 | trace!(logger => "create build user"); 112 | ctx.script_exec( 113 | [ 114 | ( 115 | ExecOpts::new().cmd(&format!("useradd -m {}", BUILD_USER)), 116 | Some("failed to create build user"), 117 | ), 118 | ( 119 | ExecOpts::new().cmd(&format!("passwd -d {}", BUILD_USER)), 120 | Some("failed to create build user"), 121 | ), 122 | ( 123 | ExecOpts::new() 124 | .cmd(&format!("chown -Rv {0}:{0} .", BUILD_USER)) 125 | .working_dir(&bld_dir), 126 | Some("failed to change ownership of build directory"), 127 | ), 128 | ( 129 | ExecOpts::new() 130 | .cmd("chmod 644 PKGBUILD") 131 | .working_dir(&bld_dir), 132 | Some("failed to change mode of PKGBUILD"), 133 | ), 134 | ( 135 | ExecOpts::new() 136 | .cmd("makepkg") 137 | .working_dir(&bld_dir) 138 | .user(BUILD_USER), 139 | Some("failed to makepkg"), 140 | ), 141 | ], 142 | logger, 143 | ) 144 | .await?; 145 | 146 | let pkg = format!("{}.pkg.tar.zst", package_name); 147 | let pkg_path = bld_dir.join(&pkg); 148 | 149 | ctx.container 150 | .download_files(&pkg_path, output_dir, logger) 151 | .await 152 | .map(|_| output_dir.join(pkg)) 153 | .context("failed to download finished package") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pkger-core/src/build/package/sign.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::log::{info, BoxedCollector}; 3 | use crate::runtime::container::ExecOpts; 4 | use crate::{ErrContext, Result}; 5 | 6 | use crate::gpg::GpgKey; 7 | use std::{ 8 | fs, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | /// Uploads the `gpg_key` to `destination` in the container and returns the 13 | /// full path of the key in the container. 14 | pub(crate) async fn upload_gpg_key( 15 | ctx: &Context<'_>, 16 | gpg_key: &GpgKey, 17 | destination: &Path, 18 | logger: &mut BoxedCollector, 19 | ) -> Result { 20 | info!(logger => "uploading GPG key to '{}'", destination.display()); 21 | let key = fs::read(gpg_key.path()).context("failed reading the gpg key")?; 22 | 23 | ctx.container 24 | .upload_files( 25 | vec![(PathBuf::from("./GPG-SIGN-KEY").as_path(), key.as_slice())], 26 | destination, 27 | logger, 28 | ) 29 | .await 30 | .map(|_| destination.join("GPG-SIGN-KEY")) 31 | .context("failed to upload gpg key") 32 | } 33 | 34 | /// Imports the gpg key located at `path` to the database in the container. 35 | pub(crate) async fn import_gpg_key( 36 | ctx: &Context<'_>, 37 | gpg_key: &GpgKey, 38 | path: &Path, 39 | logger: &mut BoxedCollector, 40 | ) -> Result<()> { 41 | info!(logger => "importing GPG key from '{}'", path.display()); 42 | ctx.checked_exec( 43 | &ExecOpts::new().cmd(&format!( 44 | r#"gpg --pinentry-mode=loopback --passphrase {} --import {}"#, 45 | gpg_key.pass(), 46 | path.display(), 47 | )), 48 | logger, 49 | ) 50 | .await 51 | .map(|_| ()) 52 | } 53 | -------------------------------------------------------------------------------- /pkger-core/src/build/patches.rs: -------------------------------------------------------------------------------- 1 | use crate::build::{container, remote}; 2 | use crate::log::{debug, info, trace, BoxedCollector}; 3 | use crate::recipe::{Patch, Patches}; 4 | use crate::runtime::container::ExecOpts; 5 | use crate::Result; 6 | 7 | use std::path::PathBuf; 8 | 9 | pub async fn apply( 10 | ctx: &container::Context<'_>, 11 | patches: Vec<(Patch, PathBuf)>, 12 | logger: &mut BoxedCollector, 13 | ) -> Result<()> { 14 | info!(logger => "applying patches"); 15 | trace!(logger => "{:?}", patches); 16 | for (patch, location) in patches { 17 | if let Some(images) = patch.images() { 18 | if !images.is_empty() && !images.contains(&ctx.build.image.name) { 19 | debug!(logger => "skipping patch {:?}", patch); 20 | continue; 21 | } 22 | } 23 | debug!(logger => "applying patch: {:?}", patch); 24 | ctx.checked_exec( 25 | &ExecOpts::default() 26 | .cmd(&format!( 27 | "patch -p{} < {}", 28 | patch.strip_level(), 29 | location.display() 30 | )) 31 | .working_dir(&ctx.build.container_bld_dir), 32 | logger, 33 | ) 34 | .await?; 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | pub async fn collect( 41 | ctx: &container::Context<'_>, 42 | patches: &Patches, 43 | logger: &mut BoxedCollector, 44 | ) -> Result> { 45 | info!(logger => "collecting patches"); 46 | let mut out = Vec::new(); 47 | let patch_dir = ctx.build.container_tmp_dir.join("patches"); 48 | ctx.create_dirs(&[patch_dir.as_path()], logger).await?; 49 | 50 | let mut to_copy = Vec::new(); 51 | 52 | for patch in patches.resolve_names(ctx.build.target.image()) { 53 | let src = patch.patch(); 54 | if src.starts_with("http") { 55 | trace!(logger => "found http source '{}'", src); 56 | remote::fetch_http_source(ctx, src, &patch_dir, logger).await?; 57 | out.push(( 58 | patch.clone(), 59 | patch_dir.join(src.split('/').last().unwrap_or_default()), 60 | )); 61 | continue; 62 | } 63 | 64 | let patch_p = PathBuf::from(src); 65 | if patch_p.is_absolute() { 66 | trace!(logger => "found absolute path '{}'", patch_p.display()); 67 | out.push(( 68 | patch.clone(), 69 | patch_dir.join(patch_p.file_name().unwrap_or_default()), 70 | )); 71 | to_copy.push(patch_p); 72 | continue; 73 | } 74 | 75 | let patch_recipe_p = ctx.build.recipe.recipe_dir.join(src); 76 | trace!(logger => "using patch from recipe_dir '{}'", patch_recipe_p.display()); 77 | out.push((patch.clone(), patch_dir.join(src))); 78 | to_copy.push(patch_recipe_p); 79 | } 80 | 81 | let to_copy = to_copy.iter().map(PathBuf::as_path).collect::>(); 82 | 83 | remote::fetch_fs_source(ctx, &to_copy, &patch_dir, logger).await?; 84 | 85 | Ok(out) 86 | } 87 | -------------------------------------------------------------------------------- /pkger-core/src/build/remote.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::log::{info, trace, BoxedCollector}; 3 | use crate::proxy::ShouldProxyResult; 4 | use crate::recipe::GitSource; 5 | use crate::runtime::container::ExecOpts; 6 | use crate::template; 7 | use crate::{unix_timestamp, ErrContext, Result}; 8 | 9 | use std::path::{Path, PathBuf}; 10 | 11 | pub async fn fetch_git_source( 12 | ctx: &Context<'_>, 13 | repo: &GitSource, 14 | logger: &mut BoxedCollector, 15 | ) -> Result<()> { 16 | info!(logger => "cloning git repository to {}, url = {}, branch = {}", ctx.build.container_bld_dir.display(),repo.url(), repo.branch()); 17 | 18 | let tmp = tempdir::TempDir::new(&ctx.build.id) 19 | .context("failed to initialize temporary directory for git repo")?; 20 | let url = template::render(repo.url(), ctx.vars.inner()); 21 | 22 | tokio::task::block_in_place(|| { 23 | let mut repo_builder = git2::build::RepoBuilder::new(); 24 | 25 | let mut proxy_opts = git2::ProxyOptions::new(); 26 | 27 | match ctx.build.proxy.should_proxy(&url) { 28 | ShouldProxyResult::Http => { 29 | if let Some(url) = ctx.build.proxy.http_proxy() { 30 | proxy_opts.url(&url.to_string()); 31 | } 32 | } 33 | ShouldProxyResult::Https => { 34 | if let Some(url) = ctx.build.proxy.https_proxy() { 35 | proxy_opts.url(&url.to_string()); 36 | } 37 | } 38 | _ => {} 39 | } 40 | 41 | let mut opts = git2::FetchOptions::new(); 42 | opts.proxy_options(proxy_opts); 43 | 44 | repo_builder.branch(repo.branch()); 45 | repo_builder.fetch_options(opts); 46 | repo_builder 47 | .clone(&url, tmp.path()) 48 | .context("failed to clone git repository") 49 | })?; 50 | 51 | let tar_file = vec![]; 52 | let mut tar = tar::Builder::new(tar_file); 53 | 54 | tar.append_dir_all("./", tmp.path()) 55 | .context("failed to build tar archive of git repo")?; 56 | tar.finish()?; 57 | let tar_file = tar.into_inner()?; 58 | let tar_name = format!("git-repo-{}.tar", unix_timestamp().as_secs()); 59 | 60 | ctx.container 61 | .upload_and_extract_archive(tar_file, &ctx.build.container_bld_dir, &tar_name, logger) 62 | .await 63 | .context("failed to upload git repo") 64 | } 65 | 66 | pub async fn fetch_http_source( 67 | ctx: &Context<'_>, 68 | source: &str, 69 | dest: &Path, 70 | logger: &mut BoxedCollector, 71 | ) -> Result<()> { 72 | info!(logger => "fetching http source to {}, url = {}", dest.display(), source); 73 | 74 | ctx.checked_exec( 75 | &ExecOpts::default() 76 | .cmd(&format!("curl -LO {}", source)) 77 | .working_dir(dest), 78 | logger, 79 | ) 80 | .await 81 | .map(|_| ()) 82 | } 83 | 84 | pub async fn fetch_fs_source( 85 | ctx: &Context<'_>, 86 | files: &[&Path], 87 | dest: &Path, 88 | logger: &mut BoxedCollector, 89 | ) -> Result<()> { 90 | info!(logger => "fetching files to {}", dest.display()); 91 | 92 | let tar_file = vec![]; 93 | let mut tar = tar::Builder::new(tar_file); 94 | 95 | for path in files { 96 | if !path.exists() { 97 | return Err(anyhow!("source path '{}' doesn't exist", path.display())); 98 | } 99 | if path.is_dir() { 100 | trace!(logger => "adding entry {} to archive", path.display()); 101 | let dir_name = path.file_name().unwrap_or_default(); 102 | tar.append_dir_all(format!("./{}", dir_name.to_string_lossy()), path) 103 | .context("failed adding directory to archive")?; 104 | } else if path.is_file() { 105 | trace!(logger => "adding file {} to archive", path.display()); 106 | let mut file = 107 | std::fs::File::open(path).context("failed to open file to add to archive")?; 108 | let file_name = path.file_name().unwrap_or_default(); 109 | tar.append_file(&format!("./{}", file_name.to_string_lossy()), &mut file) 110 | .context("failed adding file to archive")?; 111 | } 112 | } 113 | 114 | tar.finish()?; 115 | let tar_file = tar.into_inner()?; 116 | 117 | let tar_name = format!("fs-source-{}.tar", unix_timestamp().as_secs()); 118 | 119 | ctx.container 120 | .upload_and_extract_archive(tar_file, dest, &tar_name, logger) 121 | .await 122 | } 123 | 124 | pub async fn fetch_source(ctx: &Context<'_>, logger: &mut BoxedCollector) -> Result<()> { 125 | if let Some(repo) = &ctx.build.recipe.metadata.git { 126 | fetch_git_source(ctx, repo, logger).await?; 127 | } else if !ctx.build.recipe.metadata.source.is_empty() { 128 | for source in &ctx.build.recipe.metadata.source { 129 | if source.starts_with("http") { 130 | fetch_http_source(ctx, source, &ctx.build.container_tmp_dir, logger).await?; 131 | } else { 132 | let p = PathBuf::from(source); 133 | let source = if p.is_absolute() { 134 | p 135 | } else { 136 | ctx.build 137 | .recipe_dir 138 | .join(&ctx.build.recipe.metadata.name) 139 | .join(template::render(source, ctx.vars.inner())) 140 | }; 141 | fetch_fs_source( 142 | ctx, 143 | &[source.as_path()], 144 | &ctx.build.container_tmp_dir, 145 | logger, 146 | ) 147 | .await?; 148 | } 149 | } 150 | ctx.checked_exec( 151 | &ExecOpts::default() 152 | .cmd(&format!( 153 | r#" 154 | for file in *; 155 | do 156 | if [[ $file =~ (.*[.]tar.*|.*[.](tgz|tbz|txz|tlz|tsz|taz|tz)) ]] 157 | then 158 | tar xvf $file -C {0} 159 | elif [[ $file == *.zip ]] 160 | then 161 | unzip $file -d {0} 162 | else 163 | cp -rv $file {0} 164 | fi 165 | done"#, 166 | ctx.build.container_bld_dir.display(), 167 | )) 168 | .working_dir(&ctx.build.container_tmp_dir) 169 | .shell("/bin/bash"), 170 | logger, 171 | ) 172 | .await?; 173 | } else { 174 | trace!(logger => "no sources to fetch"); 175 | } 176 | Ok(()) 177 | } 178 | -------------------------------------------------------------------------------- /pkger-core/src/build/scripts.rs: -------------------------------------------------------------------------------- 1 | use crate::build::container::Context; 2 | use crate::log::{debug, info, trace, BoxedCollector}; 3 | use crate::runtime::container::ExecOpts; 4 | use crate::template; 5 | use crate::{Error, Result}; 6 | 7 | use std::path::PathBuf; 8 | 9 | macro_rules! run_script { 10 | ($phase:literal, $script:expr, $dir:expr, $ctx:ident, $logger:ident) => {{ 11 | info!($logger => "running script for {} phase", $phase); 12 | trace!($logger => "{:?}", $script); 13 | info!($logger => concat!("executing ", $phase, " scripts")); 14 | let mut opts = ExecOpts::default(); 15 | let mut _dir; 16 | 17 | if let Some(dir) = &$script.working_dir { 18 | _dir = PathBuf::from(template::render(dir.to_string_lossy(), $ctx.vars.inner())); 19 | trace!($logger => "Working directory: {}", _dir.display()); 20 | opts = opts.working_dir(&_dir); 21 | } else { 22 | trace!($logger => "Working directory: {} (Default)", $dir.display()); 23 | opts = opts.working_dir($dir); 24 | } 25 | 26 | if let Some(shell) = &$script.shell { 27 | trace!($logger => "Shell: {}", shell); 28 | opts = opts.shell(shell.as_str()); 29 | } 30 | 31 | for cmd in &$script.steps { 32 | debug!($logger => "Processing: {:?}", cmd); 33 | if let Some(images) = &cmd.images { 34 | trace!($logger => "only execute on {:?}", images); 35 | if !images.contains(&$ctx.build.target.image().to_owned()) { 36 | trace!($logger => "'{}' not found in images", $ctx.build.target.image()); 37 | if !cmd.has_target_specified() { 38 | debug!($logger => "skipping command, excluded by image filter"); 39 | continue; 40 | } 41 | } 42 | } 43 | 44 | let target = $ctx.build.target.build_target(); 45 | if !cmd.should_run_on_target(target) { 46 | trace!($logger => "skipping command, shouldn't run on target {:?}", target); 47 | continue; 48 | } 49 | 50 | if !cmd.should_run_on_version(&$ctx.build.build_version) { 51 | trace!($logger => "skipping command, shouldn't run on version {}", $ctx.build.build_version); 52 | continue; 53 | } 54 | 55 | info!($logger => "running command {:?}", cmd); 56 | $ctx.checked_exec(&opts.clone().cmd(&cmd.cmd), $logger) 57 | .await?; 58 | } 59 | 60 | Ok::<_, Error>(()) 61 | }}; 62 | } 63 | 64 | pub async fn run(ctx: &Context<'_>, logger: &mut BoxedCollector) -> Result<()> { 65 | info!(logger => "executing scripts"); 66 | if let Some(config_script) = &ctx.build.recipe.configure_script { 67 | run_script!( 68 | "configure", 69 | config_script, 70 | &ctx.build.container_bld_dir, 71 | ctx, 72 | logger 73 | )?; 74 | } else { 75 | info!(logger => "no configure steps to run"); 76 | } 77 | 78 | let build_script = &ctx.build.recipe.build_script; 79 | run_script!( 80 | "build", 81 | build_script, 82 | &ctx.build.container_bld_dir, 83 | ctx, 84 | logger 85 | )?; 86 | 87 | if let Some(install_script) = &ctx.build.recipe.install_script { 88 | run_script!( 89 | "install", 90 | install_script, 91 | &ctx.build.container_out_dir, 92 | ctx, 93 | logger 94 | )?; 95 | } else { 96 | info!(logger => "no install steps to run"); 97 | } 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /pkger-core/src/gpg.rs: -------------------------------------------------------------------------------- 1 | use crate::{err, Error, Result}; 2 | 3 | use std::path::{Path, PathBuf}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct GpgKey { 7 | path: PathBuf, 8 | name: String, 9 | pass: String, 10 | } 11 | 12 | impl GpgKey { 13 | /// Returns a `GpgKey` if the key exists on the filesystem, otherwise 14 | /// returns an error. 15 | pub fn new(path: &Path, name: &str, pass: &str) -> Result { 16 | if !path.exists() { 17 | return err!("gpg key does not exist in `{}`", path.display()); 18 | } 19 | Ok(Self { 20 | path: path.to_path_buf(), 21 | name: name.to_owned(), 22 | pass: pass.to_owned(), 23 | }) 24 | } 25 | 26 | pub fn name(&self) -> &str { 27 | &self.name 28 | } 29 | 30 | pub fn pass(&self) -> &str { 31 | &self.pass 32 | } 33 | 34 | pub fn path(&self) -> &PathBuf { 35 | &self.path 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkger-core/src/image/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod os; 2 | pub mod state; 3 | 4 | use anyhow::Context; 5 | pub use os::find; 6 | pub use state::{ImageState, ImagesState}; 7 | 8 | use crate::recipe::{BuildTarget, BuildTargetInfo, Os}; 9 | use crate::{err, Error, Result}; 10 | 11 | use std::convert::AsRef; 12 | use std::fs; 13 | use std::path::{Path, PathBuf}; 14 | 15 | #[derive(Clone, Debug)] 16 | /// A representation of an image on the filesystem 17 | pub struct Image { 18 | pub name: String, 19 | pub path: PathBuf, 20 | } 21 | 22 | impl Image { 23 | pub fn new(name: String, path: PathBuf) -> Self { 24 | Self { name, path } 25 | } 26 | 27 | pub fn simple(target: BuildTarget) -> BuildTargetInfo { 28 | match target { 29 | BuildTarget::Rpm => ( 30 | "docker.io/rockylinux/rockylinux:latest", 31 | "pkger-rpm", 32 | Os::new("Rocky", None::<&str>), 33 | ), 34 | BuildTarget::Deb => ( 35 | "debian:latest", 36 | "pkger-deb", 37 | Os::new("Debian", None::<&str>), 38 | ), 39 | BuildTarget::Pkg => ("archlinux", "pkger-pkg", Os::new("Arch", None::<&str>)), 40 | BuildTarget::Gzip => ( 41 | "debian:latest", 42 | "pkger-gzip", 43 | Os::new("Debian", None::<&str>), 44 | ), 45 | BuildTarget::Apk => ( 46 | "alpine:latest", 47 | "pkger-apk", 48 | Os::new("Alpine", None::<&str>), 49 | ), 50 | } 51 | .into() 52 | } 53 | 54 | pub fn create_simple( 55 | images_dir: &Path, 56 | target: BuildTarget, 57 | custom_image: Option<&str>, 58 | ) -> Result { 59 | let BuildTargetInfo { image, name, os: _ } = Self::simple(target); 60 | let image = custom_image.unwrap_or(image); 61 | 62 | let image_dir = images_dir.join(name); 63 | fs::create_dir_all(&image_dir)?; 64 | 65 | let dockerfile = format!("FROM {}", image); 66 | fs::write(image_dir.join("Dockerfile"), dockerfile.as_bytes())?; 67 | 68 | Image::try_from_path(image_dir) 69 | } 70 | 71 | pub fn try_get_or_new_simple( 72 | images_dir: &Path, 73 | target: BuildTarget, 74 | custom_image: Option<&str>, 75 | ) -> Result<(Image, Os)> { 76 | let BuildTargetInfo { image: _, name, os } = Self::simple(target); 77 | 78 | let image_dir = images_dir.join(name); 79 | if image_dir.exists() { 80 | return Image::try_from_path(image_dir).map(|i| (i, os)); 81 | } 82 | 83 | Self::create_simple(images_dir, target, custom_image).map(|i| (i, os)) 84 | } 85 | 86 | /// Loads an `FsImage` from the given `path` 87 | pub fn try_from_path>(path: P) -> Result { 88 | let path = path.as_ref().to_path_buf(); 89 | if !path.join("Dockerfile").exists() { 90 | return err!("Dockerfile missing from image `{}`", path.display()); 91 | } 92 | Ok(Image { 93 | // we can unwrap here because we know the Dockerfile exists 94 | name: path.file_name().unwrap().to_string_lossy().to_string(), 95 | path, 96 | }) 97 | } 98 | 99 | pub fn load_dockerfile(&self) -> Result { 100 | fs::read_to_string(self.path.join("Dockerfile")) 101 | .context("failed to read a Dockerfile of image") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkger-core/src/image/os.rs: -------------------------------------------------------------------------------- 1 | use crate::log::{info, trace, BoxedCollector}; 2 | use crate::oneshot::{self, OneShotCtx}; 3 | use crate::recipe::Os; 4 | use crate::runtime::container::CreateOpts; 5 | use crate::runtime::RuntimeConnector; 6 | use crate::{err, ErrContext, Error, Result}; 7 | 8 | /// Finds out the operating system and version of the image with id `image_id` 9 | pub async fn find( 10 | image_id: &str, 11 | runtime: &RuntimeConnector, 12 | logger: &mut BoxedCollector, 13 | ) -> Result { 14 | info!(logger => "finding os of image {}", image_id); 15 | 16 | macro_rules! return_if_ok { 17 | ($check:expr) => { 18 | match $check 19 | .await 20 | { 21 | Ok(os) => return Ok(os), 22 | Err(e) => trace!(logger => "{:?}", e), 23 | } 24 | }; 25 | } 26 | 27 | return_if_ok!(from_osrelease(image_id, runtime, logger)); 28 | return_if_ok!(from_issue(image_id, runtime, logger)); 29 | return_if_ok!(from_rhrelease(image_id, runtime, logger)); 30 | 31 | err!("failed to determine distribution") 32 | } 33 | 34 | async fn from_osrelease( 35 | image_id: &str, 36 | runtime: &RuntimeConnector, 37 | logger: &mut BoxedCollector, 38 | ) -> Result { 39 | let out = oneshot::run( 40 | &OneShotCtx::new( 41 | runtime, 42 | &CreateOpts::new(image_id).cmd(vec!["cat", "/etc/os-release"]), 43 | true, 44 | true, 45 | ), 46 | logger, 47 | ) 48 | .await?; 49 | 50 | trace!(logger => "stderr: {}", String::from_utf8_lossy(&out.stderr)); 51 | 52 | let out = String::from_utf8_lossy(&out.stdout); 53 | trace!(logger => "stdout: {}", out); 54 | 55 | fn extract_key(out: &str, key: &str) -> Option { 56 | let key = [key, "="].join(""); 57 | if let Some(line) = out.lines().find(|line| line.starts_with(&key)) { 58 | let line = line.strip_prefix(&key).unwrap(); 59 | if line.starts_with('"') { 60 | return Some(line.trim_matches('"').to_string()); 61 | } 62 | return Some(line.to_string()); 63 | } 64 | None 65 | } 66 | 67 | let os_name = extract_key(&out, "ID"); 68 | let version = extract_key(&out, "VERSION_ID"); 69 | let os = Os::new(os_name.context("os name is missing")?, version); 70 | if os.is_unknown() { 71 | return err!("unknown os"); 72 | } 73 | Ok(os) 74 | } 75 | 76 | fn extract_version(text: &str) -> Option { 77 | let mut chars = text.chars(); 78 | if let Some(idx) = chars.position(|c| c.is_numeric()) { 79 | let mut end_idx = idx; 80 | for ch in chars { 81 | let is_valid = ch.is_numeric() || ch == '.' || ch == '-'; 82 | if !is_valid { 83 | break; 84 | } 85 | end_idx += 1; 86 | } 87 | Some(text[idx..=end_idx].to_string()) 88 | } else { 89 | None 90 | } 91 | } 92 | 93 | async fn os_from( 94 | image_id: &str, 95 | runtime: &RuntimeConnector, 96 | file: &str, 97 | logger: &mut BoxedCollector, 98 | ) -> Result { 99 | let out = oneshot::run( 100 | &OneShotCtx::new( 101 | runtime, 102 | &CreateOpts::new(image_id).cmd(vec!["cat", file]), 103 | true, 104 | true, 105 | ), 106 | logger, 107 | ) 108 | .await?; 109 | 110 | trace!(logger => "stderr: {}", String::from_utf8_lossy(&out.stderr)); 111 | 112 | let out = String::from_utf8_lossy(&out.stdout); 113 | trace!(logger => "stdout: {}", out); 114 | 115 | let os_version = extract_version(&out); 116 | 117 | let os = Os::new(out, os_version); 118 | if os.is_unknown() { 119 | return err!("unknown os"); 120 | } 121 | Ok(os) 122 | } 123 | 124 | async fn from_rhrelease( 125 | image_id: &str, 126 | runtime: &RuntimeConnector, 127 | logger: &mut BoxedCollector, 128 | ) -> Result { 129 | os_from(image_id, runtime, "/etc/redhat-release", logger).await 130 | } 131 | 132 | async fn from_issue( 133 | image_id: &str, 134 | runtime: &RuntimeConnector, 135 | logger: &mut BoxedCollector, 136 | ) -> Result { 137 | os_from(image_id, runtime, "/etc/issue", logger).await 138 | } 139 | -------------------------------------------------------------------------------- /pkger-core/src/image/state.rs: -------------------------------------------------------------------------------- 1 | use crate::image::find; 2 | 3 | use crate::log::{debug, info, trace, BoxedCollector}; 4 | use crate::recipe::{Os, RecipeTarget}; 5 | use crate::runtime::RuntimeConnector; 6 | use crate::{ErrContext, Result}; 7 | 8 | use std::collections::{HashMap, HashSet}; 9 | use std::convert::AsRef; 10 | use std::fs; 11 | use std::path::{Path, PathBuf}; 12 | use std::time::{SystemTime, UNIX_EPOCH}; 13 | 14 | use serde::{Deserialize, Serialize}; 15 | 16 | pub static DEFAULT_STATE_FILE: &str = ".pkger.state"; 17 | 18 | #[derive(Deserialize, Clone, Debug, Serialize)] 19 | /// Saved state of an image that contains all the metadata of the image 20 | pub struct ImageState { 21 | pub id: String, 22 | pub image: String, 23 | pub tag: String, 24 | pub os: Os, 25 | pub timestamp: SystemTime, 26 | pub deps: HashSet, 27 | pub simple: bool, 28 | } 29 | 30 | impl PartialEq for ImageState { 31 | fn eq(&self, other: &Self) -> bool { 32 | self.id == other.id 33 | && self.image == other.image 34 | && self.tag == other.tag 35 | && self.os == other.os 36 | && self.timestamp == other.timestamp 37 | && self.deps == other.deps 38 | && self.simple == other.simple 39 | } 40 | } 41 | 42 | impl ImageState { 43 | #[allow(clippy::too_many_arguments)] 44 | pub async fn new( 45 | id: &str, 46 | target: &RecipeTarget, 47 | tag: &str, 48 | timestamp: &SystemTime, 49 | runtime: &RuntimeConnector, 50 | deps: &HashSet<&str>, 51 | simple: bool, 52 | logger: &mut BoxedCollector, 53 | ) -> Result { 54 | let name = format!( 55 | "{}-{}", 56 | target.image(), 57 | timestamp 58 | .duration_since(UNIX_EPOCH) 59 | .unwrap_or_default() 60 | .as_secs() 61 | ); 62 | info!(logger => "creating image state for {}, id: {}, tag: {}", name, id, tag); 63 | let os = if let Some(os) = target.image_os() { 64 | os.clone() 65 | } else { 66 | find(id, runtime, logger).await? 67 | }; 68 | debug!(logger => "parsed image info: {:?}", os); 69 | 70 | Ok(ImageState { 71 | id: id.to_string(), 72 | image: target.image().to_string(), 73 | os, 74 | tag: tag.to_string(), 75 | timestamp: *timestamp, 76 | deps: deps.iter().map(|s| s.to_string()).collect(), 77 | simple, 78 | }) 79 | } 80 | 81 | /// Verifies if a given image exists in docker, on connection error returns false 82 | pub async fn exists(&self, runtime: &RuntimeConnector, logger: &mut BoxedCollector) -> bool { 83 | info!(logger => "checking if image '{}' exists", self.image); 84 | match runtime { 85 | RuntimeConnector::Docker(docker) => { 86 | docker.images().get(&self.id).inspect().await.is_ok() 87 | } 88 | RuntimeConnector::Podman(podman) => { 89 | podman.images().get(&*self.id).inspect().await.is_ok() 90 | } 91 | } 92 | } 93 | } 94 | 95 | //#################################################################################################### 96 | 97 | #[derive(Deserialize, Debug, Serialize)] 98 | pub struct ImagesState { 99 | /// Contains historical build data of images. Each key-value pair contains an image name and 100 | /// [ImageState](ImageState) struct representing the state of the image. 101 | pub images: HashMap, 102 | /// Path to a file containing image state 103 | path: PathBuf, 104 | #[serde(skip_serializing)] 105 | #[serde(default)] 106 | has_changed: bool, 107 | } 108 | 109 | impl Default for ImagesState { 110 | fn default() -> Self { 111 | ImagesState::new(DEFAULT_STATE_FILE) 112 | } 113 | } 114 | 115 | impl ImagesState { 116 | pub fn new>(path: P) -> Self { 117 | Self { 118 | images: HashMap::new(), 119 | path: path.into(), 120 | has_changed: false, 121 | } 122 | } 123 | 124 | /// Tries to initialize images state from the given path, if the path doesn't exist creates 125 | /// a new ImagesState. 126 | pub fn load>(path: P) -> Result { 127 | let state_file = path.as_ref(); 128 | if !state_file.exists() { 129 | debug!("state file doesn't exist"); 130 | return Ok(ImagesState::new(state_file)); 131 | } 132 | debug!("loading state"); 133 | let contents = 134 | fs::read(state_file).context("failed to read images state file from the filesystem")?; 135 | let state = 136 | serde_cbor::from_slice(&contents).context("failed to deserialize images state")?; 137 | 138 | Ok(state) 139 | } 140 | 141 | /// Updates the target image with a new state. 142 | pub fn update(&mut self, target: RecipeTarget, state: ImageState) { 143 | if let Some(old_state) = self.images.get(&target) { 144 | if old_state != &state { 145 | self.has_changed = true 146 | } 147 | } 148 | self.images.insert(target, state); 149 | } 150 | 151 | /// Saves the images state to the filesystem. 152 | pub fn save(&self) -> Result<()> { 153 | trace!("saving images state"); 154 | serde_cbor::to_vec(&self) 155 | .context("failed to serialize image state") 156 | .and_then(|d| fs::write(&self.path, d).context("failed to save state file")) 157 | } 158 | 159 | /// Returns the location from which this state was initialized. 160 | pub fn locations(&self) -> &Path { 161 | &self.path 162 | } 163 | 164 | /// Clears the state to contain no images. 165 | pub fn clear(&mut self) { 166 | self.images.clear(); 167 | } 168 | 169 | /// Returns true if the state was updated. 170 | pub fn has_changed(&self) -> bool { 171 | self.has_changed 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pkger-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate anyhow; 3 | 4 | #[macro_use] 5 | extern crate lazy_static; 6 | 7 | pub mod archive; 8 | pub mod build; 9 | pub mod gpg; 10 | pub mod image; 11 | #[macro_export] 12 | pub mod log; 13 | pub mod oneshot; 14 | pub mod proxy; 15 | pub mod recipe; 16 | pub mod runtime; 17 | pub mod ssh; 18 | pub mod template; 19 | 20 | pub use anyhow::{anyhow, Context as ErrContext, Error, Result}; 21 | 22 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 23 | 24 | pub fn unix_timestamp() -> Duration { 25 | SystemTime::now() 26 | .duration_since(UNIX_EPOCH) 27 | .unwrap_or_default() 28 | } 29 | 30 | #[macro_export] 31 | macro_rules! err { 32 | ($it:ident) => { 33 | Err(Error::msg($it)) 34 | }; 35 | ($lit:literal) => { 36 | Err(Error::msg($lit)) 37 | }; 38 | ($($tt:tt)*) => { 39 | Err(Error::msg(format!($($tt)*))) 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /pkger-core/src/oneshot.rs: -------------------------------------------------------------------------------- 1 | use crate::log::BoxedCollector; 2 | use crate::runtime::container::{Container, CreateOpts, Output}; 3 | use crate::runtime::{DockerContainer, PodmanContainer, RuntimeConnector}; 4 | use crate::Result; 5 | 6 | use std::time::SystemTime; 7 | 8 | #[derive(Debug)] 9 | /// Simple job that spawns a container with a command to execute and returns its stdout and/or 10 | /// stderr. 11 | pub struct OneShotCtx<'job> { 12 | id: String, 13 | runtime: &'job RuntimeConnector, 14 | opts: &'job CreateOpts, 15 | stdout: bool, 16 | stderr: bool, 17 | } 18 | 19 | pub async fn run(ctx: &OneShotCtx<'_>, logger: &mut BoxedCollector) -> Result> { 20 | match ctx.runtime { 21 | RuntimeConnector::Docker(docker) => { 22 | let mut container = DockerContainer::new(docker.clone()); 23 | container.spawn(ctx.opts, logger).await?; 24 | 25 | container.logs(ctx.stdout, ctx.stderr, logger).await 26 | } 27 | RuntimeConnector::Podman(podman) => { 28 | let mut container = PodmanContainer::new(podman.clone()); 29 | container.spawn(ctx.opts, logger).await?; 30 | 31 | container.logs(ctx.stdout, ctx.stderr, logger).await 32 | } 33 | } 34 | } 35 | 36 | impl<'job> OneShotCtx<'job> { 37 | pub fn new( 38 | runtime: &'job RuntimeConnector, 39 | opts: &'job CreateOpts, 40 | stdout: bool, 41 | stderr: bool, 42 | ) -> Self { 43 | let id = format!( 44 | "pkger-oneshot-{}", 45 | SystemTime::now() 46 | .duration_since(SystemTime::UNIX_EPOCH) 47 | .unwrap_or_default() 48 | .as_secs() 49 | ); 50 | 51 | Self { 52 | id, 53 | runtime, 54 | opts, 55 | stdout, 56 | stderr, 57 | } 58 | } 59 | 60 | pub fn id(&self) -> &str { 61 | self.id.as_str() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/cmd.rs: -------------------------------------------------------------------------------- 1 | use crate::recipe::BuildTarget; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] 6 | /// Wrapper type for steps parsed from a recipe. Can be either a simple string or a map specifying 7 | /// other parameters. 8 | /// 9 | /// Examples: 10 | /// "echo 123" 11 | /// 12 | /// { cmd = "echo 123", images = ["rocky", "debian"] } 13 | /// 14 | /// { cmd = "echo 321", rpm = true } # execute only when building rpm target 15 | pub struct Command { 16 | pub cmd: String, 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub images: Option>, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub versions: Option>, 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub rpm: Option, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub deb: Option, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub pkg: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub gzip: Option, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub apk: Option, 31 | } 32 | 33 | impl From<&str> for Command { 34 | fn from(s: &str) -> Self { 35 | Self { 36 | cmd: s.to_string(), 37 | images: None, 38 | versions: None, 39 | rpm: None, 40 | deb: None, 41 | pkg: None, 42 | gzip: None, 43 | apk: None, 44 | } 45 | } 46 | } 47 | 48 | impl Command { 49 | pub fn has_target_specified(&self) -> bool { 50 | self.rpm.is_some() || self.deb.is_some() || self.pkg.is_some() || self.gzip.is_some() 51 | } 52 | 53 | pub fn should_run_on_target(&self, target: &BuildTarget) -> bool { 54 | if !self.has_target_specified() { 55 | return true; 56 | } 57 | match &target { 58 | BuildTarget::Rpm => self.rpm, 59 | BuildTarget::Deb => self.deb, 60 | BuildTarget::Pkg => self.pkg, 61 | BuildTarget::Gzip => self.gzip, 62 | BuildTarget::Apk => self.apk, 63 | } 64 | .unwrap_or_default() 65 | } 66 | 67 | pub fn should_run_on_version(&self, version: impl AsRef) -> bool { 68 | match &self.versions { 69 | None => true, 70 | Some(versions) => versions.iter().any(|v| v.as_str() == version.as_ref()), 71 | } 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | #[test] 79 | fn should_run_on_works() { 80 | let mut cmd = Command::from("echo 123"); 81 | assert!(cmd.should_run_on_target(&BuildTarget::Deb)); 82 | assert!(cmd.should_run_on_target(&BuildTarget::Rpm)); 83 | assert!(cmd.should_run_on_target(&BuildTarget::Pkg)); 84 | assert!(cmd.should_run_on_target(&BuildTarget::Gzip)); 85 | assert!(cmd.should_run_on_target(&BuildTarget::Apk)); 86 | cmd.rpm = Some(true); 87 | assert!(cmd.should_run_on_target(&BuildTarget::Rpm)); 88 | assert!(!cmd.should_run_on_target(&BuildTarget::Gzip)); 89 | assert!(!cmd.should_run_on_target(&BuildTarget::Pkg)); 90 | assert!(!cmd.should_run_on_target(&BuildTarget::Deb)); 91 | assert!(!cmd.should_run_on_target(&BuildTarget::Apk)); 92 | cmd.deb = Some(true); 93 | cmd.pkg = Some(true); 94 | cmd.gzip = Some(true); 95 | cmd.apk = Some(true); 96 | assert!(cmd.should_run_on_target(&BuildTarget::Rpm)); 97 | assert!(cmd.should_run_on_target(&BuildTarget::Gzip)); 98 | assert!(cmd.should_run_on_target(&BuildTarget::Pkg)); 99 | assert!(cmd.should_run_on_target(&BuildTarget::Deb)); 100 | assert!(cmd.should_run_on_target(&BuildTarget::Apk)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/envs.rs: -------------------------------------------------------------------------------- 1 | use serde_yaml::Mapping; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Clone, Default, Debug, PartialEq, Eq)] 5 | pub struct Env(HashMap); 6 | 7 | impl From> for Env { 8 | fn from(env: Option) -> Self { 9 | let mut data = HashMap::new(); 10 | 11 | if let Some(env) = env { 12 | env.into_iter() 13 | .filter(|(k, v)| k.is_string() && v.is_string()) 14 | .for_each(|(k, v)| { 15 | data.insert( 16 | k.as_str().unwrap().to_string(), 17 | v.as_str().unwrap().to_string(), 18 | ); 19 | }); 20 | } 21 | 22 | Env(data) 23 | } 24 | } 25 | 26 | impl Env { 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | 31 | pub fn insert(&mut self, key: K, value: V) -> Option 32 | where 33 | K: Into, 34 | V: Into, 35 | { 36 | self.0.insert(key.into(), value.into()) 37 | } 38 | 39 | pub fn is_empty(&self) -> bool { 40 | self.0.is_empty() 41 | } 42 | 43 | pub fn remove(&mut self, key: K) -> Option 44 | where 45 | K: AsRef, 46 | { 47 | self.0.remove(key.as_ref()) 48 | } 49 | 50 | pub fn kv_vec(self) -> Vec { 51 | self.0 52 | .into_iter() 53 | .map(|(k, v)| format!("{}={}", k, v)) 54 | .collect() 55 | } 56 | 57 | pub fn iter(&self) -> impl Iterator { 58 | self.0.iter() 59 | } 60 | 61 | pub fn inner(&self) -> &HashMap { 62 | &self.0 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | #[test] 70 | fn renders_entries_as_vars() { 71 | let mut env = Env::new(); 72 | assert!(env.is_empty()); 73 | 74 | env.insert("key", "val"); 75 | env.insert("second", "val2"); 76 | 77 | let envs = env.clone().kv_vec(); 78 | 79 | assert!(envs.contains(&"key=val".to_string())); 80 | assert!(envs.contains(&"second=val2".to_string())); 81 | 82 | env.remove("key"); 83 | env.remove("second"); 84 | assert!(env.is_empty()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::log::{debug, trace, warning, BoxedCollector}; 2 | use crate::recipe::{Recipe, RecipeRep}; 3 | use crate::{err, ErrContext, Error, Result}; 4 | 5 | use std::fs; 6 | use std::path::{Path, PathBuf}; 7 | 8 | #[derive(Clone, Debug, Default)] 9 | pub struct Loader { 10 | path: PathBuf, 11 | } 12 | 13 | impl Loader { 14 | /// Initializes a recipe loader without loading any recipes. The provided `path` must be a directory 15 | pub fn new>(path: P) -> Result { 16 | let path = path.as_ref(); 17 | let metadata = fs::metadata(path) 18 | .context(format!("failed to verify recipe path `{}`", path.display()))?; 19 | 20 | if !metadata.is_dir() { 21 | return err!("recipes path is not a directory"); 22 | } 23 | 24 | Ok(Loader { 25 | path: path.to_path_buf(), 26 | }) 27 | } 28 | 29 | pub fn load_rep(&self, recipe: &str) -> Result { 30 | let base_path = self.path.join(recipe); 31 | let mut path = base_path.join("recipe.yml"); 32 | if !path.exists() { 33 | path = base_path.join("recipe.yaml"); 34 | } 35 | RecipeRep::load(path) 36 | } 37 | 38 | pub fn load(&self, recipe: &str) -> Result { 39 | let base_path = self.path.join(recipe); 40 | self.load_rep(recipe) 41 | .and_then(|rep| Recipe::new(rep, base_path)) 42 | } 43 | 44 | pub fn list(&self) -> Result> { 45 | fs::read_dir(&self.path) 46 | .map(|entries| { 47 | entries 48 | .filter_map(|entry| { 49 | entry 50 | .ok() 51 | .filter(|e| e.file_type().map(|e| e.is_dir()).unwrap_or(false)) 52 | .map(|e| e.file_name().to_string_lossy().to_string()) 53 | }) 54 | .collect() 55 | }) 56 | .context("failed to list recipes") 57 | } 58 | 59 | /// Loads all recipes in the underlying directory 60 | pub fn load_all(&self, logger: &mut BoxedCollector) -> Result> { 61 | let path = self.path.as_path(); 62 | 63 | debug!(logger => "loading reicipes from '{}'", path.display()); 64 | 65 | let mut recipes = Vec::new(); 66 | 67 | for entry in fs::read_dir(path)? { 68 | match entry { 69 | Ok(entry) => { 70 | let filename = entry.file_name().to_string_lossy().to_string(); 71 | let path = entry.path(); 72 | match RecipeRep::try_from(entry).map(|rep| Recipe::new(rep, path)) { 73 | Ok(result) => { 74 | let recipe = result?; 75 | trace!(logger => "{:?}", recipe); 76 | recipes.push(recipe); 77 | } 78 | Err(e) => { 79 | warning!(logger => "failed to read recipe from '{}', reason: {:?}", filename, e); 80 | } 81 | } 82 | } 83 | Err(e) => warning!(logger => "invalid entry, reason: {:?}", e), 84 | } 85 | } 86 | 87 | Ok(recipes) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/arch.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Formatter; 3 | 4 | #[allow(non_camel_case_types)] 5 | #[derive(Clone, Debug, PartialEq, Eq)] 6 | pub enum BuildArch { 7 | All, 8 | x86_64, 9 | x86, 10 | Arm, 11 | Armv6h, 12 | Armv7h, 13 | Arm64, 14 | Other(String), 15 | } 16 | 17 | impl From<&str> for BuildArch { 18 | fn from(s: &str) -> Self { 19 | match &s.to_lowercase()[..] { 20 | "all" | "any" | "noarch" => Self::All, 21 | "x86_64" | "amd64" => Self::x86_64, 22 | "i386" | "x86" => Self::x86, 23 | "armel" | "arm" => Self::Arm, 24 | "armv6hl" | "armv6h" => Self::Armv6h, 25 | "armv7hl" | "armv7h" | "armhf" => Self::Armv7h, 26 | "aarch64" | "arm64" => Self::Arm64, 27 | arch => Self::Other(arch.to_string()), 28 | } 29 | } 30 | } 31 | 32 | impl AsRef for BuildArch { 33 | fn as_ref(&self) -> &str { 34 | use BuildArch::*; 35 | match self { 36 | All => "all", 37 | x86_64 => "x86_64", 38 | x86 => "x86", 39 | Arm => "arm", 40 | Armv6h => "armv6h", 41 | Armv7h => "armv7h", 42 | Arm64 => "aarch64", 43 | Other(arch) => arch.as_str(), 44 | } 45 | } 46 | } 47 | 48 | impl fmt::Display for BuildArch { 49 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 50 | write!(f, "{}", self.as_ref()) 51 | } 52 | } 53 | 54 | impl BuildArch { 55 | pub fn deb_name(&self) -> &str { 56 | use BuildArch::*; 57 | match &self { 58 | All => "all", 59 | x86_64 => "amd64", 60 | x86 => "i386", 61 | Arm => "armel", 62 | Armv6h => "armhf", 63 | Armv7h => "armhf", 64 | Arm64 => "arm64", 65 | Other(arch) => arch, 66 | } 67 | } 68 | 69 | pub fn rpm_name(&self) -> &str { 70 | use BuildArch::*; 71 | match &self { 72 | All => "noarch", 73 | x86_64 => "x86_64", 74 | x86 => "i386", 75 | Arm => "armel", 76 | Armv6h => "armv6hl", 77 | Armv7h => "armv7hl", 78 | Arm64 => "aarch64", 79 | Other(arch) => arch, 80 | } 81 | } 82 | 83 | pub fn pkg_name(&self) -> &str { 84 | use BuildArch::*; 85 | match &self { 86 | All => "any", 87 | x86_64 => "x86_64", 88 | x86 => "i386", 89 | Arm => "arm", 90 | Armv6h => "armv6h", 91 | Armv7h => "armv7h", 92 | Arm64 => "aarch64", 93 | Other(arch) => arch, 94 | } 95 | } 96 | 97 | pub fn apk_name(&self) -> &str { 98 | use BuildArch::*; 99 | match &self { 100 | All => "all", 101 | x86_64 => "x86_64", 102 | x86 => "x86", 103 | Arm => "armhf", 104 | Armv6h => "armhf", 105 | Armv7h => "armv7", 106 | Arm64 => "aarch64", 107 | Other(arch) => arch, 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/deps.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | 3 | use anyhow::Context; 4 | use serde_yaml::{Mapping, Sequence, Value as YamlValue}; 5 | use std::collections::{HashMap, HashSet}; 6 | use std::convert::TryFrom; 7 | 8 | pub static COMMON_DEPS_KEY: &str = "all"; 9 | 10 | type DepsMap = HashMap>; 11 | 12 | #[derive(Clone, Debug, PartialEq, Eq)] 13 | pub struct Dependencies { 14 | inner: DepsMap, 15 | } 16 | 17 | impl Default for Dependencies { 18 | fn default() -> Self { 19 | let mut deps = Self { 20 | inner: HashMap::new(), 21 | }; 22 | 23 | // ensure the COMMON_DEPS_KEY entry is created by default 24 | deps.inner 25 | .insert(COMMON_DEPS_KEY.to_string(), HashSet::new()); 26 | deps 27 | } 28 | } 29 | 30 | impl TryFrom for Dependencies { 31 | type Error = crate::Error; 32 | 33 | fn try_from(table: Mapping) -> Result { 34 | let mut deps = Self::default(); 35 | for (image, image_deps) in table { 36 | if image_deps.is_sequence() { 37 | let mut deps_set = HashSet::new(); 38 | for dep in image_deps.as_sequence().unwrap() { 39 | if !dep.is_string() { 40 | return Err(anyhow!( 41 | "expected a string as dependency, found `{:?}`", 42 | dep 43 | )); 44 | } 45 | 46 | deps_set.insert(dep.as_str().unwrap().to_string()); 47 | } 48 | let image = image 49 | .as_str() 50 | .map(|s| s.to_string()) 51 | .context("expected image name")?; 52 | if image.contains('+') { 53 | for image in image.split('+') { 54 | deps.update_or_insert(image.to_string(), &deps_set); 55 | } 56 | } else { 57 | deps.update_or_insert(image.to_string(), &deps_set); 58 | } 59 | } else { 60 | return Err(anyhow!( 61 | "expected array of dependencies, found `{:?}`", 62 | image_deps 63 | )); 64 | } 65 | } 66 | Ok(deps) 67 | } 68 | } 69 | 70 | impl TryFrom for Dependencies { 71 | type Error = crate::Error; 72 | fn try_from(array: Sequence) -> Result { 73 | let mut deps = Self::default(); 74 | let mut dep_set = HashSet::new(); 75 | for dep in array { 76 | if let YamlValue::String(dep) = dep { 77 | dep_set.insert(dep); 78 | } else { 79 | return Err(anyhow!( 80 | "expected a string as dependency name, found `{:?}`", 81 | dep 82 | )); 83 | } 84 | } 85 | deps.inner_mut() 86 | .insert(COMMON_DEPS_KEY.to_string(), dep_set); 87 | 88 | Ok(deps) 89 | } 90 | } 91 | 92 | impl TryFrom for Dependencies { 93 | type Error = crate::Error; 94 | fn try_from(deps: YamlValue) -> Result { 95 | match deps { 96 | YamlValue::Mapping(table) => Self::try_from(table), 97 | YamlValue::Sequence(array) => Self::try_from(array), 98 | _ => Err(anyhow!( 99 | "expected a map or array of dependencies, found `{:?}`", 100 | deps 101 | )), 102 | } 103 | } 104 | } 105 | 106 | impl Dependencies { 107 | /// Returns a set of dependencies for the given `image`. This includes common images 108 | /// from [COMMON_DEPS_KEY](COMMON_DEPS_KEY). 109 | pub fn resolve_names(&self, image: &str) -> HashSet<&str> { 110 | let mut deps = HashSet::new(); 111 | if let Some(common_deps) = self.inner.get(COMMON_DEPS_KEY) { 112 | deps.extend(common_deps.iter().map(|s| s.as_str())); 113 | } 114 | if let Some(image_deps) = self.inner.get(image) { 115 | deps.extend(image_deps.iter().map(|s| s.as_str())); 116 | } 117 | 118 | deps 119 | } 120 | 121 | /// Returns `true` if the `image` depends on the `dependency` or the dependency is in common 122 | /// dependencies. 123 | pub fn depends_on(&self, image: &str, dependency: &str) -> bool { 124 | if let Some(common_deps) = self.inner.get(COMMON_DEPS_KEY) { 125 | if common_deps.contains(dependency) { 126 | return true; 127 | } 128 | } 129 | if let Some(image_deps) = self.inner.get(image) { 130 | return image_deps.contains(dependency); 131 | } 132 | 133 | false 134 | } 135 | 136 | pub fn inner(&self) -> &DepsMap { 137 | &self.inner 138 | } 139 | 140 | pub fn inner_mut(&mut self) -> &mut DepsMap { 141 | &mut self.inner 142 | } 143 | 144 | /// Updates the dependencies of the given `image` by extending them or inserting the new ones 145 | /// if the entry doesn't yet exist. 146 | pub fn update_or_insert(&mut self, image: I, deps: D) 147 | where 148 | I: Into, 149 | V: Into, 150 | D: IntoIterator, 151 | { 152 | let image = image.into(); 153 | if let Some(image_deps) = self.inner.get_mut(&image) { 154 | image_deps.extend(deps.into_iter().map(|s| s.into())); 155 | } else { 156 | self.inner 157 | .insert(image, deps.into_iter().map(|s| s.into()).collect()); 158 | } 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use super::*; 165 | 166 | macro_rules! test_deps { 167 | ( 168 | input = $inp:expr, 169 | want = $( 170 | $image:ident => $($dep:literal),+ 171 | );+) => { 172 | let input: YamlValue = serde_yaml::from_str($inp).unwrap(); 173 | let input = input.as_mapping().unwrap().get(&serde_yaml::Value::String("build_depends".to_string())).unwrap().clone(); 174 | let got = Dependencies::try_from(input).unwrap(); 175 | 176 | $( 177 | let mut $image = HashSet::new(); 178 | $( 179 | $image.insert($dep); 180 | )+ 181 | 182 | assert_eq!($image, got.resolve_names(stringify!($image))); 183 | )+ 184 | 185 | } 186 | } 187 | 188 | #[test] 189 | fn parses_deps() { 190 | test_deps!( 191 | input = r#" 192 | build_depends: 193 | all: ["gcc", "pkg-config", "git"] 194 | rocky: ["cargo", "openssl-devel"] 195 | debian: ["curl", "libssl-dev"] 196 | "#, 197 | want = 198 | all => "gcc", "pkg-config", "git"; 199 | rocky => "cargo", "openssl-devel", "gcc", "pkg-config", "git"; 200 | debian => "curl", "libssl-dev", "gcc", "pkg-config", "git" 201 | ); 202 | test_deps!( 203 | input = r#" 204 | build_depends: 205 | - gcc 206 | - pkg-config 207 | - git 208 | "#, 209 | want = 210 | all => "gcc", "pkg-config", "git" 211 | ); 212 | } 213 | 214 | #[test] 215 | fn parses_joined_deps() { 216 | test_deps!( 217 | input = r#" 218 | build_depends: 219 | rocky+fedora34: [ cargo, openssl-devel ] 220 | debian+ubuntu20: [ libssl-dev ] 221 | debian: [ curl ] 222 | "#, 223 | want = 224 | rocky => "cargo", "openssl-devel"; 225 | fedora34 => "cargo", "openssl-devel"; 226 | debian => "curl", "libssl-dev"; 227 | ubuntu20 => "libssl-dev" 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/git.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Result}; 2 | 3 | use serde_yaml::{Mapping, Value as YamlValue}; 4 | use std::convert::TryFrom; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub struct GitSource { 8 | url: String, 9 | // defaults to master 10 | branch: String, 11 | } 12 | 13 | impl From<&str> for GitSource { 14 | fn from(s: &str) -> Self { 15 | Self { 16 | url: s.to_string(), 17 | branch: "master".to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl TryFrom for GitSource { 23 | type Error = Error; 24 | fn try_from(table: Mapping) -> Result { 25 | if let Some(url) = table.get(&YamlValue::from("url")) { 26 | if !url.is_string() { 27 | return Err(anyhow!("expected a string as url, found `{:?}`", url)); 28 | } 29 | 30 | let url = url.as_str().unwrap().to_string(); 31 | 32 | if let Some(branch) = table.get(&YamlValue::from("branch")) { 33 | if !branch.is_string() { 34 | return Err(anyhow!("expected a string as branch, found `{:?}`", branch)); 35 | } 36 | 37 | return Ok(GitSource::new( 38 | url, 39 | Some(branch.as_str().unwrap().to_string()), 40 | )); 41 | } 42 | 43 | Ok(GitSource::new(url, None::<&str>)) 44 | } else { 45 | Err(anyhow!( 46 | "expected a url entry in a table, found `{:?}`", 47 | table 48 | )) 49 | } 50 | } 51 | } 52 | 53 | impl TryFrom for GitSource { 54 | type Error = Error; 55 | fn try_from(value: YamlValue) -> Result { 56 | match value { 57 | YamlValue::Mapping(table) => Self::try_from(table), 58 | YamlValue::String(s) => Ok(Self::from(s.as_str())), 59 | value => Err(anyhow!( 60 | "expected a table or a string as git source, found `{:?}`", 61 | value 62 | )), 63 | } 64 | } 65 | } 66 | impl GitSource { 67 | pub fn new(url: U, branch: Option) -> Self 68 | where 69 | U: Into, 70 | B: Into, 71 | { 72 | Self { 73 | url: url.into(), 74 | branch: branch.map(B::into).unwrap_or_else(|| "master".to_string()), 75 | } 76 | } 77 | pub fn url(&self) -> &str { 78 | &self.url 79 | } 80 | pub fn branch(&self) -> &str { 81 | &self.branch 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/image.rs: -------------------------------------------------------------------------------- 1 | use crate::recipe::{BuildTarget, Os}; 2 | use crate::{Error, Result}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use serde_yaml::{Mapping, Value as YamlValue}; 6 | use std::convert::TryFrom; 7 | 8 | #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq, Hash)] 9 | pub struct ImageTarget { 10 | #[serde(rename = "name")] 11 | pub image: String, 12 | #[serde(rename = "target")] 13 | pub build_target: BuildTarget, 14 | pub os: Option, 15 | } 16 | 17 | impl ImageTarget { 18 | pub fn new(image: I, build_target: BuildTarget, os: Option) -> Self 19 | where 20 | I: Into, 21 | { 22 | Self { 23 | image: image.into(), 24 | build_target, 25 | os, 26 | } 27 | } 28 | } 29 | 30 | pub fn deserialize_images<'de, D>(deserializer: D) -> Result, D::Error> 31 | where 32 | D: serde::de::Deserializer<'de>, 33 | { 34 | use serde::de::Error; 35 | 36 | let mut images = Vec::new(); 37 | let mapping = serde_yaml::Sequence::deserialize(deserializer)?; 38 | for value in mapping { 39 | match value { 40 | serde_yaml::Value::Mapping(map) => { 41 | images.push(ImageTarget::try_from(map).map_err(D::Error::custom)?); 42 | } 43 | _ => { 44 | return Err(D::Error::custom( 45 | "invalid value for image target, expected mapping", 46 | )) 47 | } 48 | } 49 | } 50 | Ok(images) 51 | } 52 | 53 | impl TryFrom for ImageTarget { 54 | type Error = Error; 55 | 56 | fn try_from(map: Mapping) -> Result { 57 | if let Some(image) = map.get(&YamlValue::from("name")) { 58 | if !image.is_string() { 59 | return Err(anyhow!( 60 | "expected a string as image name, found `{:?}`", 61 | image 62 | )); 63 | } 64 | let image = image.as_str().unwrap().to_string(); 65 | 66 | let target = if let Some(target) = map.get(&YamlValue::from("target")) { 67 | if !target.is_string() { 68 | return Err(anyhow!( 69 | "expected a string as image target, found `{:?}`", 70 | image 71 | )); 72 | } else { 73 | BuildTarget::try_from(target.as_str().unwrap())? 74 | } 75 | } else { 76 | BuildTarget::default() 77 | }; 78 | 79 | let os = if let Some(os) = map.get(&YamlValue::from("os")) { 80 | if !os.is_string() { 81 | return Err(anyhow!( 82 | "expected a string as image os, found `{:?}`", 83 | image 84 | )); 85 | } else { 86 | Some(Os::new(os.as_str().unwrap(), None::<&str>)) 87 | } 88 | } else { 89 | None 90 | }; 91 | 92 | Ok(ImageTarget { 93 | image, 94 | build_target: target, 95 | os, 96 | }) 97 | } else { 98 | Err(anyhow!("image name not found in `{:?}`", map)) 99 | } 100 | } 101 | } 102 | 103 | impl TryFrom for ImageTarget { 104 | type Error = Error; 105 | fn try_from(value: YamlValue) -> Result { 106 | match value { 107 | YamlValue::Mapping(map) => Self::try_from(map), 108 | YamlValue::String(image) => Ok(Self { 109 | image, 110 | build_target: BuildTarget::default(), 111 | os: None, 112 | }), 113 | value => Err(anyhow!( 114 | "expected a map or string for image, found `{:?}`", 115 | value 116 | )), 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/os.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::convert::AsRef; 3 | 4 | //#################################################################################################### 5 | 6 | #[derive(Debug, Deserialize, Clone, Serialize, Eq, PartialEq, Hash)] 7 | pub struct Os { 8 | distribution: Distro, 9 | version: Option, 10 | } 11 | 12 | impl Os { 13 | /// If a matching distribution is found returns an Os object, otherwise returns an error. 14 | pub fn new(os: O, version: Option) -> Self 15 | where 16 | O: AsRef, 17 | V: Into, 18 | { 19 | Self { 20 | distribution: Distro::from(os.as_ref()), 21 | version: version.map(V::into), 22 | } 23 | } 24 | 25 | pub fn version(&self) -> &str { 26 | if let Some(version) = &self.version { 27 | version.as_str() 28 | } else { 29 | "" 30 | } 31 | } 32 | 33 | pub fn name(&self) -> &str { 34 | self.distribution.as_ref() 35 | } 36 | 37 | pub fn package_manager(&self) -> PackageManager { 38 | let version: u8 = self.version().parse().unwrap_or_default(); 39 | match self.distribution { 40 | Distro::Arch => PackageManager::Pacman, 41 | Distro::Debian | Distro::Ubuntu => PackageManager::Apt, 42 | Distro::Rocky | Distro::RedHat | Distro::CentOS if version >= 8 => PackageManager::Dnf, 43 | Distro::Fedora if version >= 22 => PackageManager::Dnf, 44 | Distro::Rocky => PackageManager::Dnf, 45 | Distro::RedHat | Distro::CentOS | Distro::Fedora => PackageManager::Yum, 46 | Distro::Alpine => PackageManager::Apk, 47 | Distro::Unknown => PackageManager::Unknown, 48 | } 49 | } 50 | 51 | pub fn is_unknown(&self) -> bool { 52 | matches!(self.distribution, Distro::Unknown) 53 | } 54 | } 55 | 56 | //#################################################################################################### 57 | 58 | #[allow(clippy::upper_case_acronyms)] 59 | #[derive(Copy, Debug, Deserialize, Clone, Serialize, PartialEq, Eq, Hash)] 60 | pub enum Distro { 61 | Arch, 62 | CentOS, 63 | Debian, 64 | Fedora, 65 | RedHat, 66 | Ubuntu, 67 | Rocky, 68 | Alpine, 69 | Unknown, 70 | } 71 | 72 | impl AsRef for Distro { 73 | fn as_ref(&self) -> &str { 74 | use Distro::*; 75 | match self { 76 | Arch => "arch", 77 | CentOS => "centos", 78 | Debian => "debian", 79 | Fedora => "fedora", 80 | RedHat => "redhat", 81 | Ubuntu => "ubuntu", 82 | Rocky => "rocky", 83 | Alpine => "alpine", 84 | Unknown => "unknown", 85 | } 86 | } 87 | } 88 | 89 | impl From<&str> for Distro { 90 | fn from(s: &str) -> Self { 91 | use Distro::*; 92 | const DISTROS: &[(&str, Distro)] = &[ 93 | ("arch", Arch), 94 | ("centos", CentOS), 95 | ("debian", Debian), 96 | ("fedora", Fedora), 97 | ("redhat", RedHat), 98 | ("red hat", RedHat), 99 | ("ubuntu", Ubuntu), 100 | ("rocky", Rocky), 101 | ("alpine", Alpine), 102 | ]; 103 | let out = s.to_lowercase(); 104 | for (name, distro) in DISTROS.iter() { 105 | if out.contains(name) { 106 | return *distro; 107 | } 108 | } 109 | Unknown 110 | } 111 | } 112 | 113 | //#################################################################################################### 114 | 115 | #[derive(Debug, Clone)] 116 | pub enum PackageManager { 117 | Apt, 118 | Dnf, 119 | Pacman, 120 | Yum, 121 | Apk, 122 | Unknown, 123 | } 124 | 125 | impl AsRef for PackageManager { 126 | fn as_ref(&self) -> &str { 127 | match self { 128 | Self::Apt => "apt-get", 129 | Self::Dnf => "dnf", 130 | Self::Pacman => "pacman", 131 | Self::Yum => "yum", 132 | Self::Apk => "apk", 133 | Self::Unknown => "unkown", 134 | } 135 | } 136 | } 137 | 138 | impl PackageManager { 139 | pub fn install_args(&self) -> Vec<&'static str> { 140 | match self { 141 | Self::Apt => vec!["install", "-y"], 142 | Self::Dnf => vec!["install", "-y"], 143 | Self::Pacman => vec!["-S", "--noconfirm"], 144 | Self::Yum => vec!["install", "-y"], 145 | Self::Apk => vec!["add"], 146 | Self::Unknown => vec![], 147 | } 148 | } 149 | 150 | pub fn update_repos_args(&self) -> Vec<&'static str> { 151 | match self { 152 | Self::Apt => vec!["update", "-y"], 153 | Self::Dnf | Self::Yum => vec!["clean", "metadata"], 154 | Self::Pacman => vec!["-Sy", "--noconfirm"], 155 | Self::Apk => vec!["update"], 156 | Self::Unknown => vec![], 157 | } 158 | } 159 | 160 | pub fn upgrade_packages_args(&self) -> Vec<&'static str> { 161 | match self { 162 | Self::Apt => vec!["dist-upgrade", "-y"], 163 | Self::Dnf | Self::Yum => vec!["update", "-y"], 164 | Self::Pacman => vec!["-Syu", "--noconfirm"], 165 | Self::Apk => vec!["upgrade"], 166 | Self::Unknown => vec![], 167 | } 168 | } 169 | 170 | pub fn clean_cache(&self) -> Vec<&'static str> { 171 | match self { 172 | Self::Apt => vec!["clean"], 173 | Self::Dnf | Self::Yum => vec!["clean", "metadata"], 174 | Self::Pacman => vec!["-Sc"], 175 | Self::Apk => vec!["cache", "clean"], 176 | Self::Unknown => vec![], 177 | } 178 | } 179 | 180 | pub fn should_clean_cache(&self) -> bool { 181 | #[allow(clippy::match_like_matches_macro)] 182 | match self { 183 | Self::Apk => false, 184 | _ => true, 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/metadata/target.rs: -------------------------------------------------------------------------------- 1 | use crate::recipe::Os; 2 | use crate::{Error, Result}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use std::convert::{AsRef, TryFrom}; 6 | 7 | pub struct BuildTargetInfo { 8 | pub image: &'static str, 9 | pub name: &'static str, 10 | pub os: Os, 11 | } 12 | 13 | impl From<(&'static str, &'static str, Os)> for BuildTargetInfo { 14 | fn from(it: (&'static str, &'static str, Os)) -> Self { 15 | Self { 16 | image: it.0, 17 | name: it.1, 18 | os: it.2, 19 | } 20 | } 21 | } 22 | 23 | #[derive(Copy, Clone, Deserialize, Serialize, Debug, Eq, PartialEq, Hash)] 24 | #[serde(rename_all = "lowercase")] 25 | pub enum BuildTarget { 26 | Rpm, 27 | Deb, 28 | Gzip, 29 | Pkg, 30 | Apk, 31 | } 32 | 33 | impl Default for BuildTarget { 34 | fn default() -> Self { 35 | Self::Gzip 36 | } 37 | } 38 | 39 | impl TryFrom<&str> for BuildTarget { 40 | type Error = Error; 41 | 42 | fn try_from(s: &str) -> Result { 43 | match &s.to_lowercase()[..] { 44 | "rpm" => Ok(Self::Rpm), 45 | "deb" => Ok(Self::Deb), 46 | "gzip" => Ok(Self::Gzip), 47 | "pkg" => Ok(Self::Pkg), 48 | "apk" => Ok(Self::Apk), 49 | target => Err(anyhow!("unknown build target `{}`", target)), 50 | } 51 | } 52 | } 53 | 54 | impl AsRef for BuildTarget { 55 | fn as_ref(&self) -> &str { 56 | match &self { 57 | BuildTarget::Rpm => "rpm", 58 | BuildTarget::Deb => "deb", 59 | BuildTarget::Gzip => "gzip", 60 | BuildTarget::Pkg => "pkg", 61 | BuildTarget::Apk => "apk", 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkger-core/src/recipe/target.rs: -------------------------------------------------------------------------------- 1 | use crate::recipe::metadata::{BuildTarget, ImageTarget, Os}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq, Hash)] 6 | pub struct RecipeTarget { 7 | name: String, 8 | image_target: ImageTarget, 9 | } 10 | 11 | impl RecipeTarget { 12 | pub fn new(name: String, image_target: ImageTarget) -> Self { 13 | Self { name, image_target } 14 | } 15 | 16 | pub fn build_target(&self) -> &BuildTarget { 17 | &self.image_target.build_target 18 | } 19 | 20 | pub fn recipe(&self) -> &str { 21 | &self.name 22 | } 23 | 24 | pub fn image(&self) -> &str { 25 | &self.image_target.image 26 | } 27 | 28 | pub fn image_os(&self) -> &Option { 29 | &self.image_target.os 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkger-core/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod container; 2 | pub mod docker; 3 | pub mod podman; 4 | 5 | pub use docker::DockerContainer; 6 | pub use docker_api; 7 | pub use podman::PodmanContainer; 8 | pub use podman_api; 9 | 10 | use crate::{ErrContext, Result}; 11 | 12 | use docker_api::Docker; 13 | use podman_api::Podman; 14 | 15 | #[derive(Clone, Debug)] 16 | pub enum RuntimeConnector { 17 | Docker(docker_api::Docker), 18 | Podman(podman_api::Podman), 19 | } 20 | 21 | pub struct ConnectionPool { 22 | connector: RuntimeConnector, 23 | } 24 | 25 | impl ConnectionPool { 26 | pub async fn new_checked(uri: impl Into) -> Result { 27 | let uri = uri.into(); 28 | let podman = Podman::new(&uri)?; 29 | if podman.ping().await.is_ok() { 30 | return Ok(Self::podman(podman)); 31 | } 32 | let docker = Docker::new(&uri)?; 33 | docker 34 | .ping() 35 | .await 36 | .map(|_| Self::docker(docker)) 37 | .context(format!("failed to ping container runtime at `{uri}`")) 38 | } 39 | 40 | pub fn docker(docker: Docker) -> Self { 41 | Self { 42 | connector: RuntimeConnector::Docker(docker), 43 | } 44 | } 45 | 46 | pub fn podman(podman: Podman) -> Self { 47 | Self { 48 | connector: RuntimeConnector::Podman(podman), 49 | } 50 | } 51 | 52 | pub fn connect(&self) -> RuntimeConnector { 53 | self.connector.clone() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkger-core/src/ssh.rs: -------------------------------------------------------------------------------- 1 | use crate::{err, Error, Result}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | #[cfg(target_os = "linux")] 6 | use {crate::ErrContext, std::env}; 7 | 8 | pub const SOCK_ENV: &str = "SSH_AUTH_SOCK"; 9 | 10 | #[derive(Clone, Deserialize, Debug, Serialize)] 11 | pub struct SshConfig { 12 | #[serde(default)] 13 | pub forward_agent: bool, 14 | #[serde(default)] 15 | pub disable_key_verification: bool, 16 | } 17 | 18 | /// Returns the path to the SSH authentication socket depending on the operating system 19 | /// and checks if the socket exists. 20 | pub fn auth_sock() -> Result { 21 | #[cfg(target_os = "linux")] 22 | let socket = env::var(SOCK_ENV).context("missing ssh auth socket environment variable")?; 23 | 24 | #[cfg(target_os = "macos")] 25 | let socket = "/run/host-services/ssh-auth.sock".to_owned(); 26 | 27 | let path = PathBuf::from(&socket); 28 | if !path.exists() { 29 | return err!("ssh auth socket does not exist"); 30 | } 31 | 32 | Ok(socket) 33 | } 34 | -------------------------------------------------------------------------------- /pkger-core/src/template/lexer.rs: -------------------------------------------------------------------------------- 1 | use crate::template::{Token, Variable}; 2 | 3 | pub struct Lexer<'text> { 4 | text: &'text str, 5 | pos: usize, 6 | } 7 | 8 | impl<'text> Lexer<'text> { 9 | pub fn new(text: &'text str) -> Self { 10 | Self { text, pos: 0 } 11 | } 12 | 13 | pub fn next_token(&mut self) -> Token { 14 | self.parse_token() 15 | } 16 | 17 | fn nth(&self, n: usize) -> Option { 18 | if self.pos < self.text.len() { 19 | // This is much faster than self.text.chars().nth() 20 | self.text[n..n + 1].chars().next() 21 | } else { 22 | None 23 | } 24 | } 25 | 26 | fn next_pos(&mut self) -> bool { 27 | if self.pos < self.text.len() { 28 | self.pos += 1; 29 | true 30 | } else { 31 | false 32 | } 33 | } 34 | 35 | fn peek(&self) -> Option { 36 | self.nth(self.pos + 1) 37 | } 38 | 39 | fn cur(&self) -> char { 40 | self.nth(self.pos).unwrap_or_default() 41 | } 42 | 43 | fn is_eof(&self) -> bool { 44 | self.pos == self.text.len() 45 | } 46 | 47 | fn parse_token(&mut self) -> Token { 48 | if self.cur() == '$' { 49 | self.next_pos(); 50 | self.parse_variable() 51 | } else if self.is_eof() { 52 | Token::EOF 53 | } else { 54 | self.parse_text() 55 | } 56 | } 57 | 58 | fn parse_braced_variable(&mut self) -> Token { 59 | let var_start = self.pos - 1; 60 | 61 | self.next_pos(); 62 | if self.cur() == ' ' { 63 | self.next_pos(); 64 | } 65 | loop { 66 | let cur = self.cur(); 67 | let ok = if cur == '}' { 68 | self.next_pos(); 69 | true 70 | } else if cur.is_ascii_whitespace() && self.peek() == Some('}') { 71 | self.next_pos(); 72 | self.next_pos(); 73 | true 74 | } else { 75 | false 76 | }; 77 | 78 | if ok { 79 | return Token::Variable(Variable::new( 80 | &self.text[var_start..self.pos], 81 | self.text[var_start + 2..self.pos - 1].trim(), 82 | )); 83 | } else if !Variable::is_valid_name_char(cur) || !self.next_pos() { 84 | return Token::Text(&self.text[var_start..self.pos]); 85 | } 86 | } 87 | } 88 | 89 | fn parse_unbraced_variable(&mut self) -> Token { 90 | let var_start = self.pos - 1; 91 | loop { 92 | let cur = self.cur(); 93 | if !Variable::is_valid_name_char(cur) { 94 | if var_start == self.pos - 1 { 95 | return Token::Text(&self.text[var_start..self.pos]); 96 | } else { 97 | return Token::Variable(Variable::new( 98 | &self.text[var_start..self.pos], 99 | self.text[var_start + 1..self.pos].trim(), 100 | )); 101 | } 102 | } 103 | 104 | if !self.next_pos() { 105 | return Token::Variable(Variable::new( 106 | &self.text[var_start..self.pos], 107 | self.text[var_start..self.pos].trim(), 108 | )); 109 | } 110 | } 111 | } 112 | 113 | fn parse_variable(&mut self) -> Token { 114 | if self.cur() == '{' { 115 | self.parse_braced_variable() 116 | } else { 117 | self.parse_unbraced_variable() 118 | } 119 | } 120 | 121 | fn parse_text(&mut self) -> Token { 122 | let start = self.pos; 123 | loop { 124 | if self.cur() == '$' || !self.next_pos() { 125 | return Token::Text(&self.text[start..self.pos]); 126 | } 127 | } 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | #[test] 136 | fn simple_case() { 137 | let text = "this is my super ${ cool } text."; 138 | let mut parser = Lexer::new(text); 139 | assert_eq!(parser.next_token(), Token::Text("this is my super ")); 140 | assert_eq!( 141 | parser.next_token(), 142 | Token::Variable(Variable::new("${ cool }", "cool")) 143 | ); 144 | assert_eq!(parser.next_token(), Token::Text(" text.")); 145 | assert_eq!(parser.next_token(), Token::EOF); 146 | assert_eq!(parser.next_token(), Token::EOF); 147 | } 148 | 149 | #[test] 150 | fn multiple_vars() { 151 | let text = r#"this is a ${much} more ${ complex } case. 152 | It includes multiple ${lines} and ${ variables }."#; 153 | 154 | let mut parser = Lexer::new(text); 155 | assert_eq!(parser.next_token(), Token::Text("this is a ")); 156 | assert_eq!( 157 | parser.next_token(), 158 | Token::Variable(Variable::new("${much}", "much")) 159 | ); 160 | assert_eq!(parser.next_token(), Token::Text(" more ")); 161 | assert_eq!( 162 | parser.next_token(), 163 | Token::Variable(Variable::new("${ complex }", "complex")) 164 | ); 165 | assert_eq!( 166 | parser.next_token(), 167 | Token::Text(" case.\n It includes multiple ") 168 | ); 169 | assert_eq!( 170 | parser.next_token(), 171 | Token::Variable(Variable::new("${lines}", "lines")) 172 | ); 173 | assert_eq!(parser.next_token(), Token::Text(" and ")); 174 | assert_eq!( 175 | parser.next_token(), 176 | Token::Variable(Variable::new("${ variables }", "variables")) 177 | ); 178 | assert_eq!(parser.next_token(), Token::Text(".")); 179 | assert_eq!(parser.next_token(), Token::EOF); 180 | } 181 | 182 | #[test] 183 | fn corner_cases() { 184 | let text = "this ${should be just text$}${123this_is-CorrecT }${}"; 185 | let mut parser = Lexer::new(text); 186 | assert_eq!(parser.next_token(), Token::Text("this ")); 187 | assert_eq!(parser.next_token(), Token::Text("${should")); 188 | assert_eq!(parser.next_token(), Token::Text(" be just text")); 189 | assert_eq!(parser.next_token(), Token::Text("$")); 190 | assert_eq!(parser.next_token(), Token::Text("}")); 191 | assert_eq!( 192 | parser.next_token(), 193 | Token::Variable(Variable::new( 194 | "${123this_is-CorrecT }", 195 | "123this_is-CorrecT" 196 | )), 197 | ); 198 | assert_eq!( 199 | parser.next_token(), 200 | Token::Variable(Variable::new("${}", "")) 201 | ); 202 | assert_eq!(parser.next_token(), Token::EOF); 203 | } 204 | 205 | #[test] 206 | fn no_braces() { 207 | let text = "this is my super $COOL_ $} text."; 208 | let mut parser = Lexer::new(text); 209 | assert_eq!(parser.next_token(), Token::Text("this is my super ")); 210 | assert_eq!( 211 | parser.next_token(), 212 | Token::Variable(Variable::new("$COOL_", "COOL_")) 213 | ); 214 | assert_eq!(parser.next_token(), Token::Text(" ")); 215 | assert_eq!(parser.next_token(), Token::Text("$")); 216 | assert_eq!(parser.next_token(), Token::Text("} text.")); 217 | assert_eq!(parser.next_token(), Token::EOF); 218 | } 219 | 220 | #[test] 221 | fn empty_text() { 222 | let text = ""; 223 | let mut parser = Lexer::new(text); 224 | assert_eq!(parser.next_token(), Token::EOF); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /pkger-core/src/template/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | mod lexer; 4 | 5 | #[derive(Debug, PartialEq, Eq)] 6 | pub struct Variable<'text> { 7 | text: &'text str, 8 | name: &'text str, 9 | } 10 | 11 | impl<'text> Variable<'text> { 12 | pub fn new(text: &'text str, name: &'text str) -> Self { 13 | Self { text, name } 14 | } 15 | 16 | pub fn name(&self) -> &str { 17 | self.name 18 | } 19 | 20 | pub fn text(&self) -> &str { 21 | self.text 22 | } 23 | 24 | pub fn is_valid_name_char(ch: char) -> bool { 25 | ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' 26 | } 27 | } 28 | 29 | #[derive(Debug, PartialEq, Eq)] 30 | pub enum Token<'text> { 31 | Variable(Variable<'text>), 32 | Text(&'text str), 33 | EOF, 34 | } 35 | 36 | pub fn render(text: T, vars: &HashMap) -> String 37 | where 38 | T: AsRef, 39 | V: AsRef, 40 | { 41 | let mut parser = lexer::Lexer::new(text.as_ref()); 42 | let mut rendered = String::new(); 43 | 44 | loop { 45 | match parser.next_token() { 46 | Token::Text(txt) => rendered.push_str(txt), 47 | Token::Variable(var) => { 48 | if let Some(value) = vars.get(var.name()) { 49 | rendered.push_str(value.as_ref()); 50 | } else { 51 | rendered.push_str(var.text()); 52 | } 53 | } 54 | Token::EOF => break, 55 | } 56 | } 57 | 58 | rendered 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use crate::template::render; 64 | use std::collections::HashMap; 65 | 66 | #[test] 67 | fn renders_braced_vars() { 68 | let text = "cd $TEST_VAR/${PKGER_BLD_DIR}/${ RECIPE }/${ RECIPE_VERSION}${DOESNT_EXIST}"; 69 | let mut vars = HashMap::new(); 70 | vars.insert("PKGER_BLD_DIR".to_string(), "/tmp/test".to_string()); 71 | vars.insert("RECIPE".to_string(), "pkger-test".to_string()); 72 | vars.insert("RECIPE_VERSION".to_string(), "0.1.0".to_string()); 73 | 74 | assert_eq!( 75 | render(text, &vars), 76 | "cd $TEST_VAR//tmp/test/pkger-test/0.1.0${DOESNT_EXIST}".to_string() 77 | ); 78 | } 79 | 80 | #[test] 81 | fn renders_unbraced_vars() { 82 | let text = "cd $TEST_VAR/$PKGER_BLD_DIR/$RECIPE/$RECIPE_VERSION$DOESNT_EXIST"; 83 | let mut vars = HashMap::new(); 84 | vars.insert("PKGER_BLD_DIR".to_string(), "/tmp/test".to_string()); 85 | vars.insert("RECIPE".to_string(), "pkger-test".to_string()); 86 | vars.insert("RECIPE_VERSION".to_string(), "0.1.0".to_string()); 87 | 88 | assert_eq!( 89 | render(text, &vars), 90 | "cd $TEST_VAR//tmp/test/pkger-test/0.1.0$DOESNT_EXIST".to_string() 91 | ); 92 | } 93 | } 94 | --------------------------------------------------------------------------------