├── .envrc ├── src ├── globals.rs ├── lib.rs ├── commands │ ├── completions.rs │ ├── validate │ │ ├── yaml_span │ │ │ └── error.rs │ │ ├── schema_validator │ │ │ └── error.rs │ │ └── location.rs │ ├── completions │ │ └── shells.rs │ └── prune.rs └── bin │ └── bluebuild.rs ├── .earthlyignore ├── .github ├── CODEOWNERS ├── workflows │ ├── main.yml │ ├── flakehub-tagged.yml │ ├── zizmor.yml │ └── pr.yml └── dependabot.yml ├── integration-tests ├── test-repo │ ├── files │ │ ├── usr │ │ │ ├── .gitkeep │ │ │ └── bin │ │ │ │ └── test-file │ │ └── scripts │ │ │ ├── bluebuild.sh │ │ │ └── example.sh │ ├── modules │ │ ├── .gitkeep │ │ ├── test-module │ │ │ └── test-module.sh │ │ └── test-nu-modules │ │ │ └── test-nu-modules.nu │ ├── test_secret_file.txt │ ├── containerfiles │ │ └── labels │ │ │ └── Containerfile │ ├── .gitignore │ ├── cosign.pub │ └── recipes │ │ ├── invalid-module.yml │ │ ├── akmods.yml │ │ ├── recipe-arm64.yml │ │ ├── recipe-buildah.yml │ │ ├── recipe-podman.yml │ │ ├── recipe-rechunk.yml │ │ ├── recipe-build-chunked-oci.yml │ │ ├── recipe-docker-external.yml │ │ ├── recipe.yml │ │ ├── bluebuild.yml │ │ ├── recipe-bluefin.yml │ │ ├── recipe-multiplatform-docker.yml │ │ ├── recipe-multiplatform-podman.yml │ │ ├── recipe-multiplatform-buildah.yml │ │ ├── recipe-multiplatform-rechunk.yml │ │ ├── recipe-multiplatform-build-chunked-oci.yml │ │ ├── invalid-stages.yml │ │ ├── stages.yml │ │ ├── recipe-gts.yml │ │ ├── recipe-invalid.yml │ │ ├── recipe-invalid-module.yml │ │ ├── recipe-invalid-from-file.yml │ │ ├── flatpaks.yml │ │ └── recipe-invalid-stage.yml ├── empty-files-repo │ ├── modules │ │ └── .gitkeep │ ├── .gitignore │ ├── cosign.pub │ ├── recipes │ │ └── recipe.yml │ └── README.md ├── legacy-test-repo │ ├── modules │ │ ├── .gitkeep │ │ └── test-module │ │ │ └── test-module.sh │ ├── config │ │ ├── files │ │ │ └── usr │ │ │ │ ├── .gitkeep │ │ │ │ └── test-file │ │ ├── containerfiles │ │ │ └── labels │ │ │ │ └── Containerfile │ │ ├── akmods.yml │ │ ├── scripts │ │ │ └── example.sh │ │ └── recipe.yml │ ├── .gitignore │ └── cosign.pub ├── test-scripts │ └── test-file.sh └── mock-scripts │ ├── buildah │ ├── docker │ ├── podman │ ├── rpm-ostree │ └── bootc ├── image_files ├── entrypoint.sh └── containers.conf ├── template ├── rinja.toml ├── templates │ ├── modules │ │ ├── copy │ │ │ ├── module.yml │ │ │ ├── copy.j2 │ │ │ ├── copy.tsp │ │ │ └── README.md │ │ ├── containerfile │ │ │ ├── module.yml │ │ │ ├── containerfile.j2 │ │ │ └── containerfile.tsp │ │ └── akmods │ │ │ └── akmods.j2 │ ├── github_issue.j2 │ ├── init │ │ ├── gitlab-ci.yml.j2 │ │ └── README.j2 │ ├── Containerfile.j2 │ └── stages.j2 └── Cargo.toml ├── process ├── drivers │ ├── types.rs │ ├── opts │ │ ├── boot.rs │ │ ├── inspect.rs │ │ ├── oci_copy.rs │ │ ├── ci.rs │ │ ├── build_chunked_oci.rs │ │ ├── rechunk.rs │ │ └── run.rs │ ├── opts.rs │ ├── types │ │ └── metadata.rs │ ├── skopeo_driver.rs │ ├── functions.rs │ ├── oci_client_driver.rs │ ├── docker_driver │ │ └── metadata.rs │ ├── rpm_ostree_driver.rs │ ├── bootc_driver.rs │ └── bootc_driver │ │ └── status.rs ├── process.rs └── Cargo.toml ├── test-files ├── recipes │ ├── modules │ │ ├── signing-pass.yml │ │ ├── bling-fail.yml │ │ ├── signing-fail.yml │ │ ├── bling-pass.yml │ │ ├── yafti-fail.yml │ │ ├── akmods-fail.yml │ │ ├── akmods-pass.yml │ │ ├── script-fail.yml │ │ ├── yafti-pass.yml │ │ ├── copy-pass.yml │ │ ├── files-fail.yml │ │ ├── files-pass.yml │ │ ├── gschema-overrides-fail.yml │ │ ├── justfiles-fail.yml │ │ ├── gschema-overrides-pass.yml │ │ ├── justfiles-pass.yml │ │ ├── script-pass.yml │ │ ├── copy-fail.yml │ │ ├── fonts-fail.yml │ │ ├── gnome-extensions-fail.yml │ │ ├── containerfile-fail.yml │ │ ├── fonts-pass.yml │ │ ├── gnome-extensions-pass.yml │ │ ├── containerfile-pass.yml │ │ ├── rpm-ostree-fail.yml │ │ ├── chezmoi-fail.yml │ │ ├── chezmoi-pass.yml │ │ ├── brew-pass.yml │ │ ├── rpm-ostree-pass.yml │ │ ├── brew-fail.yml │ │ ├── systemd-fail.yml │ │ ├── systemd-pass.yml │ │ ├── default-flatpaks-fail.yml │ │ └── default-flatpaks-pass.yml │ ├── stage-pass.yml │ ├── stage-fail.yml │ ├── stage-list-pass.yml │ ├── stage-list-fail.yml │ ├── recipe-fail.yml │ ├── recipe-pass.yml │ ├── module-list-fail.yml │ └── module-list-pass.yml ├── keys │ ├── cosign.pub │ └── cosign.key ├── github-events │ ├── scheduled.json │ ├── branch.json │ ├── default-branch.json │ └── pr-branch.json └── schema │ ├── import-v1.json │ ├── modules │ ├── signing-v1.json │ ├── signing-latest.json │ ├── gschema-overrides-v1.json │ ├── gschema-overrides-latest.json │ ├── script-v1.json │ ├── script-latest.json │ ├── yafti-v1.json │ ├── yafti-latest.json │ ├── justfiles-v1.json │ ├── copy-v1.json │ ├── justfiles-latest.json │ ├── copy-latest.json │ ├── containerfile-v1.json │ ├── containerfile-latest.json │ ├── fonts-v1.json │ ├── fonts-latest.json │ ├── gnome-extensions-v1.json │ ├── gnome-extensions-latest.json │ ├── bling-v1.json │ ├── bling-latest.json │ ├── files-v1.json │ ├── files-latest.json │ ├── akmods-v1.json │ ├── akmods-latest.json │ ├── chezmoi-v1.json │ ├── chezmoi-latest.json │ ├── rpm-ostree-latest.json │ └── rpm-ostree-v1.json │ ├── stage-list-v1.json │ ├── module-custom-v1.json │ ├── module-list-v1.json │ ├── module-stage-list-v1.json │ └── stage-v1.json ├── .helix └── languages.toml ├── SECURITY.md ├── cosign.pub ├── rust-toolchain.toml ├── distrobox.ini ├── .rusty-hook.toml ├── .gitignore ├── recipe ├── src │ ├── akmods_info.rs │ ├── lib.rs │ ├── maybe_version.rs │ ├── module │ │ └── type_ver.rs │ ├── module_ext.rs │ └── stages_ext.rs └── Cargo.toml ├── .editorconfig ├── rustfmt.toml ├── scripts ├── pre_build.sh ├── post_build.sh ├── setup.sh ├── run_module.sh └── exports.sh ├── utils ├── build.rs ├── Cargo.toml └── src │ ├── test_utils.rs │ └── traits.rs ├── install.sh ├── flake.nix └── flake.lock /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /src/globals.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.earthlyignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gmpinder 2 | -------------------------------------------------------------------------------- /integration-tests/test-repo/files/usr/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration-tests/test-repo/modules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration-tests/empty-files-repo/modules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/modules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration-tests/test-repo/files/usr/bin/test-file: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/files/usr/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration-tests/test-repo/test_secret_file.txt: -------------------------------------------------------------------------------- 1 | TEST_PASS 2 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/files/usr/test-file: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /integration-tests/test-scripts/test-file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -f /usr/test-file ] 4 | -------------------------------------------------------------------------------- /image_files/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | dumb-init "$@" 6 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/.gitignore: -------------------------------------------------------------------------------- 1 | /Containerfile 2 | /Containerfile.* 3 | /.bluebuild* 4 | -------------------------------------------------------------------------------- /integration-tests/test-repo/containerfiles/labels/Containerfile: -------------------------------------------------------------------------------- 1 | LABEL org.test.label="this is a test" 2 | -------------------------------------------------------------------------------- /template/rinja.toml: -------------------------------------------------------------------------------- 1 | [[syntax]] 2 | name = "github-actions" 3 | expr_start = "{{{" 4 | expr_end = "}}}" 5 | -------------------------------------------------------------------------------- /process/drivers/types.rs: -------------------------------------------------------------------------------- 1 | mod drivers; 2 | mod metadata; 3 | 4 | pub use drivers::*; 5 | pub use metadata::*; 6 | -------------------------------------------------------------------------------- /integration-tests/empty-files-repo/.gitignore: -------------------------------------------------------------------------------- 1 | cosign.key 2 | cosign.private 3 | /.bluebuild* 4 | /Containerfile 5 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/containerfiles/labels/Containerfile: -------------------------------------------------------------------------------- 1 | LABEL org.test.label="this is a test" 2 | -------------------------------------------------------------------------------- /integration-tests/test-repo/.gitignore: -------------------------------------------------------------------------------- 1 | /Containerfile 2 | /Containerfile.* 3 | /.bluebuild* 4 | /secrets 5 | /.bluebuild-scripts_* 6 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/modules/test-module/test-module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "This is a test module" 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/signing-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/signing.json 3 | type: signing 4 | -------------------------------------------------------------------------------- /test-files/recipes/modules/bling-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/bling.json 3 | type: bling 4 | install: rpmfusion 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/signing-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/signing.json 3 | type: signing 4 | dir: /dir 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/bling-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/bling.json 3 | type: bling 4 | install: 5 | - rpmfusion 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/yafti-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/yafti.json 3 | type: yafti 4 | custom-flatpaks: 5 | - test.org 6 | -------------------------------------------------------------------------------- /.helix/languages.toml: -------------------------------------------------------------------------------- 1 | [language-server.rust-analyzer.config] 2 | cargo.features = "all" 3 | 4 | [language-server.rust-analyzer.config.check] 5 | command = "clippy" 6 | args = ["--no-deps"] 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/akmods-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/akmods.json 3 | type: akmods 4 | base: main 5 | install: openrgb 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report vulnerabilities privately via [this form](https://github.com/blue-build/cli/security/advisories/new). 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/akmods-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/akmods.json 3 | type: akmods 4 | base: main 5 | install: 6 | - openrgb 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/script-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/script.json 3 | type: script 4 | snippets: rm -fr /* 5 | scripts: test.sh 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/yafti-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/yafti.json 3 | type: yafti 4 | custom-flatpaks: 5 | - PrettyTest: test.org 6 | -------------------------------------------------------------------------------- /cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmljzkrD93JsnDGEaa5FCI0BPgZCa 3 | vNO6CI08UCNWIT5osG+Tt8AWcVze1oXzCR1ifBUCy/znZ8JLLnUuL/2TGQ== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/copy-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/copy.json 3 | type: copy 4 | from: test-stage 5 | src: /out/test 6 | dest: /in/test 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/files-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/files.json 3 | type: files 4 | files: 5 | source: test 6 | destination: /usr 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/files-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/files.json 3 | type: files 4 | files: 5 | - source: test 6 | destination: /usr 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/gschema-overrides-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/gschema-overrides.json 3 | type: gschema-overrides 4 | include: test 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/justfiles-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/justfiles.json 3 | type: justfiles 4 | validate: 'true' 5 | include: ./justfile 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/gschema-overrides-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/gschema-overrides.json 3 | type: gschema-overrides 4 | include: 5 | - test 6 | -------------------------------------------------------------------------------- /test-files/recipes/modules/justfiles-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/justfiles.json 3 | type: justfiles 4 | validate: true 5 | include: 6 | - ./justfile 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/script-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/script.json 3 | type: script 4 | snippets: 5 | - rm -fr /* 6 | scripts: 7 | - test.sh 8 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = [ 4 | "x86_64-unknown-linux-gnu", 5 | "x86_64-unknown-linux-musl", 6 | "aarch64-unknown-linux-gnu", 7 | "aarch64-unknown-linux-musl", 8 | ] 9 | -------------------------------------------------------------------------------- /test-files/keys/cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq8TdgrRtcWVq6MXuB2uznS14EOQ9 3 | Ol41BztsDr0Qd8BGfYM6lOkZ+/NLteBFZ9gQsgVhVrjrSifcHmMAUOZYwg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/copy-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/copy.json 3 | type: copy 4 | from: test-stage 5 | src: 6 | - /out/test 7 | dest: 8 | - /in/test 9 | -------------------------------------------------------------------------------- /test-files/recipes/modules/fonts-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/fonts.json 3 | type: fonts 4 | fonts: 5 | nerd-fonts: "JetbrainsMono" 6 | google-fonts: "Test" 7 | -------------------------------------------------------------------------------- /test-files/recipes/modules/gnome-extensions-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/gnome-extensions.json 3 | type: gnome-extensions 4 | install: test 5 | uninstall: rmtest 6 | -------------------------------------------------------------------------------- /test-files/github-events/scheduled.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | "default_branch": "main", 4 | "html_url": "https://github.com/gmpinder/testos", 5 | "owner": { 6 | "login": "gmpinder" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test-files/recipes/stage-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/stage-v1.json 3 | from: test 4 | name: test 5 | modules: 6 | - type: script 7 | snippets: 8 | - echo test 9 | -------------------------------------------------------------------------------- /integration-tests/test-repo/cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgJYNEq43hrKPwWgWah14yBOUjMCd 3 | 1eG8hOwIbOTSRq+siTLep8G2m5FSYit/ea+H+0IXZS0ruLdgzoPUI7Babw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test-files/recipes/modules/containerfile-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/containerfile.json 3 | type: containerfile 4 | snippets: RUN echo "Hello!" 5 | containerfiles: test 6 | -------------------------------------------------------------------------------- /test-files/recipes/stage-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/stage-v1.json 3 | from: test 4 | name: 5 | - test 6 | modules: 7 | type: script 8 | snippets: 9 | - echo test 10 | -------------------------------------------------------------------------------- /distrobox.ini: -------------------------------------------------------------------------------- 1 | [bluebuild] 2 | image=ghcr.io/blue-build/cli:latest-distrobox 3 | exported_bins=/usr/bin/bluebuild 4 | init_hooks=ln -sf /usr/bin/distrobox-host-exec /usr/local/bin/podman 5 | pull=true 6 | replace=true 7 | start_now=true 8 | -------------------------------------------------------------------------------- /integration-tests/empty-files-repo/cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgJYNEq43hrKPwWgWah14yBOUjMCd 3 | 1eG8hOwIbOTSRq+siTLep8G2m5FSYit/ea+H+0IXZS0ruLdgzoPUI7Babw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgJYNEq43hrKPwWgWah14yBOUjMCd 3 | 1eG8hOwIbOTSRq+siTLep8G2m5FSYit/ea+H+0IXZS0ruLdgzoPUI7Babw== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /integration-tests/test-repo/modules/test-module/test-module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "This is a test module" 6 | 7 | get_json_array FILES '.test[]' '{"test":[1,2,3]}' 8 | 9 | echo "${FILES[@]}" 10 | -------------------------------------------------------------------------------- /test-files/recipes/modules/fonts-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/fonts.json 3 | type: fonts 4 | fonts: 5 | nerd-fonts: 6 | - "JetbrainsMono" 7 | google-fonts: 8 | - "Test" 9 | -------------------------------------------------------------------------------- /test-files/recipes/modules/gnome-extensions-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/gnome-extensions.json 3 | type: gnome-extensions 4 | install: 5 | - test 6 | uninstall: 7 | - rmtest 8 | -------------------------------------------------------------------------------- /test-files/recipes/modules/containerfile-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/containerfile.json 3 | type: containerfile 4 | snippets: 5 | - RUN echo "Hello!" 6 | containerfiles: 7 | - test 8 | -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-push = "cargo fmt --check --all && cargo test --workspace && cargo test --workspace --all-features && cargo clippy -- -D warnings && cargo clippy --all-features -- -D warnings" 3 | 4 | [logging] 5 | verbose = true 6 | -------------------------------------------------------------------------------- /integration-tests/test-repo/files/scripts/bluebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cargo install blue-build --debug --all-features --target x86_64-unknown-linux-gnu 6 | mkdir -p /out/ 7 | mv $CARGO_HOME/bin/bluebuild /out/bluebuild 8 | -------------------------------------------------------------------------------- /test-files/github-events/branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/test-branch", 3 | "repository": { 4 | "default_branch": "main", 5 | "owner": { 6 | "login": "Test-Owner" 7 | }, 8 | "html_url": "https://example.com/" 9 | } 10 | } -------------------------------------------------------------------------------- /test-files/github-events/default-branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/main", 3 | "repository": { 4 | "default_branch": "main", 5 | "owner": { 6 | "login": "Test-Owner" 7 | }, 8 | "html_url": "https://example.com/" 9 | } 10 | } -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/invalid-module.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json 3 | modules: 4 | # Tests installing rpms from a combo image stage 5 | - type: akmods 6 | install: openrazer 7 | 8 | -------------------------------------------------------------------------------- /template/templates/modules/copy/module.yml: -------------------------------------------------------------------------------- 1 | name: copy 2 | shortdesc: The copy module is a direct translation of the `COPY` instruction in a Containerfile. 3 | example: | 4 | type: copy 5 | from: docker.io/mikefarah/yq 6 | src: /usr/bin/yq 7 | dest: /usr/bin/ 8 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/akmods.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json 3 | modules: [] 4 | # Tests installing rpms from a combo image stage 5 | # - type: akmods 6 | # install: 7 | # - openrazer 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .sccache/ 3 | .vscode/ 4 | result* 5 | .direnv/ 6 | .arg 7 | .secret 8 | 9 | cosign.key 10 | !test-files/keys/cosign.key 11 | 12 | # Local testing for bluebuild recipe files 13 | /config/* 14 | /Containerfile 15 | /expand.rs 16 | /digest-list 17 | -------------------------------------------------------------------------------- /test-files/recipes/stage-list-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/stage-list-v1.json 3 | stages: 4 | - from: test 5 | name: test 6 | modules: 7 | - type: script 8 | snippets: 9 | - echo test 10 | -------------------------------------------------------------------------------- /image_files/containers.conf: -------------------------------------------------------------------------------- 1 | [containers] 2 | netns="host" 3 | userns="host" 4 | ipcns="host" 5 | utsns="host" 6 | cgroupns="host" 7 | cgroups="disabled" 8 | log_driver = "k8s-file" 9 | [engine] 10 | cgroup_manager = "cgroupfs" 11 | events_logger="file" 12 | runtime="crun" 13 | -------------------------------------------------------------------------------- /test-files/recipes/stage-list-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/stage-list-v1.json 3 | stages: 4 | - from: test 5 | name: 6 | - test 7 | modules: 8 | type: script 9 | snippets: 10 | - echo test 11 | 12 | -------------------------------------------------------------------------------- /recipe/src/akmods_info.rs: -------------------------------------------------------------------------------- 1 | use bon::Builder; 2 | 3 | #[derive(Debug, Clone, Builder, PartialEq, Eq, Hash)] 4 | pub struct AkmodsInfo { 5 | #[builder(into)] 6 | pub images: (String, Option, Option), 7 | 8 | #[builder(into)] 9 | pub stage_name: String, 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*.j2] 11 | indent_size = 2 12 | 13 | [justfile] 14 | indent_size = 2 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /process/drivers/opts/boot.rs: -------------------------------------------------------------------------------- 1 | use bon::Builder; 2 | use oci_client::Reference; 3 | 4 | #[derive(Debug, Clone, Copy, Builder)] 5 | #[builder(derive(Debug, Clone))] 6 | pub struct SwitchOpts<'scope> { 7 | pub image: &'scope Reference, 8 | 9 | #[builder(default)] 10 | pub reboot: bool, 11 | } 12 | -------------------------------------------------------------------------------- /test-files/github-events/pr-branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "ref": "test-branch" 4 | }, 5 | "base": { 6 | "ref": "main" 7 | }, 8 | "repository": { 9 | "default_branch": "main", 10 | "owner": { 11 | "login": "Test-Owner" 12 | }, 13 | "html_url": "https://example.com/" 14 | } 15 | } -------------------------------------------------------------------------------- /test-files/recipes/modules/rpm-ostree-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/rpm-ostree.json 3 | type: rpm-ostree 4 | repos: 5 | - test.repo 6 | keys: test.key 7 | optfix: 8 | - test 9 | install: 10 | - test 11 | remove: rmtest 12 | replace: 13 | - replacetest 14 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/akmods.yml: -------------------------------------------------------------------------------- 1 | # TODO: Add back installs after upstream issues are fixed 2 | 3 | modules: [] 4 | # Tests installing rpms from a combo image stage 5 | # - type: akmods 6 | # base: surface 7 | # nvidia-version: 550 8 | # install: 9 | # - nvidia 10 | # - openrazer 11 | -------------------------------------------------------------------------------- /process/drivers/opts/inspect.rs: -------------------------------------------------------------------------------- 1 | use bon::Builder; 2 | use oci_client::Reference; 3 | 4 | #[derive(Debug, Clone, Copy, Builder, Hash)] 5 | #[builder(derive(Clone))] 6 | pub struct GetMetadataOpts<'scope> { 7 | #[builder(into)] 8 | pub image: &'scope Reference, 9 | 10 | #[builder(default)] 11 | pub no_cache: bool, 12 | } 13 | -------------------------------------------------------------------------------- /test-files/recipes/modules/chezmoi-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/chezmoi.json 3 | type: chezmoi 4 | repository: 'test-repo.git' 5 | branch: 'main' 6 | all-users: "true" 7 | run-every: "1d" 8 | wait-after-boot: "5m" 9 | disable-init: false 10 | disable-update: "false" 11 | file-conflict-policy: none 12 | -------------------------------------------------------------------------------- /test-files/recipes/modules/chezmoi-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/chezmoi.json 3 | type: chezmoi 4 | repository: 'test-repo.git' 5 | branch: 'main' 6 | all-users: false 7 | run-every: "1d" 8 | wait-after-boot: "5m" 9 | disable-init: false 10 | disable-update: false 11 | file-conflict-policy: replace 12 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-arm64.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-arm64 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-buildah.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-buildah 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-podman.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-podman 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-rechunk.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-rechunk 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /test-files/recipes/modules/brew-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/brew.json 3 | type: brew 4 | auto-update: true 5 | update-interval: "6h" 6 | auto-upgrade: true 7 | update-wait-after-boot: "10min" 8 | upgrade-interval: "8h" 9 | upgrade-wait-after-boot: "30min" 10 | nofile-limits: true 11 | brew-analytics: false 12 | -------------------------------------------------------------------------------- /template/templates/modules/containerfile/module.yml: -------------------------------------------------------------------------------- 1 | name: containerfile 2 | shortdesc: The containerfile module enables the addition of custom Containerfile instructions into the build process. 3 | example: | 4 | type: containerfile 5 | snippets: 6 | - RUN --mount=type=tmpfs,target=/tmp /some/script.sh 7 | containerfiles: 8 | - example 9 | - subroutine 10 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-build-chunked-oci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-build-chunked-oci 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-docker-external.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-docker-external 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: common.yml 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true 2 | newline_style = "Unix" 3 | 4 | # The following lines may be uncommented on nightly Rust. 5 | # Once these features have stabilized, they should be added to the always-enabled options above. 6 | # unstable_features = true 7 | # imports_granularity = "Crate" 8 | # wrap_comments = true 9 | # comment_width = 100 10 | # normalize_comments = true -------------------------------------------------------------------------------- /test-files/recipes/modules/rpm-ostree-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/rpm-ostree.json 3 | type: rpm-ostree 4 | repos: 5 | - test.repo 6 | keys: 7 | - test.key 8 | optfix: 9 | - test 10 | install: 11 | - test 12 | remove: 13 | - rmtest 14 | replace: 15 | - from-repo: updates 16 | packages: 17 | - replacetest 18 | -------------------------------------------------------------------------------- /template/templates/modules/containerfile/containerfile.j2: -------------------------------------------------------------------------------- 1 | {%- if let Some(containerfiles) = module.get_containerfile_list() %} 2 | {%- for c in containerfiles %} 3 | {{ self::print_containerfile(c) }} 4 | {%- endfor %} 5 | {%- endif %} 6 | {%- if let Some(snippets) = module.get_containerfile_snippets() %} 7 | {%- for s in snippets %} 8 | {{ s }} 9 | {%- endfor %} 10 | {%- endif %} 11 | -------------------------------------------------------------------------------- /template/templates/modules/copy/copy.j2: -------------------------------------------------------------------------------- 1 | {%- if let Some((from_img, src, dest)) = module.get_copy_args() %} 2 | COPY 3 | {%- 4 | if let Some(from_img) = from_img 5 | %} --from={{ from_img }}{% 6 | endif 7 | %} 8 | {%- match build_engine %} 9 | {%- when BuildEngine::Docker %} --link 10 | {%- else %} 11 | {%- endmatch %} {{ src }} {{ dest }} 12 | {%- endif %} 13 | 14 | -------------------------------------------------------------------------------- /test-files/recipes/modules/brew-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/brew.json 3 | type: brew 4 | auto-update: true 5 | update-interval: "6h" 6 | auto-upgrade: "true" 7 | update-wait-after-boot: "10min" 8 | upgrade-interval: "8h" 9 | upgrade-wait-after-boot: "30min" 10 | nofile-limits: true 11 | brew-analytics: "fasle" 12 | install: 13 | - test 14 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | blue-build-tag: none 7 | cosign-version: none 8 | image-version: latest 9 | stages: 10 | - from-file: stages.yml 11 | modules: 12 | - from-file: common.yml 13 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/bluebuild.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-stage-list-v1.json 3 | stages: 4 | - name: blue-build 5 | from: rust 6 | modules: 7 | - type: script 8 | scripts: 9 | - bluebuild.sh 10 | modules: 11 | - type: copy 12 | from: blue-build 13 | src: /out/bluebuild 14 | dest: /usr/bin/bluebuild 15 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-bluefin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-bluefin 4 | description: This is my personal OS image. 5 | base-image: ghcr.io/ublue-os/bluefin 6 | blue-build-tag: none 7 | cosign-version: none 8 | image-version: stable 9 | stages: 10 | - from-file: stages.yml 11 | modules: 12 | - from-file: common.yml 13 | -------------------------------------------------------------------------------- /integration-tests/test-repo/modules/test-nu-modules/test-nu-modules.nu: -------------------------------------------------------------------------------- 1 | #!/usr/libexec/bluebuild/nu/nu 2 | 3 | def main [$arg] { 4 | # Parse the JSON string into a NuShell table 5 | let parsed_json = ($arg | from json) 6 | 7 | # List all top-level properties and their values 8 | print "Top-level properties and values:" 9 | $parsed_json | items {|key, value| $"Property: ($key), Value: ($value)" } 10 | } 11 | -------------------------------------------------------------------------------- /process/drivers/opts/oci_copy.rs: -------------------------------------------------------------------------------- 1 | use blue_build_utils::container::OciRef; 2 | use bon::Builder; 3 | 4 | #[derive(Debug, Clone, Copy, Builder)] 5 | #[builder(derive(Debug, Clone))] 6 | pub struct CopyOciOpts<'scope> { 7 | pub src_ref: &'scope OciRef, 8 | pub dest_ref: &'scope OciRef, 9 | 10 | #[builder(default)] 11 | pub privileged: bool, 12 | 13 | #[builder(default)] 14 | pub retry_count: u8, 15 | } 16 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/scripts/example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tell this script to exit if there are any errors. 4 | # You should have this in every custom script, to ensure that your completed 5 | # builds actually ran successfully without any errors! 6 | set -oue pipefail 7 | 8 | # Your code goes here. 9 | echo 'This is an example shell script' 10 | echo 'Scripts here will run during build if specified in recipe.yml' 11 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-multiplatform-docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-multiplatform-docker 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | platforms: 8 | - linux/amd64 9 | - linux/arm64 10 | stages: 11 | - from-file: stages.yml 12 | modules: 13 | - from-file: common.yml 14 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-multiplatform-podman.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-multiplatform-podman 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | platforms: 8 | - linux/amd64 9 | - linux/arm64 10 | stages: 11 | - from-file: stages.yml 12 | modules: 13 | - from-file: common.yml 14 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-multiplatform-buildah.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-multiplatform-buildah 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | platforms: 8 | - linux/amd64 9 | - linux/arm64 10 | stages: 11 | - from-file: stages.yml 12 | modules: 13 | - from-file: common.yml 14 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-multiplatform-rechunk.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-multiplatform-rechunk 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | platforms: 8 | - linux/amd64 9 | - linux/arm64 10 | stages: 11 | - from-file: stages.yml 12 | modules: 13 | - from-file: common.yml 14 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-multiplatform-build-chunked-oci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-multiplatform-build-chunked-oci 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: latest 7 | platforms: 8 | - linux/amd64 9 | - linux/arm64 10 | stages: 11 | - from-file: stages.yml 12 | modules: 13 | - from-file: common.yml 14 | -------------------------------------------------------------------------------- /test-files/schema/import-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "import-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "from-file": { 7 | "type": "string", 8 | "description": "The path to another file containing module configuration to import here.\nhttps://blue-build.org/how-to/multiple-files/" 9 | } 10 | }, 11 | "required": [ 12 | "from-file" 13 | ], 14 | "additionalProperties": false 15 | } -------------------------------------------------------------------------------- /test-files/recipes/modules/systemd-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/systemd.json 3 | type: systemd 4 | system: 5 | enabled: 6 | - test.service 7 | disabled: disable-test.service 8 | masked: 9 | - masked-test.service 10 | unmasked: unmasked-test.service 11 | user: 12 | enabled: test.service 13 | disabled: 14 | - disable-test.service 15 | masked: 16 | - test: masked-test.service 17 | unmasked: 18 | - unmasked-test.service 19 | -------------------------------------------------------------------------------- /integration-tests/test-repo/files/scripts/example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tell this script to exit if there are any errors. 4 | # You should have this in every custom script, to ensure that your completed 5 | # builds actually ran successfully without any errors! 6 | set -oue pipefail 7 | 8 | # Your code goes here. 9 | echo 'This is an example shell script' 10 | echo 'Scripts here will run during build if specified in recipe.yml' 11 | 12 | get_json_array FILES '.test[]' '{"test":[1,2,3]}' 13 | 14 | echo "${FILES[@]}" 15 | -------------------------------------------------------------------------------- /test-files/recipes/modules/systemd-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/systemd.json 3 | type: systemd 4 | system: 5 | enabled: 6 | - test.service 7 | disabled: 8 | - disable-test.service 9 | masked: 10 | - masked-test.service 11 | unmasked: 12 | - unmasked-test.service 13 | user: 14 | enabled: 15 | - test.service 16 | disabled: 17 | - disable-test.service 18 | masked: 19 | - masked-test.service 20 | unmasked: 21 | - unmasked-test.service 22 | -------------------------------------------------------------------------------- /template/templates/modules/akmods/akmods.j2: -------------------------------------------------------------------------------- 1 | {%- for info in recipe.modules_ext.get_akmods_info_list(os_version) %} 2 | # Stage for AKmod {{ info.stage_name }} 3 | FROM scratch as stage-akmods-{{ info.stage_name }} 4 | COPY --from=ghcr.io/ublue-os/{{ info.images.0 }} /rpms /rpms 5 | {%- if let Some(extra_image) = info.images.1 %} 6 | COPY --from=ghcr.io/ublue-os/{{ extra_image }} /rpms /rpms 7 | {%- endif %} 8 | {%- if let Some(nv_image) = info.images.2 %} 9 | COPY --from=ghcr.io/ublue-os/{{ nv_image }} /rpms /rpms 10 | {%- endif %} 11 | {%- endfor %} 12 | -------------------------------------------------------------------------------- /integration-tests/mock-scripts/buildah: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | print_version_json() { 4 | local version="1.24.0" 5 | printf '{"version": "%s"}\n' "$version" 6 | } 7 | 8 | main() { 9 | if [[ "$1" == "version" && "$2" == "--json" ]]; then 10 | print_version_json 11 | elif [[ "$1" == "build" && "$7" == *"cli_test.tar.gz" ]]; then 12 | tarpath=$(echo "$7" | awk -F ':' '{print $2}') 13 | echo "Exporting image to a tarball (JK JUST A MOCK!)" 14 | echo "${tarpath}" 15 | touch $tarpath 16 | else 17 | echo 'Running buildah' 18 | fi 19 | } 20 | 21 | main "$@" 22 | -------------------------------------------------------------------------------- /test-files/recipes/modules/default-flatpaks-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/default-flatpaks.json 3 | type: default-flatpaks 4 | notify: 'false' 5 | system: 6 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 7 | repo-name: 8 | - flathub 9 | repo-title: test-user 10 | install: test.org 11 | remove: 12 | - bad-test.org 13 | user: 14 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 15 | repo-name: flathub 16 | repo-title: test-user 17 | install: test.org 18 | remove: 19 | - bad-test.org 20 | -------------------------------------------------------------------------------- /test-files/recipes/modules/default-flatpaks-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/modules/default-flatpaks.json 3 | type: default-flatpaks 4 | notify: false 5 | system: 6 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 7 | repo-name: flathub 8 | repo-title: test-user 9 | install: 10 | - test.org 11 | remove: 12 | - bad-test.org 13 | user: 14 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 15 | repo-name: flathub 16 | repo-title: test-user 17 | install: 18 | - test.org 19 | remove: 20 | - bad-test.org 21 | -------------------------------------------------------------------------------- /integration-tests/mock-scripts/docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | print_version_json() { 4 | local version="24.0.0" 5 | printf '{"Client":{"Version": "%s"}}\n' "$version" 6 | } 7 | 8 | main() { 9 | if [[ "$1" == "version" && "$2" == "-f" && "$3" == "json" ]]; then 10 | print_version_json 11 | elif [[ "$1" == "build" && "$7" == *"cli_test.tar.gz" ]]; then 12 | tarpath=$(echo "$7" | awk -F ':' '{print $2}') 13 | echo "Exporting image to a tarball (JK JUST A MOCK!)" 14 | echo "${tarpath}" 15 | touch $tarpath 16 | else 17 | echo 'Running docker' 18 | fi 19 | } 20 | 21 | main "$@" 22 | -------------------------------------------------------------------------------- /scripts/pre_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if ! command -v jq > /dev/null; then 6 | if command -v rpm-ostree > /dev/null; then 7 | rpm-ostree install jq 8 | else 9 | dnf -y install jq 10 | fi 11 | fi 12 | 13 | optfix_dir="/usr/lib/opt" 14 | 15 | echo "Preparing system for optfix..." 16 | mkdir -pv "${optfix_dir}" 17 | 18 | if [ -d /opt ] || [ -h /opt ]; then 19 | if ls -A /opt/* 2>/dev/null; then 20 | echo "Moving all /opt/* into ${optfix_dir}" 21 | mv -v /opt/* "${optfix_dir}" 22 | fi 23 | rm -fr /opt 24 | fi 25 | 26 | echo "Linking /opt => ${optfix_dir}" 27 | ln -fs "${optfix_dir}" /opt 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The root library for blue-build. 2 | #![doc = include_str!("../README.md")] 3 | 4 | use blue_build_process_management::drivers::types::BuildDriverType; 5 | use blue_build_template::BuildEngine; 6 | 7 | mod build_scripts; 8 | pub mod commands; 9 | 10 | pub use build_scripts::*; 11 | 12 | shadow_rs::shadow!(shadow); 13 | 14 | pub(crate) trait DriverTemplate { 15 | fn build_engine(&self) -> BuildEngine; 16 | } 17 | 18 | impl DriverTemplate for BuildDriverType { 19 | fn build_engine(&self) -> BuildEngine { 20 | match self { 21 | Self::Buildah | Self::Podman => BuildEngine::Oci, 22 | Self::Docker => BuildEngine::Docker, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-files/keys/cosign.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY----- 2 | eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6 3 | OCwicCI6MX0sInNhbHQiOiIvNjdKOVZ3WThhNnJhdk9DQUxmTzFQM05HRDRYc2s2 4 | L005aE5iYVhDNytBPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 5 | Iiwibm9uY2UiOiIvaHQ1MjlSNlhnbkFGbVV6L3U0anlRVE1lb200VDZNVCJ9LCJj 6 | aXBoZXJ0ZXh0IjoiWkpZWWsyR1FhWmdKdEh6UzBKdFVuTWhTblFXc25HcEQzYTVC 7 | MjN3ZVlLb2REbzJkeFVOZXhFSURwODhGUkMzalVSTTRiNTZFSEVjblZVWmFETDNj 8 | Z2ZrTjdNZWVvMThWWVN2Wm13STdYaFJaczExOUc2eWlmaThIcVpGYmdJM21Rd052 9 | MEVEcDFEekw0d2ZJWjBweVAreEEvM2xOeTlteWZSZDZSM1JoR0h5SWt6NVF4eHJ2 10 | WjB1VHZHVExOcmdLSHVzL3NTbis1WktsL1E9PSJ9 11 | -----END ENCRYPTED SIGSTORE PRIVATE KEY----- 12 | -------------------------------------------------------------------------------- /template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blue-build-template" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | askama = { version = "0.14.0", features = ["serde_json"] } 13 | blue-build-recipe = { version = "=0.9.27", path = "../recipe" } 14 | blue-build-utils = { version = "=0.9.27", path = "../utils" } 15 | 16 | bon.workspace = true 17 | colored.workspace = true 18 | log.workspace = true 19 | oci-client.workspace = true 20 | uuid.workspace = true 21 | 22 | [lints] 23 | workspace = true 24 | -------------------------------------------------------------------------------- /process/drivers/opts/ci.rs: -------------------------------------------------------------------------------- 1 | use blue_build_utils::{container::Tag, platform::Platform}; 2 | use bon::Builder; 3 | use oci_client::Reference; 4 | 5 | #[derive(Debug, Clone, Copy, Builder)] 6 | #[builder(derive(Debug, Clone))] 7 | pub struct GenerateTagsOpts<'scope> { 8 | pub oci_ref: &'scope Reference, 9 | 10 | #[builder(into)] 11 | pub alt_tags: Option<&'scope [Tag]>, 12 | 13 | pub platform: Option, 14 | } 15 | 16 | #[derive(Debug, Clone, Copy, Builder)] 17 | #[builder(derive(Debug, Clone))] 18 | pub struct GenerateImageNameOpts<'scope> { 19 | pub name: &'scope str, 20 | pub registry: Option<&'scope str>, 21 | pub registry_namespace: Option<&'scope str>, 22 | pub tag: Option<&'scope Tag>, 23 | } 24 | -------------------------------------------------------------------------------- /recipe/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blue-build-recipe" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | blue-build-utils = { version = "=0.9.27", path = "../utils" } 13 | 14 | cached.workspace = true 15 | colored.workspace = true 16 | log.workspace = true 17 | miette.workspace = true 18 | oci-client.workspace = true 19 | indexmap.workspace = true 20 | reqwest.workspace = true 21 | serde.workspace = true 22 | serde_yaml.workspace = true 23 | serde_json.workspace = true 24 | bon.workspace = true 25 | 26 | [lints] 27 | workspace = true 28 | -------------------------------------------------------------------------------- /utils/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use syntect::dumps; 4 | use syntect::parsing::SyntaxSetBuilder; 5 | use syntect::parsing::syntax_definition::SyntaxDefinition; 6 | 7 | fn main() { 8 | let mut ssb = SyntaxSetBuilder::new(); 9 | ssb.add( 10 | SyntaxDefinition::load_from_str( 11 | include_str!("highlights/Dockerfile.sublime-syntax"), 12 | true, 13 | None, 14 | ) 15 | .unwrap(), 16 | ); 17 | let ss = ssb.build(); 18 | 19 | dumps::dump_to_uncompressed_file( 20 | &ss, 21 | PathBuf::from(env::var("OUT_DIR").unwrap()).join("docker_syntax.bin"), 22 | ) 23 | .unwrap(); 24 | println!("cargo:rerun-if-changed=highlights/Dockerfile.sublime-syntax"); 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/completions.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, CommandFactory}; 2 | use clap_complete::generate; 3 | use miette::Result; 4 | use shells::Shells; 5 | 6 | use crate::commands::BlueBuildArgs; 7 | 8 | use super::BlueBuildCommand; 9 | 10 | mod shells; 11 | 12 | #[derive(Debug, Clone, Args)] 13 | pub struct CompletionsCommand { 14 | #[arg(value_enum)] 15 | shell: Shells, 16 | } 17 | 18 | impl BlueBuildCommand for CompletionsCommand { 19 | fn try_run(&mut self) -> Result<()> { 20 | log::debug!("Generating completions for {}", self.shell); 21 | 22 | generate( 23 | self.shell, 24 | &mut BlueBuildArgs::command(), 25 | "bluebuild", 26 | &mut std::io::stdout().lock(), 27 | ); 28 | 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test-files/schema/modules/signing-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/signing.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "signing", 9 | "description": "The signing module is used to install the required signing policies for cosign image verification with rpm-ostree and bootc.\nhttps://blue-build.org/reference/modules/signing/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | } 16 | }, 17 | "required": [ 18 | "type" 19 | ], 20 | "additionalProperties": false 21 | } -------------------------------------------------------------------------------- /test-files/schema/modules/signing-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/signing.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "signing", 9 | "description": "The signing module is used to install the required signing policies for cosign image verification with rpm-ostree and bootc.\nhttps://blue-build.org/reference/modules/signing/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | } 16 | }, 17 | "required": [ 18 | "type" 19 | ], 20 | "additionalProperties": false 21 | } -------------------------------------------------------------------------------- /integration-tests/mock-scripts/podman: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | print_version_json() { 4 | local version="4.0.0" 5 | printf '{"Client":{"Version": "%s"}}\n' "$version" 6 | } 7 | 8 | main() { 9 | if [[ "$1" == "version" && "$2" == "-f" && "$3" == "json" ]]; then 10 | print_version_json 11 | elif [[ "$1" == "build" && "$7" == *"cli_test.tar.gz" ]]; then 12 | tarpath=$(echo "$7" | awk -F ':' '{print $2}') 13 | echo "Exporting image to a tarball (JK JUST A MOCK!)" 14 | echo "${tarpath}" 15 | touch $tarpath 16 | elif [[ "$1" == "image" && "$2" == "scp" ]]; then 17 | echo "Copying image $3 to $4" 18 | elif [[ "$1" == "rmi" && "$2" == "$BB_TEST_LOCAL_IMAGE" ]]; then 19 | echo "Removing image $2" 20 | else 21 | echo 'Running podman' 22 | fi 23 | } 24 | 25 | main "$@" 26 | -------------------------------------------------------------------------------- /test-files/schema/stage-list-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "stage-list-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "stages": { 7 | "type": "array", 8 | "items": { 9 | "$ref": "#/$defs/StageEntry" 10 | }, 11 | "description": "A list of [stages](https://blue-build.org/reference/stages/) that are executed before the build of the final image.\nThis is useful for compiling programs from source without polluting the final bootable image." 12 | } 13 | }, 14 | "required": [ 15 | "stages" 16 | ], 17 | "$defs": { 18 | "StageEntry": { 19 | "anyOf": [ 20 | { 21 | "$ref": "stage-v1.json" 22 | }, 23 | { 24 | "$ref": "import-v1.json" 25 | } 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /process/drivers/opts.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | pub use boot::*; 4 | pub use build::*; 5 | pub use build_chunked_oci::*; 6 | pub use ci::*; 7 | pub use inspect::*; 8 | pub use oci_copy::*; 9 | pub use rechunk::*; 10 | pub use run::*; 11 | pub use signing::*; 12 | 13 | mod boot; 14 | mod build; 15 | mod build_chunked_oci; 16 | mod ci; 17 | mod inspect; 18 | mod oci_copy; 19 | mod rechunk; 20 | mod run; 21 | mod signing; 22 | 23 | #[derive(Debug, Copy, Clone, Default, ValueEnum)] 24 | pub enum CompressionType { 25 | #[default] 26 | Gzip, 27 | Zstd, 28 | } 29 | 30 | impl std::fmt::Display for CompressionType { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | f.write_str(match self { 33 | Self::Zstd => "zstd", 34 | Self::Gzip => "gzip", 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /template/templates/modules/copy/copy.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/json-schema"; 2 | using TypeSpec.JsonSchema; 3 | 4 | @jsonSchema("/modules/copy-latest.json") 5 | model CopyModuleLatest { 6 | ...CopyModuleV1; 7 | } 8 | 9 | @jsonSchema("/modules/copy-v1.json") 10 | model CopyModuleV1 { 11 | /** The copy module is a short-hand method of adding a COPY instruction into the Containerfile. 12 | * https://blue-build.org/reference/modules/copy/ 13 | */ 14 | type: "copy" | "copy@latest" | "copy@v1"; 15 | 16 | /** Equivalent to the --from property in a COPY statement, use to specify an image to copy from. 17 | * By default, the COPY source is the build environment's file tree. 18 | */ 19 | from?: string; 20 | 21 | /** Path to source file or directory. */ 22 | src: string; 23 | 24 | /** Path to destination file or directory. */ 25 | dest: string; 26 | } 27 | -------------------------------------------------------------------------------- /process/process.rs: -------------------------------------------------------------------------------- 1 | //! This module is responsible for managing processes spawned 2 | //! by this tool. It contains drivers for running, building, inspecting, and signing 3 | //! images that interface with tools like docker or podman. 4 | 5 | pub mod drivers; 6 | pub mod logging; 7 | pub mod signal_handler; 8 | 9 | pub static ASYNC_RUNTIME: std::sync::LazyLock = 10 | std::sync::LazyLock::new(|| { 11 | tokio::runtime::Builder::new_multi_thread() 12 | .enable_all() 13 | .build() 14 | .unwrap() 15 | }); 16 | 17 | #[cfg(test)] 18 | pub(crate) mod test { 19 | use std::sync::LazyLock; 20 | 21 | pub const TEST_TAG_1: &str = "test-tag-1"; 22 | pub const TEST_TAG_2: &str = "test-tag-2"; 23 | 24 | pub static TIMESTAMP: LazyLock = LazyLock::new(blue_build_utils::get_tag_timestamp); 25 | } 26 | -------------------------------------------------------------------------------- /template/templates/modules/containerfile/containerfile.tsp: -------------------------------------------------------------------------------- 1 | import "@typespec/json-schema"; 2 | using TypeSpec.JsonSchema; 3 | 4 | @jsonSchema("/modules/containerfile-latest.json") 5 | model ContainerfileModuleLatest { 6 | ...ContainerfileModuleV1; 7 | } 8 | 9 | @jsonSchema("/modules/containerfile-v1.json") 10 | model ContainerfileModuleV1 { 11 | /** The containerfile module is a tool for adding custom Containerfile instructions for custom image builds. 12 | * https://blue-build.org/reference/modules/containerfile/ 13 | */ 14 | type: "containerfile" | "containerfile@latest" | "containerfile@v1"; 15 | 16 | /** Lines to directly insert into the generated Containerfile. */ 17 | snippets?: Array; 18 | 19 | /** Names of directories in ./containerfiles/ containing each a Containerfile to insert into the generated Containerfile. */ 20 | containerfiles?: Array; 21 | } 22 | -------------------------------------------------------------------------------- /test-files/schema/module-custom-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "module-custom-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "description": "This is not a built-in module." 9 | }, 10 | "source": { 11 | "type": "string", 12 | "description": "The image ref of the module repository (an OCI image) to pull the module from.\nIf this is a local module, set the value to 'local'.\nhttps://blue-build.org/reference/module/#source-optional" 13 | }, 14 | "no-cache": { 15 | "type": "boolean", 16 | "default": false, 17 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 18 | } 19 | }, 20 | "required": [ 21 | "type", 22 | "source" 23 | ], 24 | "additionalProperties": {} 25 | } -------------------------------------------------------------------------------- /test-files/schema/module-list-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "module-list-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "modules": { 7 | "type": "array", 8 | "items": { 9 | "$ref": "#/$defs/ModuleEntry" 10 | }, 11 | "description": "A list of [modules](https://blue-build.org/reference/module/) that is executed in order. Multiple of the same module can be included.\n\nEach item in this list should have at least a `type:` or be specified to be included from an external file in the `recipes/` directory with `from-file:`." 12 | } 13 | }, 14 | "required": [ 15 | "modules" 16 | ], 17 | "$defs": { 18 | "ModuleEntry": { 19 | "anyOf": [ 20 | { 21 | "$ref": "module-v1.json" 22 | }, 23 | { 24 | "$ref": "import-v1.json" 25 | } 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /integration-tests/mock-scripts/rpm-ostree: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | if [ "$1" = "rebase" ]; then 6 | if [ "$2" = "ostree-unverified-image:containers-storage:$BB_TEST_LOCAL_IMAGE" ]; then 7 | echo "Rebased to local image $BB_TEST_LOCAL_IMAGE" 8 | else 9 | echo "Failed to rebase" 10 | exit 1 11 | fi 12 | elif [ "$1" = "upgrade" ]; then 13 | echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE" 14 | elif [ "$1" = "status" ]; then 15 | cat < /test.txt 29 | - type: test-module 30 | source: local 31 | - type: containerfile 32 | containerfiles: 33 | - labels 34 | snippets: 35 | - RUN echo "This is a snippet" 36 | - type: rpm-ostree 37 | install: micro 38 | -------------------------------------------------------------------------------- /scripts/post_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | . /scripts/exports.sh 5 | 6 | shopt -s nullglob 7 | 8 | optfix_dir="/usr/lib/opt" 9 | # needs nullglob, so that this array is empty if /opt is empty 10 | optdirs=("${optfix_dir}"/*) # returns a list of directories in /opt 11 | if [[ -n "${optdirs[*]}" ]]; then 12 | echo "Creating symlinks to fix packages that installed to /opt:" 13 | for optdir in "${optdirs[@]}"; do 14 | opt=$(basename "${optdir}") 15 | lib_opt_dir="${optfix_dir}/${opt}" 16 | link_opt_dir="/opt/${opt}" 17 | echo "Linking ${link_opt_dir} => ${lib_opt_dir}" 18 | echo "L+? \"${link_opt_dir}\" - - - - ${lib_opt_dir}" | tee "/usr/lib/tmpfiles.d/99-bluebuild-optfix-${opt}.conf" 19 | done 20 | fi 21 | 22 | rm -rf /tmp/* /var/* /opt 23 | ln -fs /var/opt /opt 24 | 25 | # if feature_enabled "bootc" && command -v bootc > /dev/null; then 26 | # bootc container lint 27 | # fi 28 | -------------------------------------------------------------------------------- /integration-tests/mock-scripts/bootc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | if [ "$1" = "switch" ]; then 6 | if [[ "$2" == "--transport=containers-storage" && "$3" == "$BB_TEST_LOCAL_IMAGE" ]]; then 7 | echo "Rebased to local image $BB_TEST_LOCAL_IMAGE" 8 | else 9 | echo "Failed to rebase" 10 | exit 1 11 | fi 12 | elif [ "$1" = "upgrade" ]; then 13 | echo "Performing upgrade for $BB_TEST_LOCAL_IMAGE" 14 | elif [ "$1" = "status" ]; then 15 | cat < /test.txt 30 | - type: test-module 31 | source: local 32 | - type: containerfile 33 | containerfiles: 34 | - labels 35 | snippets: 36 | - RUN echo "This is a snippet" 37 | - type: test-nu-modules 38 | source: local 39 | test-prop: 40 | - this 41 | - is 42 | - a 43 | - test 44 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main branch build 2 | 3 | permissions: {} 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-main 7 | cancel-in-progress: true 8 | 9 | on: 10 | workflow_dispatch: 11 | push: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | build: 17 | uses: ./.github/workflows/build.yml 18 | if: github.repository == 'blue-build/cli' 19 | permissions: 20 | contents: read # read repo contents 21 | packages: write # write package to ghcr 22 | id-token: write # docker auth 23 | actions: read # Provenance 24 | with: 25 | repo: ${{ github.repository }} 26 | ref: main 27 | secrets: 28 | SIGNING_SECRET: ${{ secrets.SIGNING_SECRET }} 29 | test: 30 | uses: ./.github/workflows/test.yml 31 | permissions: 32 | contents: read # read repo contents 33 | packages: write # write package to ghcr 34 | id-token: write # docker auth 35 | with: 36 | repo: ${{ github.repository }} 37 | ref: main 38 | secrets: 39 | TEST_SIGNING_SECRET: ${{ secrets.TEST_SIGNING_SECRET }} 40 | -------------------------------------------------------------------------------- /.github/workflows/flakehub-tagged.yml: -------------------------------------------------------------------------------- 1 | name: "Publish tags to FlakeHub" 2 | permissions: {} 3 | on: 4 | push: 5 | tags: 6 | - "v?[0-9]+.[0-9]+.[0-9]+*" 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: "The existing tag to publish to FlakeHub" 11 | type: "string" 12 | required: true 13 | jobs: 14 | flakehub-publish: 15 | runs-on: "ubuntu-latest" 16 | permissions: 17 | id-token: write # login to flakehub 18 | contents: read # read repo contents 19 | steps: 20 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | with: 22 | persist-credentials: false 23 | ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" 24 | - uses: DeterminateSystems/nix-installer-action@c5a866b6ab867e88becbed4467b93592bce69f8a # v21 25 | - uses: DeterminateSystems/flakehub-push@71f57208810a5d299fc6545350981de98fdbc860 # v6 26 | with: 27 | visibility: "public" 28 | name: "blue-build/cli" 29 | tag: "${{ inputs.tag }}" 30 | -------------------------------------------------------------------------------- /template/templates/github_issue.j2: -------------------------------------------------------------------------------- 1 | #### Current Behavior 2 | 3 | 4 | #### Expected Behavior 5 | 6 | 7 | #### Additional context/Screenshots 8 | 9 | 10 | #### Possible Solution 11 | 12 | 13 | #### Environment 14 | - Blue Build Version: {{ bb_version }} 15 | - Operating system: {{ os_name }} {{ os_version }} 16 | - Branch/Tag: {{ pkg_branch_tag }} 17 | - Git Commit Hash: {{ git_commit_hash }} 18 | 19 | #### Shell 20 | - Name: {{ shell_name }} 21 | - Version: {{ shell_version }} 22 | - Terminal emulator: {{ terminal_name }} {{ terminal_version }} 23 | 24 | #### Rust 25 | - Rust Version: {{ rust_version }} 26 | - Rust channel: {{ rust_channel }} {{ build_rust_channel }} 27 | - Build Time: {{ build_time }} 28 | 29 | {%- if !recipe.is_empty() %} 30 | 31 | #### Recipe: 32 | ```yml 33 | {{ recipe }} 34 | ``` 35 | {%- endif %} 36 | -------------------------------------------------------------------------------- /test-files/schema/modules/script-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/script.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "script", 9 | "description": "The script module can be used to run arbitrary bash snippets and scripts at image build time.\nhttps://blue-build.org/reference/modules/script/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "snippets": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "List of bash one-liners to run." 22 | }, 23 | "scripts": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "List of script files to run." 29 | } 30 | }, 31 | "required": [ 32 | "type" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /test-files/schema/modules/script-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/script.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "script", 9 | "description": "The script module can be used to run arbitrary bash snippets and scripts at image build time.\nhttps://blue-build.org/reference/modules/script/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "snippets": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "List of bash one-liners to run." 22 | }, 23 | "scripts": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "List of script files to run." 29 | } 30 | }, 31 | "required": [ 32 | "type" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 🌈 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | zizmor: 13 | name: zizmor latest via PyPI 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write # write security events to github 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Install the latest version of uv 24 | uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 25 | 26 | - name: Run zizmor 🌈 27 | run: uvx zizmor --format sarif . > results.sarif 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Upload SARIF file 32 | uses: github/codeql-action/upload-sarif@755f44910c12a3d7ca0d8c6e42c048b3362f7cec # v3.30.8 33 | with: 34 | sarif_file: results.sarif 35 | category: zizmor 36 | -------------------------------------------------------------------------------- /integration-tests/legacy-test-repo/config/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-legacy 4 | description: This is my personal OS image. 5 | base-image: ghcr.io/ublue-os/silverblue-surface 6 | image-version: gts 7 | modules: 8 | - type: files 9 | files: 10 | - usr: /usr 11 | 12 | - type: script 13 | scripts: 14 | - example.sh 15 | 16 | - type: rpm-ostree 17 | repos: 18 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 19 | install: 20 | - micro 21 | - starship 22 | remove: 23 | - firefox 24 | - firefox-langpacks 25 | 26 | - type: default-flatpaks@v1 27 | notify: true 28 | system: 29 | install: 30 | - org.mozilla.firefox 31 | - org.gnome.Loupe 32 | remove: 33 | - org.gnome.eog 34 | 35 | - type: signing 36 | 37 | - type: test-module 38 | source: local 39 | 40 | - type: containerfile 41 | containerfiles: 42 | - labels 43 | snippets: 44 | - RUN echo "This is a snippet" 45 | -------------------------------------------------------------------------------- /recipe/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod akmods_info; 2 | mod maybe_version; 3 | mod module; 4 | mod module_ext; 5 | mod recipe; 6 | mod stage; 7 | mod stages_ext; 8 | 9 | use std::path::{Path, PathBuf}; 10 | 11 | use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH}; 12 | use log::warn; 13 | 14 | pub use akmods_info::*; 15 | pub use maybe_version::*; 16 | pub use module::*; 17 | pub use module_ext::*; 18 | pub use recipe::*; 19 | pub use stage::*; 20 | pub use stages_ext::*; 21 | 22 | pub trait FromFileList { 23 | const LIST_KEY: &str; 24 | 25 | fn get_from_file_paths(&self) -> Vec; 26 | 27 | fn get_module_from_file_paths(&self) -> Vec { 28 | Vec::new() 29 | } 30 | } 31 | 32 | pub(crate) fn base_recipe_path() -> &'static Path { 33 | let legacy_path = Path::new(CONFIG_PATH); 34 | let recipe_path = Path::new(RECIPE_PATH); 35 | 36 | if recipe_path.exists() && recipe_path.is_dir() { 37 | recipe_path 38 | } else { 39 | warn!( 40 | "Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}" 41 | ); 42 | legacy_path 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -f /etc/os-release ]; then 4 | # Initialize variable to store the ID 5 | ID="" 6 | 7 | # Read the /etc/os-release file line by line 8 | while IFS== read -r key value; do 9 | # Check if the key is 'ID' 10 | if [ "$key" = "ID" ]; then 11 | # Remove any quotes from the value and store it in id variable 12 | ID=$(echo "$value" | tr -d '"') 13 | break 14 | fi 15 | done < /etc/os-release 16 | 17 | if [ "$ID" = "alpine" ]; then 18 | echo "Setting up Alpine based image to run BlueBuild modules" 19 | apk update 20 | apk add --no-cache bash curl coreutils wget grep jq 21 | elif [ "$ID" = "ubuntu" ] || [ "$ID" = "debian" ]; then 22 | echo "Setting up Ubuntu based image to run BlueBuild modules" 23 | export DEBIAN_FRONTEND=noninteractive 24 | apt-get update 25 | apt-get install -y bash curl coreutils wget jq 26 | elif [ "$ID" = "fedora" ]; then 27 | echo "Settig up Fedora based image to run BlueBuild modules" 28 | dnf install -y --refresh bash curl wget coreutils jq 29 | else 30 | echo "OS not detected, proceeding without setup" 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-gts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test 4 | description: This is my personal OS image. 5 | base-image: ghcr.io/ublue-os/silverblue-main 6 | nushell-version: none 7 | blue-build-tag: none 8 | alt-tags: 9 | - gts 10 | - stable 11 | image-version: gts 12 | modules: 13 | - from-file: akmods.yml 14 | - from-file: flatpaks.yml 15 | 16 | - type: files 17 | files: 18 | - source: usr 19 | destination: /usr 20 | 21 | - type: script 22 | scripts: 23 | - example.sh 24 | 25 | - type: dnf 26 | repos: 27 | files: 28 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 29 | install: 30 | packages: 31 | - micro 32 | - starship 33 | remove: 34 | packages: 35 | - firefox 36 | - firefox-langpacks 37 | 38 | - type: signing 39 | 40 | - type: test-module 41 | source: local 42 | 43 | - type: containerfile 44 | containerfiles: 45 | - labels 46 | snippets: 47 | - RUN echo "This is a snippet" 48 | -------------------------------------------------------------------------------- /test-files/schema/modules/yafti-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/yafti.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "yafti", 9 | "description": "The yafti module can be used to install yafti and set it up to run on first boot.\nhttps://blue-build.org/reference/modules/yafti/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "custom-flatpaks": { 17 | "type": "array", 18 | "items": { 19 | "$ref": "#/$defs/RecordString" 20 | }, 21 | "description": "List of custom Flatpaks to inject to the default yafti.yml. Format is: `PrettyName: org.example.flatpak_id`" 22 | } 23 | }, 24 | "required": [ 25 | "type" 26 | ], 27 | "additionalProperties": false, 28 | "$defs": { 29 | "RecordString": { 30 | "type": "object", 31 | "properties": {}, 32 | "additionalProperties": { 33 | "type": "string" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test-files/schema/modules/yafti-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/yafti.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "yafti", 9 | "description": "The yafti module can be used to install yafti and set it up to run on first boot.\nhttps://blue-build.org/reference/modules/yafti/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "custom-flatpaks": { 17 | "type": "array", 18 | "items": { 19 | "$ref": "#/$defs/RecordString" 20 | }, 21 | "description": "List of custom Flatpaks to inject to the default yafti.yml. Format is: `PrettyName: org.example.flatpak_id`" 22 | } 23 | }, 24 | "required": [ 25 | "type" 26 | ], 27 | "additionalProperties": false, 28 | "$defs": { 29 | "RecordString": { 30 | "type": "object", 31 | "properties": {}, 32 | "additionalProperties": { 33 | "type": "string" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR build 2 | 3 | permissions: {} 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 7 | cancel-in-progress: true 8 | 9 | on: 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | if: github.repository == 'blue-build/cli' 16 | permissions: 17 | contents: read # read repo contents 18 | packages: write # write package to ghcr 19 | id-token: write # docker auth 20 | actions: read # Provenance 21 | with: 22 | repo: ${{ github.event.pull_request.head.repo.full_name }} 23 | ref: ${{ github.event.pull_request.head.ref }} 24 | secrets: 25 | SIGNING_SECRET: ${{ secrets.SIGNING_SECRET }} 26 | test: 27 | uses: ./.github/workflows/test.yml 28 | permissions: 29 | contents: read # read repo contents 30 | packages: write # write package to ghcr 31 | id-token: write # docker auth 32 | with: 33 | repo: ${{ github.event.pull_request.head.repo.full_name }} 34 | ref: ${{ github.event.pull_request.head.ref }} 35 | pr_event_number: ${{ github.event.number }} 36 | secrets: 37 | TEST_SIGNING_SECRET: ${{ secrets.TEST_SIGNING_SECRET }} 38 | -------------------------------------------------------------------------------- /template/templates/modules/copy/README.md: -------------------------------------------------------------------------------- 1 | # `copy` 2 | 3 | :::caution 4 | Only compiler-based builds can use this module as it is built-in to the BlueBuild CLI tool. 5 | ::: 6 | 7 | The `copy` module is a short-hand method of adding a [`COPY`](https://docs.docker.com/reference/dockerfile/#copy) instruction into the image. This can be used to copy files from images, other stages, or even from the build context. 8 | 9 | ## Usage 10 | 11 | The `copy` module's properties are a 1-1 match with the `COPY` instruction containing `src`, `dest`, and `from` (optional). The example below will `COPY` the file `/usr/bin/yq` from `docker.io/mikefarah/yq` into `/usr/bin/`. 12 | 13 | ```yaml 14 | modules: 15 | - type: copy 16 | from: docker.io/mikefarah/yq 17 | src: /usr/bin/yq 18 | dest: /usr/bin/ 19 | ``` 20 | 21 | Creating an instruction like: 22 | 23 | ```dockerfile 24 | COPY --linked --from=docker.io/mikefarah/yq /usr/bin/yq /usr/bin/ 25 | ``` 26 | 27 | Omitting `from:` will allow copying from the build context: 28 | 29 | ```yaml 30 | modules: 31 | - type: copy 32 | src: file/to/copy.conf 33 | dest: /usr/etc/app/ 34 | ``` 35 | 36 | Creating an instruction like: 37 | 38 | ```dockerfile 39 | COPY --linked file/to/copy.conf /usr/etc/app/ 40 | ``` 41 | -------------------------------------------------------------------------------- /test-files/schema/modules/justfiles-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/justfiles.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "justfiles", 9 | "description": "The justfiles module makes it easy to include just recipes from multiple files in Universal Blue -based images.\nhttps://blue-build.org/reference/modules/justfiles/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "validate": { 17 | "type": "boolean", 18 | "default": false, 19 | "description": "Whether to validate the syntax of the justfiles against `just --fmt`. (warning: can be very unforgiving)" 20 | }, 21 | "include": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "description": "List of files or subfolders to include into this image. If omitted, all justfiles will be included." 27 | } 28 | }, 29 | "required": [ 30 | "type" 31 | ], 32 | "additionalProperties": false 33 | } -------------------------------------------------------------------------------- /test-files/schema/modules/copy-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/copy.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "copy", 9 | "description": "The copy module is a short-hand method of adding a COPY instruction into the Containerfile.\nhttps://blue-build.org/reference/modules/copy/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "from": { 17 | "type": "string", 18 | "description": "Equivalent to the --from property in a COPY statement, use to specify an image to copy from.\nBy default, the COPY source is the build environment's file tree." 19 | }, 20 | "src": { 21 | "type": "string", 22 | "description": "Path to source file or directory." 23 | }, 24 | "dest": { 25 | "type": "string", 26 | "description": "Path to destination file or directory." 27 | } 28 | }, 29 | "required": [ 30 | "type", 31 | "src", 32 | "dest" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /test-files/schema/modules/justfiles-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/justfiles.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "justfiles", 9 | "description": "The justfiles module makes it easy to include just recipes from multiple files in Universal Blue -based images.\nhttps://blue-build.org/reference/modules/justfiles/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "validate": { 17 | "type": "boolean", 18 | "default": false, 19 | "description": "Whether to validate the syntax of the justfiles against `just --fmt`. (warning: can be very unforgiving)" 20 | }, 21 | "include": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "description": "List of files or subfolders to include into this image. If omitted, all justfiles will be included." 27 | } 28 | }, 29 | "required": [ 30 | "type" 31 | ], 32 | "additionalProperties": false 33 | } -------------------------------------------------------------------------------- /test-files/schema/modules/copy-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/copy.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "copy", 9 | "description": "The copy module is a short-hand method of adding a COPY instruction into the Containerfile.\nhttps://blue-build.org/reference/modules/copy/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "from": { 17 | "type": "string", 18 | "description": "Equivalent to the --from property in a COPY statement, use to specify an image to copy from.\nBy default, the COPY source is the build environment's file tree." 19 | }, 20 | "src": { 21 | "type": "string", 22 | "description": "Path to source file or directory." 23 | }, 24 | "dest": { 25 | "type": "string", 26 | "description": "Path to destination file or directory." 27 | } 28 | }, 29 | "required": [ 30 | "type", 31 | "src", 32 | "dest" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /test-files/schema/modules/containerfile-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/containerfile.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "containerfile", 9 | "description": "The containerfile module is a tool for adding custom Containerfile instructions for custom image builds. \nhttps://blue-build.org/reference/modules/containerfile/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "snippets": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "Lines to directly insert into the generated Containerfile." 22 | }, 23 | "containerfiles": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "Names of directories in ./containerfiles/ containing each a Containerfile to insert into the generated Containerfile." 29 | } 30 | }, 31 | "required": [ 32 | "type" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /test-files/schema/modules/containerfile-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/containerfile.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "containerfile", 9 | "description": "The containerfile module is a tool for adding custom Containerfile instructions for custom image builds. \nhttps://blue-build.org/reference/modules/containerfile/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "snippets": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "Lines to directly insert into the generated Containerfile." 22 | }, 23 | "containerfiles": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "Names of directories in ./containerfiles/ containing each a Containerfile to insert into the generated Containerfile." 29 | } 30 | }, 31 | "required": [ 32 | "type" 33 | ], 34 | "additionalProperties": false 35 | } -------------------------------------------------------------------------------- /recipe/src/maybe_version.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Default, Clone, Debug)] 4 | pub enum MaybeVersion { 5 | #[default] 6 | None, 7 | VersionOrBranch(String), 8 | } 9 | 10 | impl std::fmt::Display for MaybeVersion { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | write!( 13 | f, 14 | "{}", 15 | match self { 16 | Self::None => "none".to_string(), 17 | Self::VersionOrBranch(version) => version.clone(), 18 | } 19 | ) 20 | } 21 | } 22 | 23 | impl<'de> Deserialize<'de> for MaybeVersion { 24 | fn deserialize(deserializer: D) -> std::result::Result 25 | where 26 | D: serde::Deserializer<'de>, 27 | { 28 | let val = String::deserialize(deserializer)?; 29 | 30 | Ok(match val { 31 | none if none.to_lowercase() == "none" => Self::None, 32 | version => Self::VersionOrBranch(version), 33 | }) 34 | } 35 | } 36 | 37 | impl Serialize for MaybeVersion { 38 | fn serialize(&self, serializer: S) -> std::result::Result 39 | where 40 | S: serde::Serializer, 41 | { 42 | serializer.serialize_str(&self.to_string()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test-files/schema/modules/fonts-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/fonts.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "fonts", 9 | "description": "The fonts module can be used to install fonts from Nerd Fonts or Google Fonts. \nhttps://blue-build.org/reference/modules/fonts/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "fonts": { 17 | "type": "object", 18 | "properties": { 19 | "nerd-fonts": { 20 | "type": "array", 21 | "items": { 22 | "type": "string" 23 | }, 24 | "description": "List of Nerd Fonts to install (without the \"Nerd Font\" suffix)." 25 | }, 26 | "google-fonts": { 27 | "type": "array", 28 | "items": { 29 | "type": "string" 30 | }, 31 | "description": "List of Google Fonts to install." 32 | } 33 | } 34 | } 35 | }, 36 | "required": [ 37 | "type", 38 | "fonts" 39 | ], 40 | "additionalProperties": false 41 | } -------------------------------------------------------------------------------- /test-files/schema/modules/fonts-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/fonts.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "fonts", 9 | "description": "The fonts module can be used to install fonts from Nerd Fonts or Google Fonts. \nhttps://blue-build.org/reference/modules/fonts/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "fonts": { 17 | "type": "object", 18 | "properties": { 19 | "nerd-fonts": { 20 | "type": "array", 21 | "items": { 22 | "type": "string" 23 | }, 24 | "description": "List of Nerd Fonts to install (without the \"Nerd Font\" suffix)." 25 | }, 26 | "google-fonts": { 27 | "type": "array", 28 | "items": { 29 | "type": "string" 30 | }, 31 | "description": "List of Google Fonts to install." 32 | } 33 | } 34 | } 35 | }, 36 | "required": [ 37 | "type", 38 | "fonts" 39 | ], 40 | "additionalProperties": false 41 | } -------------------------------------------------------------------------------- /process/drivers/opts/build_chunked_oci.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU32; 2 | 3 | use blue_build_utils::constants::DEFAULT_MAX_LAYERS; 4 | use bon::Builder; 5 | 6 | use super::BuildTagPushOpts; 7 | 8 | #[derive(Debug, Clone, Builder)] 9 | #[builder(derive(Debug, Clone))] 10 | pub struct BuildRechunkTagPushOpts<'scope> { 11 | pub build_tag_push_opts: BuildTagPushOpts<'scope>, 12 | pub rechunk_opts: BuildChunkedOciOpts, 13 | } 14 | 15 | #[derive(Debug, Clone, Copy, Builder)] 16 | #[builder(derive(Debug, Clone))] 17 | pub struct BuildChunkedOciOpts { 18 | /// Format version for `build-chunked-oci`. 19 | #[builder(default = BuildChunkedOciFormatVersion::V2)] 20 | pub format_version: BuildChunkedOciFormatVersion, 21 | 22 | /// Maximum number of layers to use. Currently defaults to 64 if not specified. 23 | #[builder(default = DEFAULT_MAX_LAYERS)] 24 | pub max_layers: NonZeroU32, 25 | } 26 | 27 | #[derive(Debug, Clone, Copy)] 28 | pub enum BuildChunkedOciFormatVersion { 29 | V1, 30 | V2, 31 | } 32 | 33 | impl std::fmt::Display for BuildChunkedOciFormatVersion { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | write!( 36 | f, 37 | "{}", 38 | match self { 39 | Self::V1 => "1", 40 | Self::V2 => "2", 41 | } 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-invalid.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-invalid 4 | description: 10 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: 7 | - 40 8 | - 39 9 | stages: {} 10 | modules: 11 | - from-file: akmods.yml 12 | - from-file: flatpaks.yml 13 | 14 | - type: files 15 | files: 16 | - source: usr 17 | destination: /usr 18 | 19 | - type: script 20 | scripts: 21 | - example.sh 22 | 23 | - type: rpm-ostree 24 | repos: 25 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 26 | install: 27 | - micro 28 | - starship 29 | remove: 30 | - firefox 31 | - firefox-langpacks 32 | 33 | - type: signing 34 | 35 | - type: test-module 36 | source: local 37 | 38 | - type: containerfile 39 | containerfiles: 40 | - labels 41 | snippets: 42 | - RUN echo "This is a snippet" 43 | 44 | - type: copy 45 | from: alpine-test 46 | src: /test.txt 47 | dest: / 48 | - type: copy 49 | from: ubuntu-test 50 | src: /test.txt 51 | dest: / 52 | - type: copy 53 | from: debian-test 54 | src: /test.txt 55 | dest: / 56 | - type: copy 57 | from: fedora-test 58 | src: /test.txt 59 | dest: / 60 | -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blue-build-utils" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | base64 = "0.22.1" 13 | blake2 = "0.10.6" 14 | constcat = "0.6.1" 15 | directories = "6.0.0" 16 | docker_credential = "1.3.2" 17 | process_control = { version = "4.2.2", features = ["crossbeam-channel"] } 18 | 19 | bon.workspace = true 20 | cached.workspace = true 21 | chrono.workspace = true 22 | clap = { workspace = true, features = ["derive", "env"] } 23 | comlexr.workspace = true 24 | lazy-regex.workspace = true 25 | log.workspace = true 26 | miette.workspace = true 27 | nix = { workspace = true, features = ["user"] } 28 | oci-client.workspace = true 29 | reqwest.workspace = true 30 | semver = { workspace = true, features = ["serde"] } 31 | serde.workspace = true 32 | serde_json.workspace = true 33 | serde_yaml.workspace = true 34 | syntect.workspace = true 35 | tempfile.workspace = true 36 | uuid.workspace = true 37 | which.workspace = true 38 | zeroize.workspace = true 39 | 40 | [build-dependencies] 41 | syntect.workspace = true 42 | 43 | [dev-dependencies] 44 | rstest.workspace = true 45 | 46 | [lints] 47 | workspace = true 48 | 49 | [features] 50 | test = [] 51 | -------------------------------------------------------------------------------- /test-files/recipes/recipe-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test 4 | description: This is my personal OS image. 5 | base-image: ghcr.io/ublue-os/silverblue-main 6 | image-version: latest 7 | stages: 8 | - from: test 9 | name: 10 | - test 11 | modules: 12 | type: script 13 | snippets: 14 | - echo test 15 | modules: 16 | - type: files 17 | files: 18 | source: usr 19 | destination: /usr 20 | 21 | - type: script 22 | scripts: 23 | - example.sh 24 | 25 | - type: rpm-ostree 26 | repos: 27 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 28 | install: 29 | - micro 30 | - starship 31 | remove: 32 | - firefox 33 | - firefox-langpacks 34 | 35 | - type: signing 36 | 37 | - type: test-module 38 | 39 | - type: containerfile 40 | containerfiles: labels 41 | snippets: 42 | - RUN echo "This is a snippet" 43 | 44 | - type: copy 45 | from: alpine-test 46 | src: 47 | src: /test.txt 48 | dest: / 49 | - type: copy 50 | from: ubuntu-test 51 | src: /test.txt 52 | dest: / 53 | - type: copy 54 | from: debian-test 55 | src: /test.txt 56 | dest: / 57 | - type: copy 58 | from: fedora-test 59 | src: /test.txt 60 | dest: / 61 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-invalid-module.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-invalid-module 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: 40 7 | stages: 8 | - from-file: stages.yml 9 | modules: 10 | - from-file: akmods.yml 11 | - from-file: flatpaks.yml 12 | 13 | - type: files 14 | files: 15 | - source: usr 16 | destination: /usr 17 | 18 | - type: script 19 | scripts: 20 | - example.sh 21 | 22 | - type: rpm-ostree 23 | repos: 24 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 25 | install: micro 26 | installer: test 27 | remove: 28 | - firefox 29 | - firefox-langpacks 30 | 31 | - type: signing 32 | 33 | - type: test-module 34 | source: local 35 | 36 | - type: containerfile 37 | containerfiles: 38 | labels: labels 39 | snippets: 40 | - RUN echo "This is a snippet" 41 | 42 | - type: copy 43 | from: alpine-test 44 | src: /test.txt 45 | dest: / 46 | - type: copy 47 | from: ubuntu-test 48 | src: /test.txt 49 | dest: / 50 | - type: copy 51 | from: debian-test 52 | src: /test.txt 53 | dest: / 54 | - type: copy 55 | from: fedora-test 56 | src: /test.txt 57 | dest: / 58 | -------------------------------------------------------------------------------- /template/templates/init/gitlab-ci.yml.j2: -------------------------------------------------------------------------------- 1 | workflow: 2 | rules: 3 | - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push" 4 | when: never 5 | - if: "$CI_COMMIT_TAG" 6 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 7 | - if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS" 8 | when: never 9 | - if: "$CI_COMMIT_BRANCH" 10 | 11 | stages: 12 | - build 13 | 14 | build-image: 15 | stage: build 16 | image: 17 | name: ghcr.io/blue-build/cli:{{ version }} 18 | entrypoint: [""] 19 | services: 20 | - docker:dind 21 | parallel: 22 | matrix: 23 | - RECIPE: 24 | # Add your recipe files here 25 | - recipe.yml 26 | variables: 27 | # Setup a secure connection with docker-in-docker service 28 | # https://docs.gitlab.com/ee/ci/docker/using_docker_build.html 29 | DOCKER_HOST: tcp://docker:2376 30 | DOCKER_TLS_CERTDIR: /certs 31 | DOCKER_TLS_VERIFY: 1 32 | DOCKER_CERT_PATH: $DOCKER_TLS_CERTDIR/client 33 | before_script: 34 | # Pulls secure files into the build 35 | - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash 36 | - export COSIGN_PRIVATE_KEY=$(cat .secure_files/cosign.key) 37 | script: 38 | - sleep 5 # Wait a bit for the docker-in-docker service to start 39 | - bluebuild build --push ./recipes/$RECIPE 40 | 41 | -------------------------------------------------------------------------------- /test-files/recipes/recipe-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test 4 | description: This is my personal OS image. 5 | base-image: ghcr.io/ublue-os/silverblue-main 6 | image-version: latest 7 | stages: 8 | - from: test 9 | name: test 10 | modules: 11 | - type: script 12 | snippets: 13 | - echo test 14 | modules: 15 | - type: files 16 | files: 17 | - source: usr 18 | destination: /usr 19 | 20 | - type: script 21 | scripts: 22 | - example.sh 23 | 24 | - type: rpm-ostree 25 | repos: 26 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 27 | install: 28 | - micro 29 | - starship 30 | remove: 31 | - firefox 32 | - firefox-langpacks 33 | 34 | - type: signing 35 | 36 | - type: test-module 37 | source: local 38 | 39 | - type: containerfile 40 | containerfiles: 41 | - labels 42 | snippets: 43 | - RUN echo "This is a snippet" 44 | 45 | - type: copy 46 | from: alpine-test 47 | src: /test.txt 48 | dest: / 49 | - type: copy 50 | from: ubuntu-test 51 | src: /test.txt 52 | dest: / 53 | - type: copy 54 | from: debian-test 55 | src: /test.txt 56 | dest: / 57 | - type: copy 58 | from: fedora-test 59 | src: /test.txt 60 | dest: / 61 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-invalid-from-file.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: 40 7 | stages: 8 | - from-file: invalid-stages.yml 9 | modules: 10 | - from-file: invalid-module.yml 11 | - from-file: flatpaks.yml 12 | 13 | - type: files 14 | files: 15 | - source: usr 16 | destination: /usr 17 | 18 | - type: script 19 | scripts: 20 | - example.sh 21 | 22 | - type: rpm-ostree 23 | repos: 24 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 25 | install: 26 | - micro 27 | - starship 28 | remove: 29 | - firefox 30 | - firefox-langpacks 31 | 32 | - type: signing 33 | 34 | - type: test-module 35 | source: local 36 | 37 | - type: containerfile 38 | containerfiles: 39 | - labels 40 | snippets: 41 | - RUN echo "This is a snippet" 42 | 43 | - type: copy 44 | from: alpine-test 45 | src: /test.txt 46 | dest: / 47 | - type: copy 48 | from: ubuntu-test 49 | src: /test.txt 50 | dest: / 51 | - type: copy 52 | from: debian-test 53 | src: /test.txt 54 | dest: / 55 | - type: copy 56 | from: fedora-test 57 | src: /test.txt 58 | dest: / 59 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/flatpaks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-v1.json 3 | type: default-flatpaks@v1 4 | notify: true 5 | system: 6 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 7 | repo-name: flathub-system 8 | repo-title: "Flathub (System)" # Optional; this sets the remote's user-facing name in graphical frontends like GNOME Software 9 | install: 10 | - org.gnome.Boxes 11 | - org.gnome.Calculator 12 | - org.gnome.Calendar 13 | - org.gnome.Snapshot 14 | - org.gnome.Contacts 15 | - org.gnome.clocks 16 | - com.belmoussaoui.Decoder 17 | - org.gnome.Evince 18 | - org.gnome.Maps 19 | - org.gnome.TextEditor 20 | - com.github.neithern.g4music 21 | - com.github.rafostar.Clapper 22 | - org.gnome.Loupe 23 | - io.gitlab.librewolf-community 24 | - io.missioncenter.MissionCenter 25 | - org.gnome.World.Secrets 26 | - com.belmoussaoui.Authenticator 27 | - com.vixalien.sticky 28 | - com.github.flxzt.rnote 29 | - org.localsend.localsend_app 30 | - com.dec05eba.gpu_screen_recorder 31 | - com.github.tchx84.Flatseal 32 | - io.github.flattool.Warehouse 33 | - io.github.fabrialberio.pinapp 34 | - com.mattjakeman.ExtensionManager 35 | - com.github.wwmm.easyeffects 36 | user: 37 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 38 | repo-name: flathub-user 39 | repo-title: "Flathub" 40 | -------------------------------------------------------------------------------- /integration-tests/test-repo/recipes/recipe-invalid-stage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | name: cli/test-invalid-stage 4 | description: This is my personal OS image. 5 | base-image: quay.io/fedora/fedora-bootc 6 | image-version: 40 7 | stages: 8 | - name: ubuntu-test 9 | from: 10 | - ubuntu 11 | modules: {} 12 | modules: 13 | - from-file: akmods.yml 14 | - from-file: flatpaks.yml 15 | 16 | - type: files 17 | files: 18 | - source: usr 19 | destination: /usr 20 | 21 | - type: script 22 | scripts: 23 | - example.sh 24 | 25 | - type: rpm-ostree 26 | repos: 27 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 28 | install: 29 | - micro 30 | - starship 31 | remove: 32 | - firefox 33 | - firefox-langpacks 34 | 35 | - type: signing 36 | 37 | - type: test-module 38 | source: local 39 | 40 | - type: containerfile 41 | containerfiles: 42 | - labels 43 | snippets: 44 | - RUN echo "This is a snippet" 45 | 46 | - type: copy 47 | from: alpine-test 48 | src: /test.txt 49 | dest: / 50 | - type: copy 51 | from: ubuntu-test 52 | src: /test.txt 53 | dest: / 54 | - type: copy 55 | from: debian-test 56 | src: /test.txt 57 | dest: / 58 | - type: copy 59 | from: fedora-test 60 | src: /test.txt 61 | dest: / 62 | -------------------------------------------------------------------------------- /test-files/schema/module-stage-list-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "module-stage-list-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "modules": { 7 | "type": "array", 8 | "items": { 9 | "$ref": "#/$defs/ModuleEntry" 10 | }, 11 | "description": "A list of [modules](https://blue-build.org/reference/module/) that is executed in order. Multiple of the same module can be included.\n\nEach item in this list should have at least a `type:` or be specified to be included from an external file in the `recipes/` directory with `from-file:`." 12 | }, 13 | "stages": { 14 | "type": "array", 15 | "items": { 16 | "$ref": "#/$defs/StageEntry" 17 | }, 18 | "description": "A list of [stages](https://blue-build.org/reference/stages/) that are executed before the build of the final image.\nThis is useful for compiling programs from source without polluting the final bootable image." 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "$defs": { 23 | "ModuleEntry": { 24 | "anyOf": [ 25 | { 26 | "$ref": "module-v1.json" 27 | }, 28 | { 29 | "$ref": "import-v1.json" 30 | } 31 | ] 32 | }, 33 | "StageEntry": { 34 | "anyOf": [ 35 | { 36 | "$ref": "stage-v1.json" 37 | }, 38 | { 39 | "$ref": "import-v1.json" 40 | } 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /process/drivers/types/metadata.rs: -------------------------------------------------------------------------------- 1 | use blue_build_utils::{constants::IMAGE_VERSION_LABEL, semver::Version}; 2 | use bon::Builder; 3 | use miette::{Context, Result, miette}; 4 | use oci_client::{config::Config, manifest::OciManifest}; 5 | use serde::Deserialize; 6 | 7 | #[derive(Deserialize, Debug, Clone)] 8 | pub struct ImageConfig { 9 | config: Config, 10 | } 11 | 12 | #[derive(Debug, Clone, Builder)] 13 | pub struct ImageMetadata { 14 | manifest: OciManifest, 15 | digest: String, 16 | config: ImageConfig, 17 | } 18 | 19 | impl ImageMetadata { 20 | #[must_use] 21 | pub fn digest(&self) -> &str { 22 | &self.digest 23 | } 24 | 25 | #[must_use] 26 | pub const fn manifest(&self) -> &OciManifest { 27 | &self.manifest 28 | } 29 | 30 | /// Get the version from the label if possible. 31 | /// 32 | /// # Errors 33 | /// Will error if labels don't exist, the version label 34 | /// doen't exist, or the version cannot be parsed. 35 | pub fn get_version(&self) -> Result { 36 | self.config 37 | .config 38 | .labels 39 | .as_ref() 40 | .ok_or_else(|| miette!("No labels found"))? 41 | .get(IMAGE_VERSION_LABEL) 42 | .ok_or_else(|| miette!("No version label found")) 43 | .and_then(|v| { 44 | v.parse::() 45 | .wrap_err_with(|| format!("Failed to parse version {v}")) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | VERSION=v0.9.27 6 | 7 | # Container runtime 8 | function cr() { 9 | if command -v podman > /dev/null; then 10 | podman $@ 11 | elif command -v docker > /dev/null; then 12 | docker $@ 13 | else 14 | echo "Need docker or podman to install!!" 15 | exit 1 16 | fi 17 | } 18 | 19 | # We use sudo for podman so that we can copy directly into /usr/local/bin 20 | function cleanup() { 21 | echo "Cleaning up image" 22 | cr rm blue-build-installer 23 | sleep 2 24 | cr image rm ghcr.io/blue-build/cli:${VERSION}-installer 25 | } 26 | 27 | trap cleanup SIGINT 28 | 29 | 30 | if command -v cosign &> /dev/null 31 | then 32 | PUBKEY_DIR=$(mktemp -d) 33 | PUBKEY_FILE="${PUBKEY_DIR}/cosign.pub" 34 | curl -Lo "${PUBKEY_FILE}" https://raw.githubusercontent.com/blue-build/cli/refs/heads/main/cosign.pub 35 | cosign verify --key cosign.pub "ghcr.io/blue-build/cli:${VERSION}-installer" 36 | fi 37 | 38 | cr create \ 39 | --pull always \ 40 | --name blue-build-installer \ 41 | ghcr.io/blue-build/cli:${VERSION}-installer 42 | 43 | set +e 44 | cr cp blue-build-installer:/out/bluebuild /tmp/ 45 | 46 | sudo mv /tmp/bluebuild /usr/local/bin/ 47 | 48 | RETVAL=$? 49 | set -e 50 | 51 | if [ $RETVAL != 0 ]; then 52 | cleanup 53 | echo "Failed to copy file" 54 | exit 1 55 | else 56 | # sudo mv bluebuild /usr/local/bin/ 57 | echo "Finished! BlueBuild has been installed at /usr/local/bin/bluebuild" 58 | cleanup 59 | fi 60 | 61 | -------------------------------------------------------------------------------- /src/bin/bluebuild.rs: -------------------------------------------------------------------------------- 1 | use blue_build::commands::{BlueBuildArgs, BlueBuildCommand, CommandArgs}; 2 | use blue_build_process_management::{logging::Logger, signal_handler}; 3 | use clap::Parser; 4 | use log::LevelFilter; 5 | 6 | fn main() { 7 | let args = BlueBuildArgs::parse(); 8 | 9 | Logger::new() 10 | .filter_level(args.verbosity.log_level_filter()) 11 | .filter_modules([ 12 | ("hyper::proto", LevelFilter::Off), 13 | ("hyper_util", LevelFilter::Off), 14 | ("reqwest", LevelFilter::Off), 15 | ("oci_client", LevelFilter::Off), 16 | ("rustls", LevelFilter::Off), 17 | ]) 18 | .log_out_dir(args.log_out.clone()) 19 | .init(); 20 | log::trace!("Parsed arguments: {args:#?}"); 21 | 22 | signal_handler::init(|| match args.command { 23 | CommandArgs::Build(mut command) => command.run(), 24 | CommandArgs::Generate(mut command) => command.run(), 25 | CommandArgs::Switch(mut command) => command.run(), 26 | CommandArgs::Login(mut command) => command.run(), 27 | CommandArgs::New(mut command) => command.run(), 28 | CommandArgs::Init(mut command) => command.run(), 29 | CommandArgs::GenerateIso(mut command) => command.run(), 30 | CommandArgs::Validate(mut command) => command.run(), 31 | CommandArgs::Prune(mut command) => command.run(), 32 | CommandArgs::BugReport(mut command) => command.run(), 33 | CommandArgs::Completions(mut command) => command.run(), 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test-files/schema/modules/gnome-extensions-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/gnome-extensions.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "gnome-extensions", 9 | "description": "The gnome-extensions module can be used to install GNOME extensions inside system directory.\nhttps://blue-build.org/reference/modules/gnome-extensions/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "install": { 17 | "type": "array", 18 | "items": { 19 | "anyOf": [ 20 | { 21 | "type": "string" 22 | }, 23 | { 24 | "type": "integer" 25 | } 26 | ] 27 | }, 28 | "description": "List of GNOME extensions to install. \n(case sensitive extension names or extension IDs from https://extensions.gnome.org/)" 29 | }, 30 | "uninstall": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "description": "List of system GNOME extensions to uninstall. \nOnly use this to remove extensions not installed by your package manager. Those extensions should be uninstalled using the package manager instead." 36 | } 37 | }, 38 | "required": [ 39 | "type" 40 | ], 41 | "additionalProperties": false 42 | } -------------------------------------------------------------------------------- /test-files/schema/modules/gnome-extensions-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/gnome-extensions.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "gnome-extensions", 9 | "description": "The gnome-extensions module can be used to install GNOME extensions inside system directory.\nhttps://blue-build.org/reference/modules/gnome-extensions/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "install": { 17 | "type": "array", 18 | "items": { 19 | "anyOf": [ 20 | { 21 | "type": "string" 22 | }, 23 | { 24 | "type": "integer" 25 | } 26 | ] 27 | }, 28 | "description": "List of GNOME extensions to install. \n(case sensitive extension names or extension IDs from https://extensions.gnome.org/)" 29 | }, 30 | "uninstall": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "description": "List of system GNOME extensions to uninstall. \nOnly use this to remove extensions not installed by your package manager. Those extensions should be uninstalled using the package manager instead." 36 | } 37 | }, 38 | "required": [ 39 | "type" 40 | ], 41 | "additionalProperties": false 42 | } -------------------------------------------------------------------------------- /test-files/schema/modules/bling-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/bling.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "bling", 9 | "description": "The bling module can be used to pull in small \"bling\" into your image. \nhttps://blue-build.org/reference/modules/bling/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "install": { 17 | "type": "array", 18 | "items": { 19 | "anyOf": [ 20 | { 21 | "type": "string", 22 | "const": "rpmfusion" 23 | }, 24 | { 25 | "type": "string", 26 | "const": "negativo17" 27 | }, 28 | { 29 | "type": "string", 30 | "const": "ublue-update" 31 | }, 32 | { 33 | "type": "string", 34 | "const": "1password" 35 | }, 36 | { 37 | "type": "string", 38 | "const": "dconf-update-service" 39 | }, 40 | { 41 | "type": "string", 42 | "const": "gnome-vrr" 43 | } 44 | ] 45 | }, 46 | "description": "List of bling submodules to run / things to install onto your system." 47 | } 48 | }, 49 | "required": [ 50 | "type", 51 | "install" 52 | ], 53 | "additionalProperties": false 54 | } -------------------------------------------------------------------------------- /test-files/schema/modules/bling-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/bling.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "bling", 9 | "description": "The bling module can be used to pull in small \"bling\" into your image. \nhttps://blue-build.org/reference/modules/bling/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "install": { 17 | "type": "array", 18 | "items": { 19 | "anyOf": [ 20 | { 21 | "type": "string", 22 | "const": "rpmfusion" 23 | }, 24 | { 25 | "type": "string", 26 | "const": "negativo17" 27 | }, 28 | { 29 | "type": "string", 30 | "const": "ublue-update" 31 | }, 32 | { 33 | "type": "string", 34 | "const": "1password" 35 | }, 36 | { 37 | "type": "string", 38 | "const": "dconf-update-service" 39 | }, 40 | { 41 | "type": "string", 42 | "const": "gnome-vrr" 43 | } 44 | ] 45 | }, 46 | "description": "List of bling submodules to run / things to install onto your system." 47 | } 48 | }, 49 | "required": [ 50 | "type", 51 | "install" 52 | ], 53 | "additionalProperties": false 54 | } -------------------------------------------------------------------------------- /test-files/schema/modules/files-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/files.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "files", 9 | "description": "Copy files to your image at build time\nhttps://blue-build.org/reference/modules/files/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "files": { 17 | "anyOf": [ 18 | { 19 | "type": "array", 20 | "items": { 21 | "$ref": "#/$defs/RecordString" 22 | } 23 | }, 24 | { 25 | "type": "array", 26 | "items": { 27 | "type": "object", 28 | "properties": { 29 | "source": { 30 | "type": "string" 31 | }, 32 | "destination": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "source", 38 | "destination" 39 | ] 40 | } 41 | } 42 | ], 43 | "description": "List of files / folders to copy." 44 | } 45 | }, 46 | "required": [ 47 | "type", 48 | "files" 49 | ], 50 | "additionalProperties": false, 51 | "$defs": { 52 | "RecordString": { 53 | "type": "object", 54 | "properties": {}, 55 | "additionalProperties": { 56 | "type": "string" 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /test-files/schema/modules/files-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/files.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "files", 9 | "description": "Copy files to your image at build time\nhttps://blue-build.org/reference/modules/files/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "files": { 17 | "anyOf": [ 18 | { 19 | "type": "array", 20 | "items": { 21 | "$ref": "#/$defs/RecordString" 22 | } 23 | }, 24 | { 25 | "type": "array", 26 | "items": { 27 | "type": "object", 28 | "properties": { 29 | "source": { 30 | "type": "string" 31 | }, 32 | "destination": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": [ 37 | "source", 38 | "destination" 39 | ] 40 | } 41 | } 42 | ], 43 | "description": "List of files / folders to copy." 44 | } 45 | }, 46 | "required": [ 47 | "type", 48 | "files" 49 | ], 50 | "additionalProperties": false, 51 | "$defs": { 52 | "RecordString": { 53 | "type": "object", 54 | "properties": {}, 55 | "additionalProperties": { 56 | "type": "string" 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /integration-tests/empty-files-repo/recipes/recipe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json 3 | # image will be published to ghcr.io// 4 | name: test-empty-files 5 | # description will be included in the image's metadata 6 | description: Empty files test 7 | 8 | # the base image to build on top of (FROM) and the version tag to use 9 | base-image: ghcr.io/ublue-os/silverblue-main 10 | image-version: 42 # latest is also supported if you want new updates ASAP 11 | 12 | # module configuration, executed in order 13 | # you can include multiple instances of the same module 14 | modules: 15 | - type: rpm-ostree 16 | repos: 17 | - https://copr.fedorainfracloud.org/coprs/atim/starship/repo/fedora-%OS_VERSION%/atim-starship-fedora-%OS_VERSION%.repo 18 | install: 19 | - micro 20 | - starship 21 | remove: 22 | # example: removing firefox (in favor of the flatpak) 23 | # "firefox" is the main package, "firefox-langpacks" is a dependency 24 | - firefox 25 | - firefox-langpacks # also remove firefox dependency (not required for all packages, this is a special case) 26 | 27 | - type: default-flatpaks@v1 28 | notify: true # Send notification after install/uninstall is finished (true/false) 29 | system: 30 | # If no repo information is specified, Flathub will be used by default 31 | install: 32 | - org.mozilla.firefox 33 | - org.gnome.Loupe 34 | remove: 35 | - org.gnome.eog 36 | user: {} # Also add Flathub user repo, but no user packages 37 | 38 | - type: signing # this sets up the proper policy & signing files for signed images to work fully 39 | 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | cooldown: 13 | default-days: 7 14 | - package-ecosystem: "cargo" # See documentation for possible values 15 | directory: "/utils" # Location of package manifests 16 | schedule: 17 | interval: "weekly" 18 | cooldown: 19 | default-days: 7 20 | - package-ecosystem: "cargo" # See documentation for possible values 21 | directory: "/recipe" # Location of package manifests 22 | schedule: 23 | interval: "weekly" 24 | cooldown: 25 | default-days: 7 26 | - package-ecosystem: "cargo" # See documentation for possible values 27 | directory: "/template" # Location of package manifests 28 | schedule: 29 | interval: "weekly" 30 | cooldown: 31 | default-days: 7 32 | - package-ecosystem: "cargo" # See documentation for possible values 33 | directory: "/process" # Location of package manifests 34 | schedule: 35 | interval: "weekly" 36 | cooldown: 37 | default-days: 7 38 | - package-ecosystem: "github-actions" 39 | directory: "/" 40 | schedule: 41 | interval: "weekly" 42 | cooldown: 43 | default-days: 7 44 | -------------------------------------------------------------------------------- /src/commands/validate/schema_validator/error.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_assignments)] 2 | 3 | use std::{path::PathBuf, sync::Arc}; 4 | 5 | use colored::Colorize; 6 | use miette::{Diagnostic, LabeledSpan, NamedSource}; 7 | use thiserror::Error; 8 | 9 | use crate::commands::validate::yaml_span::YamlSpanError; 10 | 11 | #[derive(Error, Diagnostic, Debug)] 12 | pub enum SchemaValidateBuilderError { 13 | #[error("Failed to get schema from URL {}:\n{}", .0, .1)] 14 | #[cfg(not(test))] 15 | #[diagnostic()] 16 | Reqwest(String, reqwest::Error), 17 | 18 | #[error(transparent)] 19 | #[cfg(test)] 20 | #[diagnostic()] 21 | SerdeJson(#[from] serde_json::Error), 22 | 23 | #[error(transparent)] 24 | #[cfg(test)] 25 | #[diagnostic()] 26 | Fs(#[from] std::io::Error), 27 | 28 | #[error("Failed to process schema from URL {}:\n{}", .0, .1)] 29 | #[diagnostic()] 30 | JsonSchemaBuild(String, jsonschema::ValidationError<'static>), 31 | } 32 | 33 | #[derive(Error, Diagnostic, Debug)] 34 | pub enum SchemaValidateError { 35 | #[error("Failed to deserialize file {}", .1.display().to_string().bold().italic())] 36 | #[diagnostic()] 37 | SerdeYaml(serde_yaml::Error, PathBuf), 38 | 39 | #[error( 40 | "{} error{} encountered", 41 | .labels.len().to_string().red(), 42 | if .labels.len() == 1 { "" } else { "s" } 43 | )] 44 | #[diagnostic()] 45 | YamlValidate { 46 | #[source_code] 47 | src: NamedSource>, 48 | 49 | #[label(collection)] 50 | labels: Vec, 51 | 52 | #[help] 53 | help: String, 54 | }, 55 | 56 | #[error(transparent)] 57 | #[diagnostic(transparent)] 58 | YamlSpan(#[from] YamlSpanError), 59 | } 60 | -------------------------------------------------------------------------------- /utils/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, LazyLock, RwLock}, 4 | thread::{self, ThreadId}, 5 | }; 6 | 7 | use miette::{Result, miette}; 8 | 9 | use crate::string; 10 | 11 | #[expect(clippy::type_complexity)] 12 | static ENV_VARS: LazyLock>>> = 13 | LazyLock::new(|| Arc::new(RwLock::new(HashMap::new()))); 14 | 15 | /// Test harness function for getting env variables. 16 | /// 17 | /// # Errors 18 | /// Will error if the env variable doesn't exist. 19 | pub fn get_env_var(key: S) -> Result 20 | where 21 | S: AsRef, 22 | { 23 | fn inner(key: &str) -> Result { 24 | let thr_id = thread::current().id(); 25 | 26 | let env_vars = ENV_VARS.read().unwrap(); 27 | let key = (thr_id, string!(key)); 28 | 29 | env_vars 30 | .get(&key) 31 | .map(ToOwned::to_owned) 32 | .inspect(|val| eprintln!("get: {key:?} = {val}")) 33 | .ok_or_else(|| miette!("Failed to retrieve env var '{key:?}'")) 34 | } 35 | inner(key.as_ref()) 36 | } 37 | 38 | pub fn set_env_var(key: S, value: T) 39 | where 40 | S: AsRef, 41 | T: AsRef, 42 | { 43 | fn inner(key: &str, value: &str) { 44 | let thr_id = thread::current().id(); 45 | 46 | let mut env_vars = ENV_VARS.write().unwrap(); 47 | 48 | let key = (thr_id, string!(key)); 49 | eprintln!("set: {key:?} = {value}"); 50 | 51 | env_vars 52 | .entry(key) 53 | .and_modify(|val| { 54 | *val = string!(value); 55 | }) 56 | .or_insert_with(|| string!(value)); 57 | } 58 | inner(key.as_ref(), value.as_ref()); 59 | } 60 | -------------------------------------------------------------------------------- /process/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blue-build-process-management" 3 | description.workspace = true 4 | edition.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | categories.workspace = true 8 | version.workspace = true 9 | 10 | [lib] 11 | path = "process.rs" 12 | 13 | [dependencies] 14 | anyhow = "1.0.100" 15 | blue-build-utils = { version = "=0.9.27", path = "../utils" } 16 | indicatif-log-bridge = "0.2.3" 17 | log4rs = { version = "1.4.0", features = ["background_rotation"] } 18 | nu-ansi-term = { version = "0.50.3", features = ["gnu_legacy"] } 19 | os_pipe = { version = "1.2.3", features = ["io_safety"] } 20 | rand = "0.9.2" 21 | signal-hook = { version = "0.3.18", features = ["extended-siginfo"] } 22 | sigstore = { version = "0.13.0", features = ["rustls-tls", "cached-client", "sigstore-trust-root", "sign", "cosign"], default-features = false } 23 | 24 | bon.workspace = true 25 | cached.workspace = true 26 | chrono.workspace = true 27 | clap = { workspace = true, features = ["derive", "env"] } 28 | colored.workspace = true 29 | comlexr.workspace = true 30 | indexmap.workspace = true 31 | indicatif.workspace = true 32 | lazy-regex.workspace = true 33 | log.workspace = true 34 | miette.workspace = true 35 | nix = { workspace = true, features = ["signal"] } 36 | rayon.workspace = true 37 | oci-client.workspace = true 38 | reqwest.workspace = true 39 | semver.workspace = true 40 | serde.workspace = true 41 | serde_json.workspace = true 42 | tempfile.workspace = true 43 | tokio.workspace = true 44 | uuid.workspace = true 45 | which.workspace = true 46 | zeroize.workspace = true 47 | 48 | [dev-dependencies] 49 | pretty_assertions.workspace = true 50 | rstest.workspace = true 51 | blue-build-utils = { version = "=0.9.27", path = "../utils", features = ["test"] } 52 | 53 | [lints] 54 | workspace = true 55 | 56 | [features] 57 | bootc = [] 58 | -------------------------------------------------------------------------------- /process/drivers/skopeo_driver.rs: -------------------------------------------------------------------------------- 1 | use comlexr::cmd; 2 | use log::trace; 3 | use miette::{IntoDiagnostic, Result, bail}; 4 | 5 | use super::opts::CopyOciOpts; 6 | use crate::logging::CommandLogging; 7 | 8 | #[derive(Debug)] 9 | pub struct SkopeoDriver; 10 | 11 | impl super::OciCopy for SkopeoDriver { 12 | fn copy_oci(&self, opts: CopyOciOpts) -> Result<()> { 13 | trace!("SkopeoDriver::copy_oci({opts:?})"); 14 | let use_sudo = opts.privileged && !blue_build_utils::running_as_root(); 15 | let status = { 16 | let c = cmd!( 17 | if use_sudo { 18 | "sudo" 19 | } else { 20 | "skopeo" 21 | }, 22 | if use_sudo && blue_build_utils::has_env_var(blue_build_utils::constants::SUDO_ASKPASS) => [ 23 | "-A", 24 | "-p", 25 | format!( 26 | "Password is required to copy {source} to {dest}", 27 | source = opts.src_ref, 28 | dest = opts.dest_ref, 29 | ) 30 | ], 31 | if use_sudo => "skopeo", 32 | "copy", 33 | "--all", 34 | if opts.retry_count != 0 => format!("--retry-times={}", opts.retry_count), 35 | opts.src_ref.to_os_string(), 36 | opts.dest_ref.to_os_string(), 37 | ); 38 | trace!("{c:?}"); 39 | c 40 | } 41 | .build_status( 42 | opts.dest_ref.to_string(), 43 | format!("Copying {} to", opts.src_ref), 44 | ) 45 | .into_diagnostic()?; 46 | 47 | if !status.success() { 48 | bail!("Failed to copy {} to {}", opts.src_ref, opts.dest_ref); 49 | } 50 | 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test-files/schema/stage-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "stage-v1.json", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "description": "The name of the stage. This is used when referencing\nthe stage when using the from: property in the [`copy` module](https://blue-build.org/reference/modules/copy/)." 9 | }, 10 | "from": { 11 | "type": "string", 12 | "description": "The full image ref (image name + tag). This will be set in the FROM statement of the stage." 13 | }, 14 | "shell": { 15 | "type": "string", 16 | "description": "Allows a user to pass in an array of strings that are passed directly into the [`SHELL` instruction](https://docs.docker.com/reference/dockerfile/#shell)." 17 | }, 18 | "modules": { 19 | "type": "array", 20 | "items": { 21 | "$ref": "#/$defs/ModuleEntry" 22 | }, 23 | "description": "The list of modules to execute. The exact same syntax used by the main recipe `modules:` property." 24 | } 25 | }, 26 | "required": [ 27 | "name", 28 | "from", 29 | "modules" 30 | ], 31 | "additionalProperties": false, 32 | "$defs": { 33 | "ModuleEntry": { 34 | "anyOf": [ 35 | { 36 | "$ref": "module-v1.json" 37 | }, 38 | { 39 | "$ref": "#/$defs/ImportedModule" 40 | } 41 | ] 42 | }, 43 | "ImportedModule": { 44 | "type": "object", 45 | "properties": { 46 | "from-file": { 47 | "type": "string", 48 | "description": "The path to another file containing module configuration to import here.\nhttps://blue-build.org/how-to/multiple-files/" 49 | } 50 | }, 51 | "required": [ 52 | "from-file" 53 | ], 54 | "additionalProperties": false 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/commands/completions/shells.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use clap_complete::{Generator, Shell as CompletionShell}; 3 | use clap_complete_nushell::Nushell; 4 | 5 | #[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Hash)] 6 | pub enum Shells { 7 | /// Bourne Again `SHell` (bash) 8 | Bash, 9 | /// Elvish shell 10 | Elvish, 11 | /// Friendly Interactive `SHell` (fish) 12 | Fish, 13 | /// `PowerShell` 14 | PowerShell, 15 | /// Z `SHell` (zsh) 16 | Zsh, 17 | /// Nushell (nu) 18 | Nushell, 19 | } 20 | 21 | impl Generator for Shells { 22 | fn file_name(&self, name: &str) -> String { 23 | match *self { 24 | Self::Bash => CompletionShell::Bash.file_name(name), 25 | Self::Elvish => CompletionShell::Elvish.file_name(name), 26 | Self::Fish => CompletionShell::Fish.file_name(name), 27 | Self::PowerShell => CompletionShell::PowerShell.file_name(name), 28 | Self::Zsh => CompletionShell::Zsh.file_name(name), 29 | Self::Nushell => Nushell.file_name(name), 30 | } 31 | } 32 | 33 | fn generate(&self, cmd: &clap::Command, buf: &mut dyn std::io::Write) { 34 | match *self { 35 | Self::Bash => CompletionShell::Bash.generate(cmd, buf), 36 | Self::Elvish => CompletionShell::Elvish.generate(cmd, buf), 37 | Self::Fish => CompletionShell::Fish.generate(cmd, buf), 38 | Self::PowerShell => CompletionShell::PowerShell.generate(cmd, buf), 39 | Self::Zsh => CompletionShell::Zsh.generate(cmd, buf), 40 | Self::Nushell => Nushell.generate(cmd, buf), 41 | } 42 | } 43 | } 44 | 45 | impl std::fmt::Display for Shells { 46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 47 | self.to_possible_value() 48 | .expect("no values are skipped") 49 | .get_name() 50 | .fmt(f) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /template/templates/Containerfile.j2: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE="{{ recipe.base_image }}@{{ base_digest }}" 2 | {%- set main_stage = recipe.name|replace('/', "-") %} 3 | FROM "${BASE_IMAGE}" AS {{ main_stage }} 4 | {% import "modules/modules.j2" as modules %} 5 | {% include "stages.j2" %} 6 | 7 | # Main image 8 | FROM {{ main_stage }} 9 | 10 | ARG TARGETARCH 11 | ARG RECIPE={{ recipe_path.display() }} 12 | ARG IMAGE_REGISTRY={{ registry }} 13 | ARG BB_BUILD_FEATURES="{{ get_features() }}" 14 | 15 | {%- if self::config_dir_exists() && !self::files_dir_exists() %} 16 | ARG CONFIG_DIRECTORY="/tmp/config" 17 | {%- else %} 18 | ARG CONFIG_DIRECTORY="/tmp/files" 19 | {%- endif %} 20 | ARG MODULE_DIRECTORY="/tmp/modules" 21 | ARG IMAGE_NAME="{{ recipe.name }}" 22 | ARG BASE_IMAGE="{{ recipe.base_image }}" 23 | 24 | {%- if self::should_color() %} 25 | ARG FORCE_COLOR=1 26 | ARG CLICOLOR_FORCE=1 27 | ARG RUST_LOG_STYLE=always 28 | {%- endif %} 29 | 30 | # Key RUN 31 | RUN --mount=type=bind,from=stage-keys,src=/keys,dst=/tmp/keys \ 32 | mkdir -p /etc/pki/containers/ \ 33 | && cp /tmp/keys/* /etc/pki/containers/ 34 | 35 | {%- if recipe.should_install_bins() %} 36 | # Bin RUN 37 | RUN --mount=type=bind,from=stage-bins,src=/bins,dst=/tmp/bins \ 38 | mkdir -p /usr/bin/ \ 39 | && cp /tmp/bins/* /usr/bin/ 40 | {%- endif %} 41 | 42 | {%- if should_install_nu() %} 43 | RUN --mount=type=bind,from={{ blue_build_utils::constants::NUSHELL_IMAGE }}:{{ get_nu_version() }},src=/nu,dst=/tmp/nu \ 44 | mkdir -p /usr/libexec/bluebuild/nu \ 45 | && cp -r /tmp/nu/* /usr/libexec/bluebuild/nu/ 46 | {%- endif %} 47 | 48 | RUN \ 49 | {{ scripts_mount("/scripts/") }} \ 50 | /scripts/pre_build.sh 51 | 52 | {% call modules::main_modules_run(recipe.modules_ext, os_version) %} 53 | 54 | RUN \ 55 | {{ scripts_mount("/scripts/") }} \ 56 | /scripts/post_build.sh 57 | 58 | # Labels are added last since they cause cache misses with buildah 59 | {%- for (name, value) in labels %} 60 | LABEL {{ name }}="{{ value }}" 61 | {%- endfor %} 62 | -------------------------------------------------------------------------------- /recipe/src/module/type_ver.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serialize}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct ModuleTypeVersion { 5 | typ: String, 6 | version: Option, 7 | } 8 | 9 | impl ModuleTypeVersion { 10 | #[must_use] 11 | pub fn typ(&self) -> &str { 12 | self.typ.as_ref() 13 | } 14 | 15 | #[must_use] 16 | pub fn version(&self) -> Option<&str> { 17 | self.version.as_deref() 18 | } 19 | } 20 | 21 | impl std::fmt::Display for ModuleTypeVersion { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self.version.as_deref() { 24 | Some(version) => { 25 | write!(f, "{}@{version}", &self.typ) 26 | } 27 | None => { 28 | write!(f, "{}", &self.typ) 29 | } 30 | } 31 | } 32 | } 33 | 34 | impl From<&str> for ModuleTypeVersion { 35 | fn from(s: &str) -> Self { 36 | if let Some((typ, version)) = s.split_once('@') { 37 | Self { 38 | typ: typ.into(), 39 | version: Some(version.into()), 40 | } 41 | } else { 42 | Self { 43 | typ: s.into(), 44 | version: None, 45 | } 46 | } 47 | } 48 | } 49 | 50 | impl From for ModuleTypeVersion { 51 | fn from(s: String) -> Self { 52 | Self::from(s.as_str()) 53 | } 54 | } 55 | 56 | impl Serialize for ModuleTypeVersion { 57 | fn serialize(&self, serializer: S) -> Result 58 | where 59 | S: serde::Serializer, 60 | { 61 | serializer.serialize_str(&self.to_string()) 62 | } 63 | } 64 | 65 | impl<'de> Deserialize<'de> for ModuleTypeVersion { 66 | fn deserialize(deserializer: D) -> Result 67 | where 68 | D: Deserializer<'de>, 69 | { 70 | let value: String = Deserialize::deserialize(deserializer)?; 71 | Ok(value.into()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/run_module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | source /tmp/scripts/exports.sh 6 | 7 | # Function to print a centered text banner within a specified width 8 | print_banner() { 9 | local term_width=80 10 | 11 | local text=" ${1} " # Text to print 12 | local padding="$(printf '%0.1s' '='{1..600})" 13 | local padlen=0 14 | 15 | if (( ${#text} < term_width )); then 16 | padlen=$(( (term_width - ${#text}) / 2 )) 17 | fi 18 | 19 | printf '%*.*s%s%*.*s\n' 0 "$padlen" "$padding" "$text" 0 "$padlen" "$padding" 20 | } 21 | 22 | get_script_path() { 23 | local script_name="$1" 24 | local extensions=("nu" "sh" "bash") 25 | local base_script_path="/tmp/modules/${script_name}/${script_name}" 26 | local tried_scripts=() 27 | 28 | # See if 29 | if [[ -f "${base_script_path}" ]]; then 30 | echo "${base_script_path}" 31 | return 0 32 | fi 33 | tried_scripts+=("${script_name}") 34 | 35 | # Iterate through each extension and check if the file exists 36 | for ext in "${extensions[@]}"; do 37 | local script_path="${base_script_path}.${ext}" 38 | tried_scripts+=("${script_name}.${ext}") 39 | 40 | if [[ -f "$script_path" ]]; then 41 | # Output only the script path without extra information 42 | echo "$script_path" 43 | return 0 # Exit the function when the first matching file is found 44 | fi 45 | done 46 | 47 | # If no matching file was found 48 | echo "Failed to find scripts matching: ${tried_scripts[*]}" >&2 49 | return 1 50 | } 51 | 52 | module="$1" 53 | params="$2" 54 | script_path="$(get_script_path "$module")" 55 | nushell_version="$(echo "${params}" | jq '.["nushell-version"] // empty')" 56 | 57 | export PATH="/usr/libexec/bluebuild/nu/:$PATH" 58 | 59 | color_string "$(print_banner "Start '${module}' Module")" "33" 60 | chmod +x "${script_path}" 61 | 62 | if "${script_path}" "${params}"; then 63 | color_string "$(print_banner "End '${module}' Module")" "32" 64 | 65 | else 66 | color_string "$(print_banner "Failed '${module}' Module")" "31" 67 | exit 1 68 | fi 69 | -------------------------------------------------------------------------------- /integration-tests/empty-files-repo/README.md: -------------------------------------------------------------------------------- 1 | # blue-build Image Repo 2 | 3 | See the [BlueBuild docs](https://blue-build.org/how-to/setup/) for quick setup instructions for setting up your own repository based on this template. 4 | 5 | After setup, it is recommended you update this README to describe your custom image. 6 | 7 | ## Installation 8 | 9 | > **Warning** 10 | > [This is an experimental feature](https://www.fedoraproject.org/wiki/Changes/OstreeNativeContainerStable), try at your own discretion. 11 | 12 | To rebase an existing atomic Fedora installation to the latest build: 13 | 14 | - First rebase to the unsigned image, to get the proper signing keys and policies installed: 15 | ``` 16 | rpm-ostree rebase ostree-unverified-registry:cli/blue-build/test-empty-files:latest 17 | ``` 18 | - Reboot to complete the rebase: 19 | ``` 20 | systemctl reboot 21 | ``` 22 | - Then rebase to the signed image, like so: 23 | ``` 24 | rpm-ostree rebase ostree-image-signed:docker://cli/blue-build/test-empty-files:latest 25 | ``` 26 | - Reboot again to complete the installation 27 | ``` 28 | systemctl reboot 29 | ``` 30 | 31 | The `latest` tag will automatically point to the latest build. That build will still always use the Fedora version specified in `recipe.yml`, so you won't get accidentally updated to the next major version. 32 | 33 | ## ISO 34 | 35 | If build on Fedora Atomic, you can generate an offline ISO with the instructions available [here](https://blue-build.org/learn/universal-blue/#fresh-install-from-an-iso). These ISOs cannot unfortunately be distributed on GitHub for free due to large sizes, so for public projects something else has to be used for hosting. 36 | 37 | ## Verification 38 | 39 | These images are signed with [Sigstore](https://www.sigstore.dev/)'s [cosign](https://github.com/sigstore/cosign). You can verify the signature by downloading the `cosign.pub` file from this repo and running the following command: 40 | 41 | ```bash 42 | cosign verify --key cosign.pub cli/blue-build/test-empty-files 43 | ``` 44 | 45 | Cloned from https://github.com/blue-build/template -------------------------------------------------------------------------------- /process/drivers/functions.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use blue_build_utils::{ 4 | constants::{BB_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PRIVATE_KEY, COSIGN_PUB_PATH}, 5 | get_env_var, string, 6 | }; 7 | use miette::{Result, bail}; 8 | 9 | use super::opts::PrivateKey; 10 | 11 | pub(super) fn get_private_key

(path: P) -> Result 12 | where 13 | P: AsRef, 14 | { 15 | let path = path.as_ref(); 16 | 17 | Ok( 18 | match ( 19 | path.join(COSIGN_PUB_PATH).exists(), 20 | get_env_var(BB_PRIVATE_KEY).ok(), 21 | get_env_var(COSIGN_PRIVATE_KEY).ok(), 22 | path.join(COSIGN_PRIV_PATH), 23 | ) { 24 | (true, Some(private_key), _, _) if !private_key.is_empty() => { 25 | PrivateKey::Env(string!(BB_PRIVATE_KEY)) 26 | } 27 | (true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => { 28 | PrivateKey::Env(string!(COSIGN_PRIVATE_KEY)) 29 | } 30 | (true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => { 31 | PrivateKey::Path(cosign_priv_key_path) 32 | } 33 | _ => { 34 | bail!( 35 | help = format!( 36 | "{}{}{}{}{}{}", 37 | format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"), 38 | format_args!( 39 | "in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n" 40 | ), 41 | format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"), 42 | "file in the root of your repo.\n\n", 43 | "See https://blue-build.org/how-to/cosign/ for more information.\n\n", 44 | "If you don't want to sign your image, use the `--no-sign` flag.", 45 | ), 46 | "{}", 47 | "Unable to find private/public key pair", 48 | ) 49 | } 50 | }, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /template/templates/init/README.j2: -------------------------------------------------------------------------------- 1 | # {{ repo_name }} Image Repo 2 | 3 | See the [BlueBuild docs](https://blue-build.org/how-to/setup/) for quick setup instructions for setting up your own repository based on this template. 4 | 5 | After setup, it is recommended you update this README to describe your custom image. 6 | 7 | ## Installation 8 | 9 | > **Warning** 10 | > [This is an experimental feature](https://www.fedoraproject.org/wiki/Changes/OstreeNativeContainerStable), try at your own discretion. 11 | 12 | To rebase an existing atomic Fedora installation to the latest build: 13 | 14 | - First rebase to the unsigned image, to get the proper signing keys and policies installed: 15 | ``` 16 | rpm-ostree rebase ostree-unverified-registry:{{ registry }}/{{ repo_name }}/{{ image_name }}:latest 17 | ``` 18 | - Reboot to complete the rebase: 19 | ``` 20 | systemctl reboot 21 | ``` 22 | - Then rebase to the signed image, like so: 23 | ``` 24 | rpm-ostree rebase ostree-image-signed:docker://{{ registry }}/{{ repo_name }}/{{ image_name }}:latest 25 | ``` 26 | - Reboot again to complete the installation 27 | ``` 28 | systemctl reboot 29 | ``` 30 | 31 | The `latest` tag will automatically point to the latest build. That build will still always use the Fedora version specified in `recipe.yml`, so you won't get accidentally updated to the next major version. 32 | 33 | ## ISO 34 | 35 | If build on Fedora Atomic, you can generate an offline ISO with the instructions available [here](https://blue-build.org/learn/universal-blue/#fresh-install-from-an-iso). These ISOs cannot unfortunately be distributed on GitHub for free due to large sizes, so for public projects something else has to be used for hosting. 36 | 37 | ## Verification 38 | 39 | These images are signed with [Sigstore](https://www.sigstore.dev/)'s [cosign](https://github.com/sigstore/cosign). You can verify the signature by downloading the `cosign.pub` file from this repo and running the following command: 40 | 41 | ```bash 42 | cosign verify --key cosign.pub {{ registry }}/{{ repo_name }}/{{ image_name }} 43 | ``` 44 | 45 | Cloned from https://github.com/blue-build/template 46 | -------------------------------------------------------------------------------- /process/drivers/opts/rechunk.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, path::Path}; 2 | 3 | use blue_build_utils::{ 4 | container::{ContainerId, Tag}, 5 | platform::Platform, 6 | secret::Secret, 7 | }; 8 | use bon::Builder; 9 | use oci_client::Reference; 10 | 11 | use super::CompressionType; 12 | 13 | #[derive(Debug, Clone, Copy, Builder)] 14 | #[builder(derive(Debug, Clone))] 15 | pub struct RechunkOpts<'scope> { 16 | pub image: &'scope Reference, 17 | pub containerfile: &'scope Path, 18 | pub labels: &'scope BTreeMap, 19 | 20 | pub platform: &'scope [Platform], 21 | pub version: &'scope str, 22 | pub name: &'scope str, 23 | pub description: &'scope str, 24 | pub base_digest: &'scope str, 25 | pub base_image: &'scope Reference, 26 | pub repo: &'scope str, 27 | 28 | /// The list of tags for the image being built. 29 | #[builder(default)] 30 | pub tags: &'scope [Tag], 31 | 32 | /// Enable pushing the image. 33 | #[builder(default)] 34 | pub push: bool, 35 | 36 | /// Enable retry logic for pushing. 37 | #[builder(default)] 38 | pub retry_push: bool, 39 | 40 | /// Number of times to retry pushing. 41 | /// 42 | /// Defaults to 1. 43 | #[builder(default = 1)] 44 | pub retry_count: u8, 45 | 46 | /// The compression type to use when pushing. 47 | #[builder(default)] 48 | pub compression: CompressionType, 49 | pub tempdir: Option<&'scope Path>, 50 | 51 | #[builder(default)] 52 | pub clear_plan: bool, 53 | 54 | /// Cache layers from the registry. 55 | pub cache_from: Option<&'scope Reference>, 56 | 57 | /// Cache layers to the registry. 58 | pub cache_to: Option<&'scope Reference>, 59 | 60 | #[builder(default)] 61 | pub secrets: &'scope [&'scope Secret], 62 | } 63 | 64 | #[derive(Debug, Clone, Copy, Builder)] 65 | #[builder(derive(Debug, Clone))] 66 | pub struct ContainerOpts<'scope> { 67 | pub container_id: &'scope ContainerId, 68 | 69 | #[builder(default)] 70 | pub privileged: bool, 71 | } 72 | 73 | #[derive(Debug, Clone, Copy, Builder)] 74 | #[builder(derive(Debug, Clone))] 75 | pub struct VolumeOpts<'scope> { 76 | pub volume_id: &'scope str, 77 | 78 | #[builder(default)] 79 | pub privileged: bool, 80 | } 81 | -------------------------------------------------------------------------------- /process/drivers/oci_client_driver.rs: -------------------------------------------------------------------------------- 1 | use blue_build_utils::credentials::Credentials; 2 | use cached::proc_macro::cached; 3 | use log::trace; 4 | use miette::{IntoDiagnostic, Result}; 5 | use oci_client::{Reference, client::ClientConfig, secrets::RegistryAuth}; 6 | 7 | use crate::{ 8 | ASYNC_RUNTIME, 9 | drivers::{InspectDriver, types::ImageMetadata}, 10 | }; 11 | 12 | use super::opts::GetMetadataOpts; 13 | 14 | pub struct OciClientDriver; 15 | 16 | impl InspectDriver for OciClientDriver { 17 | fn get_metadata(opts: GetMetadataOpts) -> Result { 18 | #[cached(result = true, key = "String", convert = r"{image.to_string()}")] 19 | fn inner(image: &Reference) -> Result { 20 | let client = oci_client::Client::new(ClientConfig::default()); 21 | let auth = match Credentials::get(image.registry()) { 22 | Some(Credentials::Basic { username, password }) => { 23 | RegistryAuth::Basic(username, password.value().into()) 24 | } 25 | Some(Credentials::Token(token)) => RegistryAuth::Bearer(token.value().into()), 26 | None => RegistryAuth::Anonymous, 27 | }; 28 | 29 | let (manifest, digest) = ASYNC_RUNTIME 30 | .block_on(client.pull_manifest(image, &auth)) 31 | .into_diagnostic()?; 32 | let (image_manifest, _image_digest) = ASYNC_RUNTIME 33 | .block_on(client.pull_image_manifest(image, &auth)) 34 | .into_diagnostic()?; 35 | let config = { 36 | let mut c: Vec = vec![]; 37 | ASYNC_RUNTIME 38 | .block_on(client.pull_blob(image, &image_manifest.config, &mut c)) 39 | .into_diagnostic()?; 40 | c 41 | }; 42 | Ok(ImageMetadata::builder() 43 | .manifest(manifest) 44 | .digest(digest) 45 | .config(serde_json::from_slice(&config).into_diagnostic()?) 46 | .build()) 47 | } 48 | trace!("OciClientDriver::get_metadata({opts:?})"); 49 | 50 | if opts.no_cache { 51 | inner_prime_cache(opts.image) 52 | } else { 53 | inner(opts.image) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test-files/schema/modules/akmods-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/akmods.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "akmods", 9 | "description": "The akmods module is a tool used for managing and installing kernel modules built by Universal Blue.\nhttps://blue-build.org/reference/modules/akmods/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "base": { 17 | "anyOf": [ 18 | { 19 | "type": "string", 20 | "const": "main" 21 | }, 22 | { 23 | "type": "string", 24 | "const": "asus" 25 | }, 26 | { 27 | "type": "string", 28 | "const": "fsync" 29 | }, 30 | { 31 | "type": "string", 32 | "const": "fsync-ba" 33 | }, 34 | { 35 | "type": "string", 36 | "const": "surface" 37 | }, 38 | { 39 | "type": "string", 40 | "const": "coreos-stable" 41 | }, 42 | { 43 | "type": "string", 44 | "const": "coreos-testing" 45 | }, 46 | { 47 | "type": "string", 48 | "const": "bazzite" 49 | } 50 | ], 51 | "default": "main", 52 | "description": "The kernel your images uses.\n- main: stock Fedora kernel / main and nvidia images\n- asus: asus kernel / asus images\n- fsync: fsync kernel / not used in any Universal Blue images\n- fsync-ba: fsync kernel, stable version / not used in any Universal Blue images\n- surface: surface kernel / surface images\n- coreos-stable: stock CoreOS kernel / uCore stable images\n- coreos-testing: stock CoreOS Testing kernel / uCore testing images\n- bazzite: Bazzite's kernel / bazzite images" 53 | }, 54 | "install": { 55 | "type": "array", 56 | "items": { 57 | "type": "string" 58 | }, 59 | "description": "List of akmods to install.\nSee all available akmods here: https://github.com/ublue-os/akmods#kmod-packages" 60 | } 61 | }, 62 | "required": [ 63 | "type", 64 | "install" 65 | ], 66 | "additionalProperties": false 67 | } -------------------------------------------------------------------------------- /test-files/schema/modules/akmods-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/akmods.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "akmods", 9 | "description": "The akmods module is a tool used for managing and installing kernel modules built by Universal Blue.\nhttps://blue-build.org/reference/modules/akmods/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "base": { 17 | "anyOf": [ 18 | { 19 | "type": "string", 20 | "const": "main" 21 | }, 22 | { 23 | "type": "string", 24 | "const": "asus" 25 | }, 26 | { 27 | "type": "string", 28 | "const": "fsync" 29 | }, 30 | { 31 | "type": "string", 32 | "const": "fsync-ba" 33 | }, 34 | { 35 | "type": "string", 36 | "const": "surface" 37 | }, 38 | { 39 | "type": "string", 40 | "const": "coreos-stable" 41 | }, 42 | { 43 | "type": "string", 44 | "const": "coreos-testing" 45 | }, 46 | { 47 | "type": "string", 48 | "const": "bazzite" 49 | } 50 | ], 51 | "default": "main", 52 | "description": "The kernel your images uses.\n- main: stock Fedora kernel / main and nvidia images\n- asus: asus kernel / asus images\n- fsync: fsync kernel / not used in any Universal Blue images\n- fsync-ba: fsync kernel, stable version / not used in any Universal Blue images\n- surface: surface kernel / surface images\n- coreos-stable: stock CoreOS kernel / uCore stable images\n- coreos-testing: stock CoreOS Testing kernel / uCore testing images\n- bazzite: Bazzite's kernel / bazzite images" 53 | }, 54 | "install": { 55 | "type": "array", 56 | "items": { 57 | "type": "string" 58 | }, 59 | "description": "List of akmods to install.\nSee all available akmods here: https://github.com/ublue-os/akmods#kmod-packages" 60 | } 61 | }, 62 | "required": [ 63 | "type", 64 | "install" 65 | ], 66 | "additionalProperties": false 67 | } -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | 3 | description = "BlueBuild's command line program that builds Containerfiles and custom images"; 4 | 5 | 6 | inputs = { 7 | flake-schemas.url = "https://flakehub.com/f/DeterminateSystems/flake-schemas/*.tar.gz"; 8 | 9 | nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz"; 10 | 11 | rust-overlay = { 12 | url = "github:oxalica/rust-overlay"; 13 | inputs.nixpkgs.follows = "nixpkgs"; 14 | }; 15 | }; 16 | 17 | 18 | outputs = { self, flake-schemas, nixpkgs, rust-overlay }: 19 | let 20 | overlays = [ 21 | rust-overlay.overlays.default 22 | (final: prev: { 23 | rustToolchain = (final.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { extensions = [ "rust-src"]; }; 24 | }) 25 | ]; 26 | 27 | supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; 28 | forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec { 29 | pkgs = import nixpkgs { inherit overlays system; }; 30 | lib = pkgs.lib; 31 | }); 32 | in { 33 | schemas = flake-schemas.schemas; 34 | 35 | packages = forEachSupportedSystem ({ pkgs, lib }: rec { 36 | default = bluebuild; 37 | bluebuild = pkgs.rustPlatform.buildRustPackage { 38 | pname = "bluebuild"; 39 | version = "v0.9.27"; 40 | 41 | src = pkgs.lib.cleanSource ./.; 42 | cargoLock.lockFile = ./Cargo.lock; 43 | 44 | meta = { 45 | description = "BlueBuild's command line program that builds Containerfiles and custom images"; 46 | homepage = "https://github.com/blue-build/cli"; 47 | license = lib.licenses.apsl20; 48 | }; 49 | }; 50 | }); 51 | 52 | devShells = forEachSupportedSystem ({ pkgs, ... }: { 53 | default = pkgs.mkShell { 54 | 55 | packages = with pkgs; [ 56 | rustToolchain 57 | cargo-bloat 58 | cargo-edit 59 | cargo-outdated 60 | cargo-watch 61 | rust-analyzer 62 | cargo 63 | rustc 64 | bacon 65 | earthly 66 | jq 67 | nixpkgs-fmt 68 | ]; 69 | 70 | env = { 71 | RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; 72 | }; 73 | }; 74 | }); 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /test-files/schema/modules/chezmoi-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/chezmoi.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "chezmoi", 9 | "description": "The chezmoi module installs the latest chezmoi release at build time, along with services to clone a dotfile repository and keep it up-to-date.\nhttps://blue-build.org/reference/modules/chezmoi/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "repository": { 17 | "type": "string", 18 | "description": "Git repository to initialize." 19 | }, 20 | "branch": { 21 | "type": "string", 22 | "default": "", 23 | "description": "Git branch of the chezmoi repository." 24 | }, 25 | "all-users": { 26 | "type": "boolean", 27 | "default": true, 28 | "description": "Whether to enable the modules services globally for all users, if false users need to enable services manually." 29 | }, 30 | "run-every": { 31 | "type": "string", 32 | "default": "1d", 33 | "description": "Dotfiles will be updated with this interval." 34 | }, 35 | "wait-after-boot": { 36 | "type": "string", 37 | "default": "5m", 38 | "description": "Dotfile updates will wait this long after a boot before running." 39 | }, 40 | "disable-init": { 41 | "type": "boolean", 42 | "default": false, 43 | "description": "Disable the service that initializes `repository` on users that are logged in or have linger enabled UI." 44 | }, 45 | "disable-update": { 46 | "type": "boolean", 47 | "default": false, 48 | "description": "Disable the timer that updates chezmoi with the set interval." 49 | }, 50 | "file-conflict-policy": { 51 | "anyOf": [ 52 | { 53 | "type": "string", 54 | "const": "skip" 55 | }, 56 | { 57 | "type": "string", 58 | "const": "replace" 59 | } 60 | ], 61 | "default": "skip", 62 | "description": "What to do when file different that exists on your repo is has been changed or exists locally. Accepts \"skip\" or \"replace\"." 63 | } 64 | }, 65 | "required": [ 66 | "type", 67 | "repository" 68 | ], 69 | "additionalProperties": false 70 | } -------------------------------------------------------------------------------- /test-files/schema/modules/chezmoi-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/chezmoi.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "chezmoi", 9 | "description": "The chezmoi module installs the latest chezmoi release at build time, along with services to clone a dotfile repository and keep it up-to-date.\nhttps://blue-build.org/reference/modules/chezmoi/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "repository": { 17 | "type": "string", 18 | "description": "Git repository to initialize." 19 | }, 20 | "branch": { 21 | "type": "string", 22 | "default": "", 23 | "description": "Git branch of the chezmoi repository." 24 | }, 25 | "all-users": { 26 | "type": "boolean", 27 | "default": true, 28 | "description": "Whether to enable the modules services globally for all users, if false users need to enable services manually." 29 | }, 30 | "run-every": { 31 | "type": "string", 32 | "default": "1d", 33 | "description": "Dotfiles will be updated with this interval." 34 | }, 35 | "wait-after-boot": { 36 | "type": "string", 37 | "default": "5m", 38 | "description": "Dotfile updates will wait this long after a boot before running." 39 | }, 40 | "disable-init": { 41 | "type": "boolean", 42 | "default": false, 43 | "description": "Disable the service that initializes `repository` on users that are logged in or have linger enabled UI." 44 | }, 45 | "disable-update": { 46 | "type": "boolean", 47 | "default": false, 48 | "description": "Disable the timer that updates chezmoi with the set interval." 49 | }, 50 | "file-conflict-policy": { 51 | "anyOf": [ 52 | { 53 | "type": "string", 54 | "const": "skip" 55 | }, 56 | { 57 | "type": "string", 58 | "const": "replace" 59 | } 60 | ], 61 | "default": "skip", 62 | "description": "What to do when file different that exists on your repo is has been changed or exists locally. Accepts \"skip\" or \"replace\"." 63 | } 64 | }, 65 | "required": [ 66 | "type", 67 | "repository" 68 | ], 69 | "additionalProperties": false 70 | } -------------------------------------------------------------------------------- /test-files/schema/modules/rpm-ostree-latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/rpm-ostree.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "rpm-ostree", 9 | "description": "The rpm-ostree module offers pseudo-declarative package and repository management using rpm-ostree.\nhttps://blue-build.org/reference/modules/rpm-ostree/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "repos": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "List of links to .repo files to download into /etc/yum.repos.d/." 22 | }, 23 | "keys": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "List of links to key files to import for installing from custom repositories." 29 | }, 30 | "optfix": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "description": "List of folder names under /opt/ to enable for installing into." 36 | }, 37 | "install": { 38 | "type": "array", 39 | "items": { 40 | "type": "string" 41 | }, 42 | "description": "List of RPM packages to install." 43 | }, 44 | "remove": { 45 | "type": "array", 46 | "items": { 47 | "type": "string" 48 | }, 49 | "description": "List of RPM packages to remove." 50 | }, 51 | "replace": { 52 | "type": "array", 53 | "items": { 54 | "type": "object", 55 | "properties": { 56 | "from-repo": { 57 | "type": "string", 58 | "description": "URL to the source COPR repo for the new packages." 59 | }, 60 | "packages": { 61 | "type": "array", 62 | "items": { 63 | "type": "string" 64 | }, 65 | "description": "List of packages to replace using packages from the defined repo." 66 | } 67 | }, 68 | "required": [ 69 | "from-repo", 70 | "packages" 71 | ] 72 | }, 73 | "description": "List of configurations for `rpm-ostree override replace`ing packages." 74 | } 75 | }, 76 | "required": [ 77 | "type" 78 | ], 79 | "additionalProperties": false 80 | } -------------------------------------------------------------------------------- /test-files/schema/modules/rpm-ostree-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "/modules/rpm-ostree.json", 4 | "type": "object", 5 | "properties": { 6 | "type": { 7 | "type": "string", 8 | "const": "rpm-ostree", 9 | "description": "The rpm-ostree module offers pseudo-declarative package and repository management using rpm-ostree.\nhttps://blue-build.org/reference/modules/rpm-ostree/" 10 | }, 11 | "no-cache": { 12 | "type": "boolean", 13 | "default": false, 14 | "description": "Whether to disabling caching for this layer.\nhttps://blue-build.org/reference/module/#no-cache-optional" 15 | }, 16 | "repos": { 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | }, 21 | "description": "List of links to .repo files to download into /etc/yum.repos.d/." 22 | }, 23 | "keys": { 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "description": "List of links to key files to import for installing from custom repositories." 29 | }, 30 | "optfix": { 31 | "type": "array", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "description": "List of folder names under /opt/ to enable for installing into." 36 | }, 37 | "install": { 38 | "type": "array", 39 | "items": { 40 | "type": "string" 41 | }, 42 | "description": "List of RPM packages to install." 43 | }, 44 | "remove": { 45 | "type": "array", 46 | "items": { 47 | "type": "string" 48 | }, 49 | "description": "List of RPM packages to remove." 50 | }, 51 | "replace": { 52 | "type": "array", 53 | "items": { 54 | "type": "object", 55 | "properties": { 56 | "from-repo": { 57 | "type": "string", 58 | "description": "URL to the source COPR repo for the new packages." 59 | }, 60 | "packages": { 61 | "type": "array", 62 | "items": { 63 | "type": "string" 64 | }, 65 | "description": "List of packages to replace using packages from the defined repo." 66 | } 67 | }, 68 | "required": [ 69 | "from-repo", 70 | "packages" 71 | ] 72 | }, 73 | "description": "List of configurations for `rpm-ostree override replace`ing packages." 74 | } 75 | }, 76 | "required": [ 77 | "type" 78 | ], 79 | "additionalProperties": false 80 | } -------------------------------------------------------------------------------- /process/drivers/docker_driver/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use blue_build_utils::platform::Platform; 4 | use miette::{Report, bail}; 5 | use serde::Deserialize; 6 | 7 | use crate::drivers::types::ImageMetadata; 8 | 9 | #[derive(Deserialize, Debug, Clone)] 10 | pub struct Metadata { 11 | manifest: Manifest, 12 | image: MetadataImage, 13 | } 14 | 15 | #[derive(Deserialize, Debug, Clone)] 16 | pub struct PlatformManifest { 17 | digest: String, 18 | platform: PlatformManifestInfo, 19 | } 20 | 21 | #[derive(Deserialize, Debug, Clone)] 22 | pub struct PlatformManifestInfo { 23 | architecture: String, 24 | } 25 | 26 | #[derive(Deserialize, Debug, Clone)] 27 | pub struct Manifest { 28 | digest: String, 29 | 30 | #[serde(default)] 31 | manifests: Vec, 32 | } 33 | 34 | #[derive(Deserialize, Debug, Clone)] 35 | pub struct MetadataPlatformImage { 36 | config: Config, 37 | } 38 | 39 | #[derive(Deserialize, Debug, Clone)] 40 | #[serde(untagged)] 41 | pub enum MetadataImage { 42 | Single(MetadataPlatformImage), 43 | Multi(HashMap), 44 | } 45 | 46 | #[derive(Deserialize, Debug, Clone)] 47 | #[serde(rename_all = "PascalCase")] 48 | pub struct Config { 49 | labels: HashMap, 50 | } 51 | 52 | impl TryFrom<(Metadata, Option)> for ImageMetadata { 53 | type Error = Report; 54 | 55 | fn try_from((metadata, platform): (Metadata, Option)) -> Result { 56 | match metadata.image { 57 | MetadataImage::Single(image) => Ok(Self { 58 | labels: image.config.labels, 59 | digest: metadata.manifest.digest, 60 | }), 61 | MetadataImage::Multi(mut platforms) => { 62 | let platform = platform.unwrap_or_default(); 63 | let Some(image) = platforms.remove(&platform.to_string()) else { 64 | bail!("Image information does not exist for {platform}"); 65 | }; 66 | let Some(manifest) = metadata 67 | .manifest 68 | .manifests 69 | .into_iter() 70 | .find(|manifest| manifest.platform.architecture == platform.arch()) 71 | else { 72 | bail!("Manifest does not exist for {platform}"); 73 | }; 74 | Ok(Self { 75 | labels: image.config.labels, 76 | digest: manifest.digest, 77 | }) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /process/drivers/rpm_ostree_driver.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | use blue_build_utils::constants::OSTREE_UNVERIFIED_IMAGE; 4 | use comlexr::cmd; 5 | use log::trace; 6 | use miette::{Context, IntoDiagnostic, bail}; 7 | 8 | use crate::logging::CommandLogging; 9 | 10 | use super::{BootDriver, BootStatus, opts::SwitchOpts}; 11 | 12 | mod status; 13 | 14 | pub use status::*; 15 | 16 | pub struct RpmOstreeDriver; 17 | 18 | impl BootDriver for RpmOstreeDriver { 19 | fn status() -> miette::Result> { 20 | let output = { 21 | let c = cmd!("rpm-ostree", "status", "--json"); 22 | trace!("{c:?}"); 23 | c 24 | } 25 | .output() 26 | .into_diagnostic()?; 27 | 28 | if !output.status.success() { 29 | bail!("Failed to get `rpm-ostree` status!"); 30 | } 31 | 32 | trace!("{}", String::from_utf8_lossy(&output.stdout)); 33 | 34 | Ok(Box::new( 35 | serde_json::from_slice::(&output.stdout) 36 | .into_diagnostic() 37 | .wrap_err_with(|| { 38 | format!( 39 | "Failed to deserialize rpm-ostree status:\n{}", 40 | String::from_utf8_lossy(&output.stdout) 41 | ) 42 | })?, 43 | )) 44 | } 45 | 46 | fn switch(opts: SwitchOpts) -> miette::Result<()> { 47 | let status = { 48 | let c = cmd!( 49 | "rpm-ostree", 50 | "rebase", 51 | format!("{OSTREE_UNVERIFIED_IMAGE}:containers-storage:{}", opts.image), 52 | if opts.reboot => "--reboot", 53 | ); 54 | 55 | trace!("{c:?}"); 56 | c 57 | } 58 | .build_status(format!("{}", opts.image), "Switching to new image") 59 | .into_diagnostic()?; 60 | 61 | if status.success().not() { 62 | bail!("Failed to switch to image {}", opts.image); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | fn upgrade(opts: SwitchOpts) -> miette::Result<()> { 69 | let status = { 70 | let c = cmd!( 71 | "rpm-ostree", 72 | "upgrade", 73 | if opts.reboot => "--reboot", 74 | ); 75 | 76 | trace!("{c:?}"); 77 | c 78 | } 79 | .build_status(format!("{}", opts.image), "Switching to new image") 80 | .into_diagnostic()?; 81 | 82 | if status.success().not() { 83 | bail!("Failed to switch to image {}", opts.image); 84 | } 85 | 86 | Ok(()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /process/drivers/bootc_driver.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | use blue_build_utils::sudo_cmd; 4 | use log::trace; 5 | use miette::{Context, IntoDiagnostic, Result, bail}; 6 | 7 | use crate::logging::CommandLogging; 8 | 9 | use super::{BootDriver, BootStatus, opts::SwitchOpts}; 10 | 11 | mod status; 12 | 13 | pub use status::*; 14 | 15 | const SUDO_PROMPT: &str = "Password needed to run bootc"; 16 | 17 | pub struct BootcDriver; 18 | 19 | impl BootDriver for BootcDriver { 20 | fn status() -> Result> { 21 | let output = { 22 | let c = sudo_cmd!(prompt = SUDO_PROMPT, "bootc", "status", "--format=json"); 23 | trace!("{c:?}"); 24 | c 25 | } 26 | .output() 27 | .into_diagnostic()?; 28 | 29 | if !output.status.success() { 30 | bail!("Failed to get `bootc` status!"); 31 | } 32 | 33 | trace!("{}", String::from_utf8_lossy(&output.stdout)); 34 | 35 | Ok(Box::new( 36 | serde_json::from_slice::(&output.stdout) 37 | .into_diagnostic() 38 | .wrap_err_with(|| { 39 | format!( 40 | "Failed to deserialize bootc status:\n{}", 41 | String::from_utf8_lossy(&output.stdout) 42 | ) 43 | })?, 44 | )) 45 | } 46 | 47 | fn switch(opts: SwitchOpts) -> Result<()> { 48 | let status = { 49 | let c = sudo_cmd!( 50 | prompt = SUDO_PROMPT, 51 | "bootc", 52 | "switch", 53 | "--transport=containers-storage", 54 | opts.image.to_string(), 55 | ); 56 | trace!("{c:?}"); 57 | c 58 | } 59 | .build_status( 60 | opts.image.to_string(), 61 | format!("Switching to {}", opts.image), 62 | ) 63 | .into_diagnostic()?; 64 | 65 | if status.success().not() { 66 | bail!("Failed to switch to {}", opts.image); 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | fn upgrade(opts: SwitchOpts) -> Result<()> { 73 | let status = { 74 | let c = sudo_cmd!(prompt = SUDO_PROMPT, "bootc", "upgrade"); 75 | trace!("{c:?}"); 76 | c 77 | } 78 | .build_status( 79 | opts.image.to_string(), 80 | format!("Switching to {}", opts.image), 81 | ) 82 | .into_diagnostic()?; 83 | 84 | if status.success().not() { 85 | bail!("Failed to switch to {}", opts.image); 86 | } 87 | 88 | Ok(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test-files/recipes/module-list-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json 3 | modules: 4 | - type: akmods 5 | base: main 6 | install: openrgb 7 | - type: bling 8 | install: rpmfusion 9 | - type: brew 10 | auto-update: true 11 | update-interval: "6h" 12 | auto-upgrade: "true" 13 | update-wait-after-boot: "10min" 14 | upgrade-interval: "8h" 15 | upgrade-wait-after-boot: "30min" 16 | nofile-limits: true 17 | brew-analytics: "fasle" 18 | install: 19 | - test 20 | - type: chezmoi 21 | repository: 'test-repo.git' 22 | branch: 'main' 23 | all-users: "true" 24 | run-every: "1d" 25 | wait-after-boot: "5m" 26 | disable-init: false 27 | disable-update: "false" 28 | file-conflict-policy: none 29 | - type: containerfile 30 | snippets: RUN echo "Hello!" 31 | containerfiles: test 32 | - type: copy 33 | from: test-stage 34 | src: 35 | - /out/test 36 | dest: 37 | - /in/test 38 | - type: default-flatpaks 39 | notify: 'false' 40 | system: 41 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 42 | repo-name: 43 | - flathub 44 | repo-title: test-user 45 | install: test.org 46 | remove: 47 | - bad-test.org 48 | user: 49 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 50 | repo-name: flathub 51 | repo-title: test-user 52 | install: test.org 53 | remove: 54 | - bad-test.org 55 | - type: files 56 | files: 57 | source: test 58 | destination: /usr 59 | - type: fonts 60 | fonts: 61 | nerd-fonts: "JetbrainsMono" 62 | google-fonts: "Test" 63 | - type: gnome-extensions 64 | install: test 65 | uninstall: rmtest 66 | - type: gschema-overrides 67 | include: test 68 | - type: justfiles 69 | validate: 'true' 70 | include: ./justfile 71 | - type: rpm-ostree 72 | repos: 73 | - test.repo 74 | keys: test.key 75 | optfix: 76 | - test 77 | install: 78 | - test 79 | remove: rmtest 80 | replace: 81 | - replacetest 82 | - type: script 83 | snippets: rm -fr /* 84 | scripts: test.sh 85 | - type: signing 86 | dir: /dir 87 | - type: systemd 88 | system: 89 | enabled: 90 | - test.service 91 | disabled: disable-test.service 92 | masked: 93 | - masked-test.service 94 | unmasked: unmasked-test.service 95 | user: 96 | enabled: test.service 97 | disabled: 98 | - disable-test.service 99 | masked: 100 | - test: masked-test.service 101 | unmasked: 102 | - unmasked-test.service 103 | - type: yafti 104 | custom-flatpaks: 105 | - test.org 106 | -------------------------------------------------------------------------------- /src/commands/prune.rs: -------------------------------------------------------------------------------- 1 | use blue_build_process_management::drivers::{BuildDriver, Driver, DriverArgs, opts::PruneOpts}; 2 | use bon::Builder; 3 | use clap::Args; 4 | use colored::Colorize; 5 | use miette::bail; 6 | 7 | use super::BlueBuildCommand; 8 | 9 | #[derive(Debug, Args, Builder)] 10 | pub struct PruneCommand { 11 | /// Remove all unused images 12 | #[builder(default)] 13 | #[arg(short, long)] 14 | all: bool, 15 | 16 | /// Do not prompt for confirmation 17 | #[builder(default)] 18 | #[arg(short, long)] 19 | force: bool, 20 | 21 | /// Prune volumes 22 | #[builder(default)] 23 | #[arg(long)] 24 | volumes: bool, 25 | 26 | #[clap(flatten)] 27 | #[builder(default)] 28 | drivers: DriverArgs, 29 | } 30 | 31 | impl BlueBuildCommand for PruneCommand { 32 | fn try_run(&mut self) -> miette::Result<()> { 33 | Driver::init(self.drivers); 34 | 35 | if !self.force { 36 | eprintln!( 37 | "{} This will remove:{default}{images}{build_cache}{volumes}", 38 | "WARNING!".bright_yellow(), 39 | default = concat!( 40 | "\n - all stopped containers", 41 | "\n - all networks not used by at least one container", 42 | ), 43 | images = if self.all { 44 | "\n - all images without at least one container associated to them" 45 | } else { 46 | "\n - all dangling images" 47 | }, 48 | build_cache = if self.all { 49 | "\n - all build cache" 50 | } else { 51 | "\n - unused build cache" 52 | }, 53 | volumes = if self.volumes { 54 | "\n - all anonymous volumes not used by at least one container" 55 | } else { 56 | "" 57 | }, 58 | ); 59 | 60 | match requestty::prompt_one( 61 | requestty::Question::confirm("anonymous") 62 | .message("Are you sure you want to continue?") 63 | .default(false) 64 | .build(), 65 | ) { 66 | Err(e) => bail!("Canceled {e:?}"), 67 | Ok(answer) => { 68 | if answer.as_bool().is_some_and(|a| !a) { 69 | return Ok(()); 70 | } 71 | } 72 | } 73 | } 74 | 75 | Driver::prune( 76 | PruneOpts::builder() 77 | .all(self.all) 78 | .volumes(self.volumes) 79 | .build(), 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /recipe/src/module_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use bon::Builder; 8 | use log::trace; 9 | use miette::{Context, IntoDiagnostic, Report, Result}; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::{AkmodsInfo, FromFileList, Module, base_recipe_path}; 13 | 14 | #[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)] 15 | pub struct ModuleExt { 16 | #[builder(default)] 17 | pub modules: Vec, 18 | } 19 | 20 | impl FromFileList for ModuleExt { 21 | const LIST_KEY: &'static str = "modules"; 22 | 23 | fn get_from_file_paths(&self) -> Vec { 24 | self.modules 25 | .iter() 26 | .filter_map(Module::get_from_file_path) 27 | .collect() 28 | } 29 | } 30 | 31 | impl TryFrom<&PathBuf> for ModuleExt { 32 | type Error = Report; 33 | 34 | fn try_from(value: &PathBuf) -> std::result::Result { 35 | Self::try_from(value.as_path()) 36 | } 37 | } 38 | 39 | impl TryFrom<&Path> for ModuleExt { 40 | type Error = Report; 41 | 42 | fn try_from(file_name: &Path) -> Result { 43 | let file_path = base_recipe_path().join(file_name); 44 | 45 | let file = fs::read_to_string(&file_path) 46 | .into_diagnostic() 47 | .with_context(|| format!("Failed to open {}", file_path.display()))?; 48 | 49 | serde_yaml::from_str::(&file).map_or_else( 50 | |_| -> Result { 51 | let module = serde_yaml::from_str::(&file) 52 | .into_diagnostic() 53 | .wrap_err_with(|| { 54 | format!("Failed to parse module file {}", file_path.display()) 55 | })?; 56 | Ok(Self::builder().modules(vec![module]).build()) 57 | }, 58 | Ok, 59 | ) 60 | } 61 | } 62 | 63 | impl ModuleExt { 64 | #[must_use] 65 | pub fn get_akmods_info_list(&self, os_version: &u64) -> Vec { 66 | trace!("get_akmods_image_list({self:#?}, {os_version})"); 67 | 68 | let mut seen = HashSet::new(); 69 | 70 | self.modules 71 | .iter() 72 | .filter(|module| { 73 | module 74 | .required_fields 75 | .as_ref() 76 | .is_some_and(|rf| rf.module_type.typ() == "akmods") 77 | }) 78 | .filter_map(|module| { 79 | Some( 80 | module 81 | .required_fields 82 | .as_ref()? 83 | .generate_akmods_info(os_version), 84 | ) 85 | }) 86 | .filter(|image| seen.insert(image.clone())) 87 | .collect() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /utils/src/traits.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::OsStr, path::Path}; 2 | 3 | trait PrivateTrait: IntoIterator {} 4 | 5 | impl PrivateTrait for T where T: IntoIterator {} 6 | 7 | #[expect(private_bounds)] 8 | pub trait CowCollecter<'a, IN, OUT>: PrivateTrait 9 | where 10 | IN: ToOwned + ?Sized, 11 | OUT: ToOwned + ?Sized, 12 | { 13 | fn collect_cow_vec(&'a self) -> Vec>; 14 | } 15 | 16 | impl<'a, T, R> CowCollecter<'a, R, R> for T 17 | where 18 | T: AsRef<[R]> + IntoIterator, 19 | R: ToOwned, 20 | { 21 | fn collect_cow_vec(&'a self) -> Vec> { 22 | self.as_ref().iter().map(Cow::Borrowed).collect() 23 | } 24 | } 25 | 26 | macro_rules! impl_cow_collector { 27 | ($type:ty) => { 28 | impl<'a, T, R> CowCollecter<'a, R, $type> for T 29 | where 30 | T: AsRef<[R]> + IntoIterator, 31 | R: AsRef<$type> + ToOwned + 'a, 32 | { 33 | fn collect_cow_vec(&'a self) -> Vec> { 34 | self.as_ref() 35 | .iter() 36 | .map(|v| v.as_ref()) 37 | .map(Cow::from) 38 | .collect() 39 | } 40 | } 41 | }; 42 | } 43 | 44 | impl_cow_collector!(str); 45 | impl_cow_collector!(Path); 46 | impl_cow_collector!(OsStr); 47 | 48 | #[expect(private_bounds)] 49 | pub trait AsRefCollector<'a, IN, OUT>: PrivateTrait 50 | where 51 | IN: ?Sized, 52 | OUT: ?Sized, 53 | { 54 | fn collect_as_ref_vec(&'a self) -> Vec<&'a OUT>; 55 | } 56 | 57 | impl<'a, T, R> AsRefCollector<'a, R, R> for T 58 | where 59 | T: AsRef<[R]> + IntoIterator, 60 | { 61 | fn collect_as_ref_vec(&'a self) -> Vec<&'a R> { 62 | self.as_ref().iter().collect() 63 | } 64 | } 65 | 66 | macro_rules! impl_asref_collector { 67 | ($type:ty) => { 68 | impl<'a, T, R> AsRefCollector<'a, R, $type> for T 69 | where 70 | T: AsRef<[R]> + IntoIterator, 71 | R: AsRef<$type> + 'a, 72 | { 73 | fn collect_as_ref_vec(&'a self) -> Vec<&'a $type> { 74 | self.as_ref().iter().map(AsRef::as_ref).collect() 75 | } 76 | } 77 | }; 78 | } 79 | 80 | impl_asref_collector!(str); 81 | impl_asref_collector!(Path); 82 | impl_asref_collector!(OsStr); 83 | 84 | #[expect(private_bounds)] 85 | pub trait IntoCollector: PrivateTrait 86 | where 87 | IN: Into, 88 | { 89 | fn collect_into_vec(self) -> Vec; 90 | } 91 | 92 | impl IntoCollector for T 93 | where 94 | T: IntoIterator, 95 | U: Into, 96 | { 97 | fn collect_into_vec(self) -> Vec { 98 | self.into_iter().map(Into::into).collect() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test-files/recipes/module-list-pass.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json 3 | modules: 4 | - type: akmods 5 | base: main 6 | install: 7 | - openrgb 8 | - type: bling 9 | install: 10 | - rpmfusion 11 | - type: brew 12 | auto-update: true 13 | update-interval: "6h" 14 | auto-upgrade: true 15 | update-wait-after-boot: "10min" 16 | upgrade-interval: "8h" 17 | upgrade-wait-after-boot: "30min" 18 | nofile-limits: true 19 | brew-analytics: false 20 | - type: chezmoi 21 | repository: 'test-repo.git' 22 | branch: 'main' 23 | all-users: false 24 | run-every: "1d" 25 | wait-after-boot: "5m" 26 | disable-init: false 27 | disable-update: false 28 | file-conflict-policy: replace 29 | - type: containerfile 30 | snippets: 31 | - RUN echo "Hello!" 32 | containerfiles: 33 | - test 34 | - type: copy 35 | from: test-stage 36 | src: /out/test 37 | dest: /in/test 38 | - type: default-flatpaks 39 | notify: false 40 | system: 41 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 42 | repo-name: flathub 43 | repo-title: test-user 44 | install: 45 | - test.org 46 | remove: 47 | - bad-test.org 48 | user: 49 | repo-url: https://dl.flathub.org/repo/flathub.flatpakrepo 50 | repo-name: flathub 51 | repo-title: test-user 52 | install: 53 | - test.org 54 | remove: 55 | - bad-test.org 56 | - type: files 57 | files: 58 | - source: test 59 | destination: /usr 60 | - type: fonts 61 | fonts: 62 | nerd-fonts: 63 | - "JetbrainsMono" 64 | google-fonts: 65 | - "Test" 66 | - type: gnome-extensions 67 | install: 68 | - test 69 | uninstall: 70 | - rmtest 71 | - type: gschema-overrides 72 | include: 73 | - test 74 | - type: justfiles 75 | validate: true 76 | include: 77 | - ./justfile 78 | - type: rpm-ostree 79 | repos: 80 | - test.repo 81 | keys: 82 | - test.key 83 | optfix: 84 | - test 85 | install: 86 | - test 87 | remove: 88 | - rmtest 89 | replace: 90 | - from-repo: updates 91 | packages: 92 | - replacetest 93 | - type: script 94 | snippets: 95 | - rm -fr /* 96 | scripts: 97 | - test.sh 98 | - type: signing 99 | - type: systemd 100 | system: 101 | enabled: 102 | - test.service 103 | disabled: 104 | - disable-test.service 105 | masked: 106 | - masked-test.service 107 | unmasked: 108 | - unmasked-test.service 109 | user: 110 | enabled: 111 | - test.service 112 | disabled: 113 | - disable-test.service 114 | masked: 115 | - masked-test.service 116 | unmasked: 117 | - unmasked-test.service 118 | - type: yafti 119 | custom-flatpaks: 120 | - PrettyTest: test.org 121 | -------------------------------------------------------------------------------- /scripts/exports.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Function to retrieve module configs and populate an array 4 | # Arguments: 5 | # 1. Variable name to store result 6 | # 2. jq query 7 | # 3. Module config content 8 | get_json_array() { 9 | local -n arr="${1}" 10 | local jq_query="${2}" 11 | local module_config="${3}" 12 | 13 | if [[ -z "${jq_query}" || -z "${module_config}" ]]; then 14 | echo "Usage: get_json_array VARIABLE_TO_STORE_RESULTS JQ_QUERY MODULE_CONFIG" >&2 15 | return 1 16 | fi 17 | 18 | readarray -t arr < <(echo "${module_config}" | jq -c -r "${jq_query}") 19 | } 20 | 21 | color_string() { 22 | local string="${1}" 23 | local color_code="${2}" 24 | local reset_code="\033[0m" 25 | 26 | # ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 27 | # Example color codes: 31=red, 32=green, 33=yellow, 34=blue, 35=magenta, 36=cyan, 37=white 28 | 29 | # Check if color code is provided, otherwise default to white (37) 30 | if [[ -z "${color_code}" ]]; then 31 | color_code="37" 32 | fi 33 | 34 | # Determine if we should force color 35 | if [ -n "${FORCE_COLOR:-}" ] || [ -n "${CLICOLOR_FORCE:-}" ]; then 36 | # Force color: Apply color codes regardless of whether output is a TTY 37 | echo -e "\033[${color_code}m${string}${reset_code}" 38 | elif [ -t 1 ]; then 39 | # Output is a TTY and color is not forced: Apply color codes 40 | echo -e "\033[${color_code}m${string}${reset_code}" 41 | else 42 | # Output is not a TTY: Do not apply color codes 43 | echo "${string}" 44 | fi 45 | } 46 | 47 | feature_enabled() { 48 | # Ensure the function is called with exactly one argument 49 | if [ "$#" -ne 1 ]; then 50 | echo "Usage: feature_enabled " >&2 51 | return 1 52 | fi 53 | 54 | local feature="$1" 55 | local -a features 56 | 57 | # Split BB_BUILD_FEATURES by commas and read into an array 58 | IFS=, 59 | read -r -a features <<< "$BB_BUILD_FEATURES" 60 | 61 | # Loop through the array and check for a match 62 | for f in "${features[@]}"; do 63 | # Trim leading and trailing whitespace 64 | local trimmed_f="${f## }" 65 | trimmed_f="${trimmed_f%% }" 66 | 67 | if [[ "$trimmed_f" == "$feature" ]]; then 68 | return 0 69 | fi 70 | done 71 | 72 | # Feature not found 73 | return 1 74 | } 75 | 76 | # Parse OS version and export it 77 | export OS_VERSION="$(awk -F= '/^VERSION_ID=/ {gsub(/"/, "", $2); print $2}' /usr/lib/os-release)" 78 | case "$TARGETARCH" in 79 | "amd64") 80 | OS_ARCH="x86_64" 81 | ;; 82 | "arm64") 83 | OS_ARCH="aarch64" 84 | ;; 85 | *) 86 | OS_ARCH="$TARGETARCH" 87 | ;; 88 | esac 89 | export OS_ARCH 90 | 91 | # Export functions for use in sub-shells or sourced scripts 92 | export -f get_json_array 93 | 94 | mkdir -p /var/roothome /var/opt /var/lib/alternatives /var/opt /var/usrlocal 95 | -------------------------------------------------------------------------------- /process/drivers/opts/run.rs: -------------------------------------------------------------------------------- 1 | use blue_build_utils::{container::ContainerId, platform::Platform}; 2 | use bon::Builder; 3 | use oci_client::Reference; 4 | 5 | #[expect(clippy::struct_excessive_bools)] 6 | #[derive(Debug, Clone, Copy, Builder)] 7 | #[builder(derive(Debug, Clone))] 8 | pub struct RunOpts<'scope> { 9 | pub image: &'scope str, 10 | 11 | #[builder(default)] 12 | pub args: &'scope [String], 13 | 14 | #[builder(default)] 15 | pub env_vars: &'scope [RunOptsEnv<'scope>], 16 | 17 | #[builder(default)] 18 | pub volumes: &'scope [RunOptsVolume<'scope>], 19 | pub user: Option<&'scope str>, 20 | 21 | #[builder(default)] 22 | pub privileged: bool, 23 | 24 | /// Run container rootless if possible, even if privileged. 25 | #[builder(default)] 26 | pub rootless: bool, 27 | 28 | #[builder(default)] 29 | pub pull: bool, 30 | 31 | #[builder(default)] 32 | pub remove: bool, 33 | pub platform: Option, 34 | } 35 | 36 | #[derive(Debug, Clone, Copy, Builder)] 37 | #[builder(derive(Debug, Clone))] 38 | pub struct RunOptsVolume<'scope> { 39 | pub path_or_vol_name: &'scope str, 40 | pub container_path: &'scope str, 41 | } 42 | 43 | #[macro_export] 44 | macro_rules! run_volumes { 45 | ($($host:expr => $container:expr),+ $(,)?) => { 46 | { 47 | ::bon::vec![ 48 | $($crate::drivers::opts::RunOptsVolume::builder() 49 | .path_or_vol_name($host) 50 | .container_path($container) 51 | .build(),)* 52 | ] 53 | } 54 | }; 55 | } 56 | 57 | #[derive(Debug, Clone, Copy, Builder)] 58 | #[builder(derive(Debug, Clone))] 59 | pub struct RunOptsEnv<'scope> { 60 | pub key: &'scope str, 61 | pub value: &'scope str, 62 | } 63 | 64 | #[macro_export] 65 | macro_rules! run_envs { 66 | ($($key:expr => $value:expr),+ $(,)?) => { 67 | { 68 | ::bon::vec![ 69 | $($crate::drivers::opts::RunOptsEnv::builder() 70 | .key($key) 71 | .value($value) 72 | .build(),)* 73 | ] 74 | } 75 | }; 76 | } 77 | 78 | #[derive(Debug, Clone, Copy, Builder)] 79 | #[builder(derive(Debug, Clone))] 80 | pub struct CreateContainerOpts<'scope> { 81 | pub image: &'scope Reference, 82 | 83 | #[builder(default)] 84 | pub privileged: bool, 85 | } 86 | 87 | #[derive(Debug, Clone, Copy, Builder)] 88 | #[builder(derive(Debug, Clone))] 89 | pub struct RemoveContainerOpts<'scope> { 90 | pub container_id: &'scope ContainerId, 91 | 92 | #[builder(default)] 93 | pub privileged: bool, 94 | } 95 | 96 | #[derive(Debug, Clone, Copy, Builder)] 97 | #[builder(derive(Debug, Clone))] 98 | pub struct RemoveImageOpts<'scope> { 99 | pub image: &'scope Reference, 100 | 101 | #[builder(default)] 102 | pub privileged: bool, 103 | } 104 | -------------------------------------------------------------------------------- /recipe/src/stages_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use bon::Builder; 7 | use miette::{Context, IntoDiagnostic, Report, Result}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::{FromFileList, Module, Stage, base_recipe_path}; 11 | 12 | #[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)] 13 | pub struct StagesExt { 14 | #[builder(default)] 15 | pub stages: Vec, 16 | } 17 | 18 | impl FromFileList for StagesExt { 19 | const LIST_KEY: &'static str = "stages"; 20 | 21 | fn get_from_file_paths(&self) -> Vec { 22 | self.stages 23 | .iter() 24 | .filter_map(Stage::get_from_file_path) 25 | .collect() 26 | } 27 | 28 | fn get_module_from_file_paths(&self) -> Vec { 29 | self.stages 30 | .iter() 31 | .flat_map(|stage| { 32 | stage 33 | .required_fields 34 | .as_ref() 35 | .map_or_else(Vec::new, |rf| rf.modules_ext.get_from_file_paths()) 36 | }) 37 | .collect() 38 | } 39 | } 40 | 41 | impl TryFrom<&PathBuf> for StagesExt { 42 | type Error = Report; 43 | 44 | fn try_from(value: &PathBuf) -> Result { 45 | Self::try_from(value.as_path()) 46 | } 47 | } 48 | 49 | impl TryFrom<&Path> for StagesExt { 50 | type Error = Report; 51 | 52 | fn try_from(file_name: &Path) -> Result { 53 | let file_path = base_recipe_path().join(file_name); 54 | 55 | let file = fs::read_to_string(&file_path) 56 | .into_diagnostic() 57 | .with_context(|| format!("Failed to open {}", file_path.display()))?; 58 | 59 | serde_yaml::from_str::(&file).map_or_else( 60 | |_| -> Result { 61 | let mut stage = serde_yaml::from_str::(&file) 62 | .into_diagnostic() 63 | .wrap_err_with(|| { 64 | format!("Failed to parse stage file {}", file_path.display()) 65 | })?; 66 | if let Some(ref mut rf) = stage.required_fields { 67 | rf.modules_ext.modules = Module::get_modules(&rf.modules_ext.modules, None)?; 68 | } 69 | Ok(Self::builder().stages(vec![stage]).build()) 70 | }, 71 | |mut stages_ext| -> Result { 72 | let mut stages: Vec = 73 | stages_ext.stages.iter().map(ToOwned::to_owned).collect(); 74 | for stage in &mut stages { 75 | if let Some(ref mut rf) = stage.required_fields { 76 | rf.modules_ext.modules = 77 | Module::get_modules(&rf.modules_ext.modules, None)?; 78 | } 79 | } 80 | stages_ext.stages = stages; 81 | Ok(stages_ext) 82 | }, 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /process/drivers/bootc_driver/status.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, path::PathBuf}; 2 | 3 | use blue_build_utils::{constants::OCI_ARCHIVE, container::ImageRef}; 4 | use log::warn; 5 | use oci_client::Reference; 6 | use serde::Deserialize; 7 | 8 | use crate::drivers::BootStatus; 9 | 10 | #[derive(Deserialize, Debug, Clone)] 11 | pub struct BootcStatus { 12 | status: BootcStatusExt, 13 | } 14 | 15 | #[derive(Deserialize, Debug, Clone)] 16 | struct BootcStatusExt { 17 | staged: Option, 18 | booted: BootcStatusImage, 19 | } 20 | 21 | #[derive(Deserialize, Debug, Clone)] 22 | struct BootcStatusImage { 23 | image: BootcStatusImageInfo, 24 | } 25 | 26 | #[derive(Deserialize, Debug, Clone)] 27 | struct BootcStatusImageInfo { 28 | image: BootcStatusImageInfoRef, 29 | } 30 | 31 | #[derive(Deserialize, Debug, Clone)] 32 | struct BootcStatusImageInfoRef { 33 | image: String, 34 | transport: String, 35 | } 36 | 37 | impl BootStatus for BootcStatus { 38 | fn transaction_in_progress(&self) -> bool { 39 | // Any call to bootc when a transaction is in progress 40 | // will cause the process to block effectively making 41 | // this check useless since bootc will continue with 42 | // the operation as soon as the current transaction is 43 | // completed. 44 | false 45 | } 46 | 47 | fn booted_image(&self) -> Option> { 48 | match self.status.booted.image.image.transport.as_str() { 49 | "registry" | "containers-storage" => Some(ImageRef::Remote(Cow::Owned( 50 | Reference::try_from(self.status.booted.image.image.image.as_str()) 51 | .inspect_err(|e| { 52 | warn!( 53 | "Failed to parse image ref {}:\n{e}", 54 | self.status.booted.image.image.image 55 | ); 56 | }) 57 | .ok()?, 58 | ))), 59 | transport if transport == OCI_ARCHIVE => Some(ImageRef::LocalTar(Cow::Owned( 60 | PathBuf::from(&self.status.booted.image.image.image), 61 | ))), 62 | _ => None, 63 | } 64 | } 65 | 66 | fn staged_image(&self) -> Option> { 67 | let staged = self.status.staged.as_ref()?; 68 | match staged.image.image.transport.as_str() { 69 | "registry" | "containers-storage" => Some(ImageRef::Remote(Cow::Owned( 70 | Reference::try_from(staged.image.image.image.as_str()) 71 | .inspect_err(|e| { 72 | warn!( 73 | "Failed to parse image ref {}:\n{e}", 74 | staged.image.image.image 75 | ); 76 | }) 77 | .ok()?, 78 | ))), 79 | transport if transport == OCI_ARCHIVE => Some(ImageRef::LocalTar(Cow::Owned( 80 | PathBuf::from(&staged.image.image.image), 81 | ))), 82 | _ => None, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/validate/location.rs: -------------------------------------------------------------------------------- 1 | use jsonschema::paths::{LazyLocation, Location as JsonLocation}; 2 | 3 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 4 | pub struct Location(JsonLocation); 5 | 6 | impl std::ops::Deref for Location { 7 | type Target = JsonLocation; 8 | 9 | fn deref(&self) -> &Self::Target { 10 | &self.0 11 | } 12 | } 13 | 14 | impl std::ops::DerefMut for Location { 15 | fn deref_mut(&mut self) -> &mut Self::Target { 16 | &mut self.0 17 | } 18 | } 19 | 20 | impl std::hash::Hash for Location { 21 | fn hash(&self, state: &mut H) { 22 | self.0.as_str().hash(state); 23 | } 24 | } 25 | 26 | impl From for Location { 27 | fn from(value: JsonLocation) -> Self { 28 | Self(value) 29 | } 30 | } 31 | 32 | impl From<&JsonLocation> for Location { 33 | fn from(value: &JsonLocation) -> Self { 34 | Self(value.clone()) 35 | } 36 | } 37 | 38 | impl std::fmt::Display for Location { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | self.0.fmt(f) 41 | } 42 | } 43 | 44 | impl TryFrom<&str> for Location { 45 | type Error = miette::Report; 46 | 47 | fn try_from(value: &str) -> Result { 48 | fn child<'a, 'b, 'c, I>(path_iter: &mut I, location: &'b LazyLocation<'b, 'a>) -> Location 49 | where 50 | I: Iterator, 51 | { 52 | let Some(path) = path_iter.next() else { 53 | return Location(JsonLocation::from(location)); 54 | }; 55 | let location = build(path, location); 56 | child(path_iter, &location) 57 | } 58 | 59 | fn build<'a, 'b>( 60 | path: &'a str, 61 | location: &'b LazyLocation<'b, 'a>, 62 | ) -> LazyLocation<'a, 'b> { 63 | path.parse::() 64 | .map_or_else(|_| location.push(path), |p| location.push(p)) 65 | } 66 | let path_count = value.split('/').count(); 67 | let mut path_iter = value.split('/'); 68 | 69 | let root = path_iter.next().unwrap(); 70 | 71 | if root.is_empty() && path_count == 1 { 72 | return Ok(Self::default()); 73 | } 74 | 75 | let Some(path) = path_iter.next() else { 76 | return Ok(Self(JsonLocation::from(&LazyLocation::new()))); 77 | }; 78 | 79 | let location = LazyLocation::new(); 80 | let location = build(path, &location); 81 | 82 | Ok(child(&mut path_iter, &location)) 83 | } 84 | } 85 | 86 | impl TryFrom<&String> for Location { 87 | type Error = miette::Report; 88 | 89 | fn try_from(value: &String) -> Result { 90 | Self::try_from(value.as_str()) 91 | } 92 | } 93 | 94 | impl TryFrom for Location { 95 | type Error = miette::Report; 96 | 97 | fn try_from(value: String) -> Result { 98 | Self::try_from(value.as_str()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /template/templates/stages.j2: -------------------------------------------------------------------------------- 1 | # This stage is responsible for holding onto 2 | # your config without copying it directly into 3 | # the final image 4 | {%- if self::files_dir_exists() %} 5 | FROM scratch AS stage-files 6 | COPY ./files /files 7 | {%- else if self::config_dir_exists() %} 8 | FROM scratch AS stage-config 9 | COPY ./config /config 10 | {%- else %} 11 | FROM scratch AS stage-files 12 | WORKDIR /files 13 | {% endif %} 14 | 15 | {%~ if self::modules_exists() %} 16 | # Copy modules 17 | # The default modules are inside blue-build/modules 18 | # Custom modules overwrite defaults 19 | FROM scratch AS stage-modules 20 | COPY ./modules /modules 21 | {% endif %} 22 | 23 | {%- if recipe.should_install_bins() %} 24 | # Bins to install 25 | # These are basic tools that are added to all images. 26 | # Generally used for the build process. We use a multi 27 | # stage process so that adding the bins into the image 28 | # can be added to the ostree commits. 29 | FROM scratch AS stage-bins 30 | {%- if recipe.should_install_cosign() %} 31 | COPY --from={{ blue_build_utils::constants::COSIGN_IMAGE_REF }}:{{ recipe.get_cosign_version() }} \ 32 | /ko-app/cosign /bins/cosign 33 | {%- endif %} 34 | {%- if recipe.should_install_bluebuild() %} 35 | COPY --from={{ blue_build_utils::constants::BLUE_BUILD_IMAGE_REF }}:{{ recipe.get_bluebuild_version() }} \ 36 | /out/bluebuild /bins/bluebuild 37 | {%- endif %} 38 | {%- endif %} 39 | 40 | # Keys for pre-verified images 41 | # Used to copy the keys into the final image 42 | # and perform an ostree commit. 43 | # 44 | # Currently only holds the current image's 45 | # public key. 46 | FROM scratch AS stage-keys 47 | {%- if self::has_cosign_file() %} 48 | COPY cosign.pub /keys/{{ recipe.name|replace('/', "_") }}.pub 49 | {% endif %} 50 | 51 | {%- include "modules/akmods/akmods.j2" %} 52 | 53 | {%~ if let Some(stages_ext) = recipe.stages_ext %} 54 | {%- for stage in stages_ext.stages %} 55 | {%- if let Some(stage) = stage.required_fields %} 56 | # {{ stage.name|capitalize }} stage 57 | FROM {{ stage.from }} AS {{ stage.name }} 58 | 59 | ARG TARGETARCH 60 | {%- if self::should_color() %} 61 | ARG FORCE_COLOR=1 62 | ARG CLICOLOR_FORCE=1 63 | ARG RUST_LOG_STYLE=always 64 | {%- endif %} 65 | 66 | {%- if stage.from != "scratch" %} 67 | COPY --from={{ blue_build_utils::constants::NUSHELL_IMAGE }}:{{ get_nu_version() }} /nu/* /usr/libexec/bluebuild/nu/ 68 | 69 | # Add compatibility for modules 70 | RUN {{ scripts_mount("/tmp/scripts/") }} \ 71 | /tmp/scripts/setup.sh 72 | 73 | {%- if self::config_dir_exists() %} 74 | ARG CONFIG_DIRECTORY="/tmp/config" 75 | {%- else %} 76 | ARG CONFIG_DIRECTORY="/tmp/files" 77 | {%- endif %} 78 | ARG MODULE_DIRECTORY="/tmp/modules" 79 | 80 | {%- if let Some(shell_args) = stage.shell %} 81 | SHELL [{% for arg in shell_args %}"{{ arg }}"{% if !loop.last %}, {% endif %}{% endfor %}] 82 | {%- else %} 83 | SHELL ["bash", "-c"] 84 | {%- endif %} 85 | {%- endif %} 86 | 87 | {% call modules::stage_modules_run(stage.modules_ext, os_version) %} 88 | {%- endif %} 89 | {%- endfor %} 90 | {%- endif %} 91 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-schemas": { 4 | "locked": { 5 | "lastModified": 1697467827, 6 | "narHash": "sha256-j8SR19V1SRysyJwpOBF4TLuAvAjF5t+gMiboN4gYQDU=", 7 | "rev": "764932025c817d4e500a8d2a4d8c565563923d29", 8 | "revCount": 29, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/flake-schemas/0.1.2/018b3da8-4cc3-7fbb-8ff7-1588413c53e2/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%2A.tar.gz" 15 | } 16 | }, 17 | "flake-utils": { 18 | "inputs": { 19 | "systems": "systems" 20 | }, 21 | "locked": { 22 | "lastModified": 1705309234, 23 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 24 | "owner": "numtide", 25 | "repo": "flake-utils", 26 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "numtide", 31 | "repo": "flake-utils", 32 | "type": "github" 33 | } 34 | }, 35 | "nixpkgs": { 36 | "locked": { 37 | "lastModified": 1708984720, 38 | "narHash": "sha256-gJctErLbXx4QZBBbGp78PxtOOzsDaQ+yw1ylNQBuSUY=", 39 | "rev": "13aff9b34cc32e59d35c62ac9356e4a41198a538", 40 | "revCount": 588909, 41 | "type": "tarball", 42 | "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.588909%2Brev-13aff9b34cc32e59d35c62ac9356e4a41198a538/018dec1e-579e-771e-9f64-eb8879874075/source.tar.gz" 43 | }, 44 | "original": { 45 | "type": "tarball", 46 | "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-schemas": "flake-schemas", 52 | "nixpkgs": "nixpkgs", 53 | "rust-overlay": "rust-overlay" 54 | } 55 | }, 56 | "rust-overlay": { 57 | "inputs": { 58 | "flake-utils": "flake-utils", 59 | "nixpkgs": [ 60 | "nixpkgs" 61 | ] 62 | }, 63 | "locked": { 64 | "lastModified": 1709086241, 65 | "narHash": "sha256-3QHK5zu/5XOa+ghBeKzvt+/BLdEPjw/xDNLcpDfbkmg=", 66 | "owner": "oxalica", 67 | "repo": "rust-overlay", 68 | "rev": "5d56056fb905ff550ee61b6ebb6674d494f57a9e", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "oxalica", 73 | "repo": "rust-overlay", 74 | "type": "github" 75 | } 76 | }, 77 | "systems": { 78 | "locked": { 79 | "lastModified": 1681028828, 80 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 81 | "owner": "nix-systems", 82 | "repo": "default", 83 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "nix-systems", 88 | "repo": "default", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | --------------------------------------------------------------------------------