├── .github
├── FUNDING.yml
├── codecov.yml
├── dependabot.yml
├── images
│ ├── build.gif
│ ├── goldboot-256.png
│ ├── overview.png
│ └── select_image.png
└── workflows
│ ├── check.yml
│ ├── prepare_release.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .gitmodules
├── .release-plz.toml
├── CONTRIBUTING.md
├── Cargo.lock
├── Cargo.toml
├── README.md
├── UNLICENSE
├── goldboot-image
├── CHANGELOG.md
├── Cargo.toml
├── src
│ ├── lib.rs
│ └── qcow
│ │ ├── header.rs
│ │ ├── levels.rs
│ │ ├── mod.rs
│ │ └── snapshot.rs
└── test
│ ├── empty.qcow2
│ └── small.qcow2
├── goldboot-macros
├── CHANGELOG.md
├── Cargo.toml
└── src
│ └── lib.rs
├── goldboot-registry
├── Cargo.toml
├── Dockerfile
├── README.md
└── src
│ ├── api
│ ├── build.rs
│ ├── image.rs
│ └── mod.rs
│ ├── cmd
│ └── mod.rs
│ ├── extract.rs
│ └── main.rs
├── goldboot
├── CHANGELOG.md
├── Cargo.toml
├── Dockerfile
├── README.md
├── build.rs
├── examples
│ └── apply_image_demo.rs
├── flake.lock
├── flake.nix
├── goldboot.json
└── src
│ ├── builder
│ ├── fabricators
│ │ ├── ansible.rs
│ │ ├── exe.rs
│ │ ├── mod.rs
│ │ └── shell.rs
│ ├── http.rs
│ ├── mod.rs
│ ├── options
│ │ ├── arch.rs
│ │ ├── dns_resolver.rs
│ │ ├── hostname.rs
│ │ ├── iso.rs
│ │ ├── locale.rs
│ │ ├── luks.rs
│ │ ├── mod.rs
│ │ ├── size.rs
│ │ ├── timezone.rs
│ │ └── unix_account.rs
│ ├── os
│ │ ├── alpine_linux
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ └── mod.rs
│ │ ├── android
│ │ │ └── mod.rs
│ │ ├── arch_linux
│ │ │ ├── archinstall.rs
│ │ │ ├── bootstrap.sh
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ └── mod.rs
│ │ ├── buildroot
│ │ │ └── mod.rs
│ │ ├── debian
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ ├── mod.rs
│ │ │ └── preseed.cfg
│ │ ├── fedora
│ │ │ ├── icon.png
│ │ │ └── icon@2x.png
│ │ ├── linux_mint
│ │ │ ├── icon.png
│ │ │ └── icon@2x.png
│ │ ├── mac_os
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ └── mod.rs
│ │ ├── mod.rs
│ │ ├── nix
│ │ │ └── mod.rs
│ │ ├── open_suse
│ │ │ ├── icon.png
│ │ │ └── icon@2x.png
│ │ ├── pop_os
│ │ │ └── mod.rs
│ │ ├── slackware
│ │ │ ├── icon.png
│ │ │ └── icon@2x.png
│ │ ├── steam_deck
│ │ │ └── mod.rs
│ │ ├── steam_os
│ │ │ └── mod.rs
│ │ ├── ubuntu
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ └── mod.rs
│ │ ├── windows_10
│ │ │ ├── configure_winrm.ps1
│ │ │ ├── icon.png
│ │ │ ├── icon@2x.png
│ │ │ └── mod.rs
│ │ ├── windows_11
│ │ │ └── mod.rs
│ │ └── windows_7
│ │ │ └── mod.rs
│ ├── ovmf
│ │ ├── aarch64.fd.zst
│ │ ├── i386.fd.zst
│ │ ├── mod.rs
│ │ └── x86_64.fd.zst
│ ├── qemu.rs
│ ├── sources.rs
│ ├── sources
│ │ └── buildroot.rs
│ ├── ssh.rs
│ └── vnc.rs
│ ├── cli
│ ├── cmd
│ │ ├── build.rs
│ │ ├── deploy.rs
│ │ ├── image.rs
│ │ ├── init.rs
│ │ ├── liveusb.rs
│ │ ├── mod.rs
│ │ └── registry.rs
│ ├── mod.rs
│ ├── progress.rs
│ └── prompt.rs
│ ├── config.rs
│ ├── gbl.rs
│ ├── gui
│ ├── app.rs
│ ├── mod.rs
│ ├── resources.rs
│ ├── resources
│ │ ├── icons
│ │ │ ├── hdd.png
│ │ │ ├── nvme.png
│ │ │ ├── ram.png
│ │ │ └── ssd.png
│ │ └── logo-512.png
│ ├── screens
│ │ ├── apply_image.rs
│ │ ├── confirm.rs
│ │ ├── mod.rs
│ │ ├── registry_login.rs
│ │ ├── select_device.rs
│ │ └── select_image.rs
│ ├── state.rs
│ ├── theme.rs
│ └── widgets
│ │ ├── header.rs
│ │ ├── hotkeys.rs
│ │ └── mod.rs
│ ├── lib.rs
│ ├── library.rs
│ ├── main.rs
│ ├── pivot_root.rs
│ └── registry
│ ├── api
│ ├── image.rs
│ └── mod.rs
│ └── mod.rs
├── rustfmt.toml
└── shell.nix
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: cilki
4 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | # ref: https://docs.codecov.com/docs/codecovyml-reference
2 | coverage:
3 | # Hold ourselves to a low bar
4 | range: 50..100
5 | round: down
6 | precision: 1
7 | status:
8 | # ref: https://docs.codecov.com/docs/commit-status
9 | project:
10 | default:
11 | # Avoid false negatives
12 | threshold: 1%
13 |
14 | # Test files aren't important for coverage
15 | ignore:
16 | - "tests"
17 |
18 | # Make comments less noisy
19 | comment:
20 | layout: "files"
21 | require_changes: true
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: "cargo"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/.github/images/build.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/.github/images/build.gif
--------------------------------------------------------------------------------
/.github/images/goldboot-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/.github/images/goldboot-256.png
--------------------------------------------------------------------------------
/.github/images/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/.github/images/overview.png
--------------------------------------------------------------------------------
/.github/images/select_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/.github/images/select_image.png
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: check
2 | permissions:
3 | contents: read
4 | # This configuration allows maintainers of this repo to create a branch and pull request based on
5 | # the new branch. Restricting the push trigger to the main branch ensures that the PR only gets
6 | # built once.
7 | on:
8 | push:
9 | branches: [master]
10 | pull_request:
11 | # If new code is pushed to a PR branch, then cancel in progress workflows for that PR. Ensures that
12 | # we don't waste CI time, and returns results quicker https://github.com/jonhoo/rust-ci-conf/pull/5
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
15 | cancel-in-progress: true
16 | jobs:
17 | fmt:
18 | runs-on: ubuntu-latest
19 | if: "!contains(github.event.head_commit.message, 'chore: release')"
20 | name: stable / fmt
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Install stable
24 | uses: dtolnay/rust-toolchain@stable
25 | with:
26 | components: rustfmt
27 | - name: cargo fmt --check
28 | run: cargo fmt --check
29 | clippy:
30 | runs-on: ubuntu-latest
31 | if: "!contains(github.event.head_commit.message, 'chore: release')"
32 | name: ${{ matrix.toolchain }} / clippy
33 | permissions:
34 | contents: read
35 | checks: write
36 | strategy:
37 | fail-fast: false
38 | matrix:
39 | # Get early warning of new lints which are regularly introduced in beta channels.
40 | toolchain: [stable, beta]
41 | steps:
42 | - uses: actions/checkout@v4
43 | - name: Install ${{ matrix.toolchain }}
44 | uses: dtolnay/rust-toolchain@master
45 | with:
46 | toolchain: ${{ matrix.toolchain }}
47 | components: clippy
48 | - name: cargo clippy
49 | uses: giraffate/clippy-action@v1
50 | with:
51 | reporter: "github-pr-check"
52 | github_token: ${{ secrets.GITHUB_TOKEN }}
53 | doc:
54 | runs-on: ubuntu-24.04
55 | if: "!contains(github.event.head_commit.message, 'chore: release')"
56 | name: nightly / doc
57 | steps:
58 | - run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libgraphene-1.0-dev libudev-dev libgtk-4-dev libglib2.0-dev
59 | - uses: actions/checkout@v4
60 | - name: Install nightly
61 | uses: dtolnay/rust-toolchain@nightly
62 | - name: cargo doc
63 | run: cargo doc --no-deps --all-features
64 | env:
65 | RUSTDOCFLAGS: --cfg docsrs
66 | msrv:
67 | runs-on: ubuntu-24.04
68 | if: "!contains(github.event.head_commit.message, 'chore: release')"
69 | strategy:
70 | matrix:
71 | msrv: ["1.74.0"]
72 | name: ubuntu / ${{ matrix.msrv }}
73 | steps:
74 | - run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libgraphene-1.0-dev libudev-dev libgtk-4-dev libglib2.0-dev
75 | - uses: actions/checkout@v4
76 | - name: Install ${{ matrix.msrv }}
77 | uses: dtolnay/rust-toolchain@master
78 | with:
79 | toolchain: ${{ matrix.msrv }}
80 | - name: cargo +${{ matrix.msrv }} check
81 | run: cargo check
82 |
--------------------------------------------------------------------------------
/.github/workflows/prepare_release.yml:
--------------------------------------------------------------------------------
1 | name: prepare_release
2 | permissions:
3 | pull-requests: write
4 | contents: write
5 | on:
6 | push:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | github:
12 | runs-on: ubuntu-latest
13 | if: "!contains(github.event.head_commit.message, 'chore: release')"
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - uses: dtolnay/rust-toolchain@stable
20 |
21 | - uses: MarcoIeni/release-plz-action@v0.5
22 | with:
23 | command: release-pr
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 |
27 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | permissions:
3 | contents: read
4 | on:
5 | push:
6 | branches: [master]
7 | pull_request:
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 | jobs:
12 | required:
13 | runs-on: ubuntu-24.04
14 | if: "!contains(github.event.head_commit.message, 'chore: release')"
15 | name: ubuntu / ${{ matrix.toolchain }}
16 | strategy:
17 | matrix:
18 | # run on stable and beta to ensure that tests won't break on the next version of the rust
19 | # toolchain
20 | toolchain: [stable, beta]
21 | steps:
22 | - run: sudo apt-get update && sudo apt-get install -y qemu-utils libpango1.0-dev libgraphene-1.0-dev libudev-dev libgtk-4-dev libglib2.0-dev
23 | - uses: actions/checkout@v4
24 | - name: Install ${{ matrix.toolchain }}
25 | uses: dtolnay/rust-toolchain@master
26 | with:
27 | toolchain: ${{ matrix.toolchain }}
28 | - name: cargo generate-lockfile
29 | # enable this ci template to run regardless of whether the lockfile is checked in or not
30 | if: hashFiles('Cargo.lock') == ''
31 | run: cargo generate-lockfile
32 | # https://twitter.com/jonhoo/status/1571290371124260865
33 | - name: cargo test --locked
34 | run: cargo test --locked --all-features
35 | # https://github.com/rust-lang/cargo/issues/6669
36 | - name: cargo test --doc
37 | run: cargo test --locked --all-features --doc
38 | os-check:
39 | # run cargo test on mac and windows
40 | runs-on: ${{ matrix.os }}
41 | if: "!contains(github.event.head_commit.message, 'chore: release')"
42 | name: ${{ matrix.os }} / stable
43 | strategy:
44 | fail-fast: false
45 | matrix:
46 | os: [macos-latest, windows-latest]
47 | steps:
48 | - uses: actions/checkout@v4
49 | - name: Install stable
50 | uses: dtolnay/rust-toolchain@stable
51 | - name: cargo generate-lockfile
52 | if: hashFiles('Cargo.lock') == ''
53 | run: cargo generate-lockfile
54 | - name: cargo test
55 | run: cargo test --locked --all-features --all-targets --workspace --exclude goldboot-uki
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | screenshots
3 | result
4 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.release-plz.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = "# Changelog"
3 | body = "Body"
4 | trim = true
5 | protect_breaking_commits = true
6 | sort_commits = "newest"
7 |
8 | commit_parsers = [
9 | { message = "^.*: add", group = "Added" },
10 | { message = "^.*: support", group = "Added" },
11 | { message = "^.*: remove", group = "Removed" },
12 | { message = "^.*: delete", group = "Removed" },
13 | { message = "^test", group = "Fixed" },
14 | { message = "^fix", group = "Fixed" },
15 | { message = "^.*: fix", group = "Fixed" },
16 | ]
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This guide contains some information for first time contributors.
4 |
5 | ## Architecture
6 |
7 | There are currently four crates to know about:
8 |
9 | - `goldboot`
10 | - The primary CLI application for building images
11 | - `goldboot-image`
12 | - Implements the goldboot image format
13 | - `goldboot-macros`
14 | - Procedural macros to simplify implementation of the `goldboot` crate
15 | - `goldboot-registry`
16 | - Web service that hosts goldboot images
17 |
18 | ## The metallurgy metaphor
19 |
20 | Although end-users could mostly ignore it, the internals of `goldboot` use
21 | vocabulary appropriated from the field of metallurgy.
22 |
23 | ### Element
24 |
25 | An image element takes an image source and refines it according to built-in
26 | rules.
27 |
28 | For example, the `ArchLinux` element knows how to take Arch Linux install media
29 | (in the form of an ISO) and install it in an automated manner.
30 |
31 | ### Building
32 |
33 | Building is the process that takes image sources and produces a final goldboot
34 | image containing all customizations.
35 |
36 | Under the hood, foundries cast images by spawning a Qemu virtual machine and
37 | running one or more image molds against it (via SSH or VNC). Once the virtual
38 | machine is fully provisioned, its shutdown and the underlying storage (.qcow2)
39 | is converted into a final goldboot image (.gb).
40 |
41 | ### Alloys
42 |
43 | An alloy is a multi-boot image that consists of more than one element.
44 |
45 | ### Fabricators
46 |
47 | Operates on images at the end of the casting process. For example, the shell
48 | fabricator runs shell commands on the image which can be useful in many cases.
49 |
50 | ### Casting
51 |
52 | If we followed the metaphor strictly, then _casting_ would be a synonym for
53 | _building_, but we decided instead to make it be the process of applying an
54 | image to a device.
55 |
56 | ## Supporting new operating systems
57 |
58 | If `goldboot` doesn't already support your operating system, it should be
59 | possible to add it relatively easily.
60 |
61 | Start by finding an OS similar to yours in the `goldboot::builder::os` module.
62 |
63 | TODO
64 |
65 | ## OS maintenance
66 |
67 | OS support often need constant maintenance as new upstream releases are made and
68 | old ones are retired. Typically this involves adding new versions and marking
69 | some as deprecated, but occasionally upstream changes may cause breakages for
70 | us.
71 |
72 | For example, we have a struct that tracks Alpine releases which needs to be
73 | updated about twice per year:
74 |
75 | ```rust
76 | #[derive(Clone, Serialize, Deserialize, Debug, EnumIter)]
77 | pub enum AlpineRelease {
78 | Edge,
79 | #[serde(rename = "v3.17")]
80 | V3_17,
81 | #[serde(rename = "v3.16")]
82 | V3_16,
83 | #[serde(rename = "v3.15")]
84 | V3_15,
85 | #[deprecated]
86 | #[serde(rename = "v3.14")]
87 | V3_14,
88 | }
89 | ```
90 |
91 | ## Testing
92 |
93 | TODO
94 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [profile.release]
2 | strip = true
3 |
4 | [workspace]
5 | members = [
6 | "goldboot",
7 | "goldboot-image",
8 | "goldboot-macros",
9 | "goldboot-registry",
10 | ]
11 |
12 | [workspace.dependencies]
13 | anyhow = "1.0.76"
14 | fossable = "0.1.2"
15 | hex = "0.4.3"
16 | rand = "0.9.0"
17 | regex = "1.10.2"
18 | serde = { version = "1.0.192", features = ["derive"] }
19 | sha2 = "0.10.8"
20 | strum = { version = "0.27.1", features = ["derive"] }
21 | tracing = "0.1.40"
22 | zstd = "0.13.0"
23 | reqwest = "0.12.15"
24 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/goldboot-image/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
--------------------------------------------------------------------------------
/goldboot-image/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = ["Tyler Cook"]
3 | description = "Defines the goldboot image format"
4 | edition = "2024"
5 | homepage = "https://goldboot.fossable.org"
6 | license = "Unlicense"
7 | name = "goldboot-image"
8 | repository = "https://github.com/fossable/goldboot"
9 | rust-version = "1.85"
10 | version = "0.0.5"
11 |
12 | [dependencies]
13 | aes-gcm = { version = "0.10.3", features = ["std"] }
14 | anyhow = { workspace = true }
15 | binrw = "0.15.0"
16 | flate2 = "1.0.28"
17 | hex = { workspace = true }
18 | rand = { workspace = true }
19 | regex = { workspace = true }
20 | serde = { workspace = true }
21 | sha2 = { workspace = true }
22 | strum = { workspace = true }
23 | tracing = { workspace = true }
24 | zstd = { workspace = true }
25 |
26 | [dev-dependencies]
27 | sha1 = "0.10.6"
28 | tempfile = "3.8.1"
29 | test-log = { version = "0.2.16", features = ["trace"] }
30 |
--------------------------------------------------------------------------------
/goldboot-image/src/qcow/header.rs:
--------------------------------------------------------------------------------
1 | use binrw::BinRead;
2 |
3 | /// Qcow header version 3.
4 | #[derive(BinRead, Debug)]
5 | #[br(magic = b"QFI\xfb")]
6 | pub struct QcowHeader {
7 | /// Version of the QCOW format.
8 | #[br(assert(version == 3))]
9 | pub version: u32,
10 |
11 | /// Offset into the image file at which the backing file name
12 | /// is stored (NB: The string is not null terminated). 0 if the
13 | /// image doesn't have a backing file.
14 | _backing_file_offset: u64,
15 |
16 | /// Length of the backing file name in bytes. Must not be
17 | /// longer than 1023 bytes. Undefined if the image doesn't have
18 | /// a backing file.
19 | _backing_file_size: u32,
20 |
21 | /// Number of bits that are used for addressing an offset
22 | /// within a cluster (1 << cluster_bits is the cluster size).
23 | /// Must not be less than 9 (i.e. 512 byte clusters).
24 | pub cluster_bits: u32,
25 |
26 | /// Virtual disk size in bytes.
27 | pub size: u64,
28 |
29 | /// Encryption method to use for contents
30 | _crypt_method: u32,
31 |
32 | /// Number of entries in the active L1 table
33 | pub l1_size: u32,
34 |
35 | /// Offset into the image file at which the active L1 table
36 | /// starts. Must be aligned to a cluster boundary.
37 | pub l1_table_offset: u64,
38 |
39 | /// Offset into the image file at which the refcount table
40 | /// starts. Must be aligned to a cluster boundary.
41 | _refcount_table_offset: u64,
42 |
43 | /// Number of clusters that the refcount table occupies
44 | _refcount_table_clusters: u32,
45 |
46 | /// Number of snapshots contained in the image
47 | pub nb_snapshots: u32,
48 |
49 | /// Offset into the image file at which the snapshot table
50 | /// starts. Must be aligned to a cluster boundary.
51 | pub snapshots_offset: u64,
52 |
53 | /// Bitmask of incompatible features. An implementation must fail to open an
54 | /// image if an unknown bit is set.
55 | #[br(align_after = 8)]
56 | _incompatible_features: u64,
57 |
58 | /// Bitmask of compatible features. An implementation can safely ignore any
59 | /// unknown bits that are set.
60 | _compatible_features: u64,
61 |
62 | /// Bitmask of auto-clear features. An implementation may only write to an
63 | /// image with unknown auto-clear features if it clears the respective bits
64 | /// from this field first.
65 | _autoclear_features: u64,
66 |
67 | /// Describes the width of a reference count block entry (width
68 | /// in bits: refcount_bits = 1 << refcount_order). For version 2
69 | /// images, the order is always assumed to be 4
70 | /// (i.e. refcount_bits = 16).
71 | /// This value may not exceed 6 (i.e. refcount_bits = 64).
72 | _refcount_order: u32,
73 |
74 | /// Total length of the header.
75 | pub header_len: u32,
76 |
77 | /// Defines the compression method used for compressed clusters.
78 | ///
79 | /// All compressed clusters in an image use the same compression
80 | /// type.
81 | ///
82 | /// If the incompatible bit "Compression type" is set: the field
83 | /// must be present and non-zero (which means non-zlib
84 | /// compression type). Otherwise, this field must not be present
85 | /// or must be zero (which means zlib).
86 | #[br(if(header_len > 104))]
87 | pub compression_type: CompressionType,
88 |
89 | /// Marks the end of the extensions
90 | _end: u32,
91 | }
92 |
93 | /// Compression type used for compressed clusters.
94 | #[derive(BinRead, Debug, Clone, Copy, PartialEq, Eq, Default)]
95 | #[br(repr(u8))]
96 | pub enum CompressionType {
97 | /// Uses flate/zlib compression for any clusters which are compressed
98 | #[default]
99 | Zlib = 0,
100 |
101 | /// Uses zstandard compression for any clusters which are compressed
102 | Zstd = 1,
103 | }
104 |
105 | impl QcowHeader {
106 | /// Get the size of a cluster in bytes from the qcow
107 | pub fn cluster_size(&self) -> u64 {
108 | 1 << self.cluster_bits
109 | }
110 |
111 | /// Get the number of entries in an L2 table.
112 | pub fn l2_entries_per_cluster(&self) -> u64 {
113 | self.cluster_size() / 8
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/goldboot-image/src/qcow/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Result, bail};
2 | use binrw::{BinRead, BinReaderExt, io::SeekFrom};
3 | use snapshot::Snapshot;
4 | use std::{
5 | fs::File,
6 | io::BufReader,
7 | path::Path,
8 | process::{Command, Stdio},
9 | };
10 | use tracing::debug;
11 |
12 | mod header;
13 | pub use header::*;
14 |
15 | pub mod levels;
16 | use levels::*;
17 |
18 | mod snapshot;
19 |
20 | /// Represents a (stripped down) qcow3 file on disk.
21 | #[derive(BinRead, Debug)]
22 | #[brw(big)]
23 | pub struct Qcow3 {
24 | /// The image header
25 | pub header: QcowHeader,
26 |
27 | /// List of snapshots present within this qcow
28 | #[br(seek_before = SeekFrom::Start(header.snapshots_offset), count = header.nb_snapshots)]
29 | pub snapshots: Vec,
30 |
31 | /// The "active" L1 table
32 | #[br(seek_before = SeekFrom::Start(header.l1_table_offset), count = header.l1_size)]
33 | pub l1_table: Vec,
34 |
35 | /// The file path
36 | #[br(ignore)]
37 | pub path: String,
38 | }
39 |
40 | impl Qcow3 {
41 | /// Open a qcow3 file from the given path.
42 | pub fn open(path: impl AsRef) -> Result {
43 | let mut file = BufReader::new(File::open(&path)?);
44 |
45 | let mut qcow: Qcow3 = file.read_be()?;
46 | qcow.path = path.as_ref().to_string_lossy().to_string();
47 |
48 | debug!(qcow = ?qcow, "Opened qcow image");
49 | Ok(qcow)
50 | }
51 |
52 | /// Allocate a new qcow3 file.
53 | pub fn create(path: impl AsRef, size: u64) -> Result {
54 | let path = path.as_ref();
55 |
56 | // If we don't pass an image size that's a power of two, qemu-img will
57 | // silently round up which is bad.
58 | assert!(size % 2 == 0, "The image size must be a power of 2");
59 |
60 | debug!(path = ?path, "Creating qcow storage");
61 | let status = Command::new("qemu-img")
62 | .args([
63 | "create",
64 | "-f",
65 | "qcow2",
66 | "-o",
67 | "cluster_size=65536",
68 | &path.to_string_lossy().to_string(),
69 | &format!("{size}"),
70 | ])
71 | .stdout(Stdio::null())
72 | .stderr(Stdio::null())
73 | .status()
74 | .unwrap();
75 |
76 | if status.code().unwrap() != 0 {
77 | bail!("Failed to allocate image with qemu-img");
78 | }
79 |
80 | Qcow3::open(path)
81 | }
82 |
83 | /// Count the number of allocated clusters.
84 | pub fn count_clusters(&self) -> Result {
85 | let mut count = 0;
86 |
87 | for l1_entry in &self.l1_table {
88 | if let Some(l2_table) =
89 | l1_entry.read_l2(&mut File::open(&self.path)?, self.header.cluster_bits)
90 | {
91 | for l2_entry in l2_table {
92 | if l2_entry.is_used {
93 | count += 1;
94 | }
95 | }
96 | }
97 | }
98 | Ok(count)
99 | }
100 | }
101 |
102 | #[cfg(test)]
103 | mod tests {
104 | use super::*;
105 |
106 | #[test]
107 | fn test_open() -> Result<()> {
108 | let qcow = Qcow3::open("test/empty.qcow2")?;
109 | assert_eq!(qcow.header.cluster_bits, 16);
110 | assert_eq!(qcow.header.cluster_size(), 65536);
111 | Ok(())
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/goldboot-image/src/qcow/snapshot.rs:
--------------------------------------------------------------------------------
1 | use super::levels::L1Entry;
2 | use binrw::{BinRead, binread, io::SeekFrom};
3 |
4 | /// An entry in the snapshot table representing the system state at a moment in
5 | /// time
6 | #[binread]
7 | #[derive(Debug)]
8 | pub struct Snapshot {
9 | /// Offset into the image file at which the L1 table for the
10 | /// snapshot starts. Must be aligned to a cluster boundary.
11 | #[br(temp)]
12 | l1_table_offset: u64,
13 |
14 | /// Number of entries in the L1 table of the snapshots
15 | #[br(temp)]
16 | l1_entry_count: u32,
17 |
18 | /// Table of L1 entries in the screenshot
19 | #[br(restore_position, seek_before = SeekFrom::Start(l1_table_offset), count = l1_entry_count)]
20 | pub l1_table: Vec,
21 |
22 | /// Length of the unique ID string describing the snapshot
23 | #[br(temp)]
24 | unique_id_len: u16,
25 |
26 | /// Length of the name of the snapshot
27 | #[br(temp)]
28 | name_len: u16,
29 |
30 | /// Time at which the snapshot was taken since the Epoch
31 | pub time: SnapshotTime,
32 |
33 | /// Time that the guest was running until the snapshot was taken in
34 | /// nanoseconds
35 | pub guest_runtime: u64,
36 |
37 | /// Size of the VM state in bytes. 0 if no VM state is saved.
38 | ///
39 | /// If there is VM state, it starts at the first cluster
40 | /// described by first L1 table entry that doesn't describe a
41 | /// regular guest cluster (i.e. VM state is stored like guest
42 | /// disk content, except that it is stored at offsets that are
43 | /// larger than the virtual disk presented to the guest)
44 | pub vm_state_size: u32,
45 |
46 | #[br(temp)]
47 | extra_data_size: u32,
48 |
49 | /// Optional extra snapshot data that comes from format updates
50 | #[br(pad_size_to = extra_data_size)]
51 | #[br(args(extra_data_size))]
52 | pub extra_data: SnapshotExtraData,
53 |
54 | /// A unique identifier for the snapshot (example value: "1")
55 | #[br(count = unique_id_len, try_map = String::from_utf8)]
56 | pub unique_id: String,
57 |
58 | /// Name of the snapshot
59 | #[br(count = name_len, try_map = String::from_utf8)]
60 | pub name: String,
61 | }
62 |
63 | /// Optional extra snapshot data that comes from format updates
64 | ///
65 | /// **Note:** Version 3 snapshots must have both vm_state_size and
66 | /// virtual_disk_size present.
67 | #[derive(BinRead, Debug)]
68 | #[br(import(size: u32))]
69 | pub struct SnapshotExtraData {
70 | /// Size of the VM state in bytes. 0 if no VM state is saved. If this field
71 | /// is present, the 32-bit value in Snapshot.vm_state_size is ignored.
72 | #[br(if(size >= 8))]
73 | pub vm_state_size: u64,
74 |
75 | /// Virtual disk size of the snapshot in bytes
76 | #[br(if(size >= 16))]
77 | pub virtual_disk_size: Option,
78 |
79 | /// icount value which corresponds to the record/replay instruction count
80 | /// when the snapshot was taken. Set to -1 if icount was disabled
81 | #[br(if(size >= 24))]
82 | pub instruction_count: Option,
83 | }
84 |
85 | /// Represents the time a snapshot was taken in the form of seconds,
86 | /// nanoseconds. The nanoseconds represent the sub-second time of the snapshot.
87 | #[derive(BinRead, Debug)]
88 | pub struct SnapshotTime {
89 | /// Seconds since the unix epoch
90 | pub secs: u32,
91 |
92 | /// Subsecond portion of time in nanoseconds
93 | pub nanosecs: u32,
94 | }
95 |
--------------------------------------------------------------------------------
/goldboot-image/test/empty.qcow2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot-image/test/empty.qcow2
--------------------------------------------------------------------------------
/goldboot-image/test/small.qcow2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot-image/test/small.qcow2
--------------------------------------------------------------------------------
/goldboot-macros/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
--------------------------------------------------------------------------------
/goldboot-macros/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = ["Tyler Cook"]
3 | description = "Supporting macros for goldboot"
4 | edition = "2024"
5 | license = "Unlicense"
6 | name = "goldboot-macros"
7 | rust-version = "1.85"
8 | version = "0.0.3"
9 | repository = "https://github.com/fossable/goldboot"
10 |
11 | [lib]
12 | proc-macro = true
13 |
14 | [dependencies]
15 | quote = "1.0.33"
16 | syn = "2.0.39"
17 |
--------------------------------------------------------------------------------
/goldboot-macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | use proc_macro::TokenStream;
2 | use quote::quote;
3 | use syn::{self};
4 |
5 | /// Automatically implement "Prompt" for all fields in a struct.
6 | #[proc_macro_derive(Prompt)]
7 | pub fn prompt(input: TokenStream) -> TokenStream {
8 | let ast = syn::parse(input).unwrap();
9 | impl_prompt(&ast)
10 | }
11 |
12 | /// Automatically implement the "size()" method from BuildImage trait.
13 | /// This assumes the struct has a field named "size" of type Size.
14 | #[proc_macro_derive(BuildImageSize)]
15 | pub fn build_image_size(input: TokenStream) -> TokenStream {
16 | let ast = syn::parse(input).unwrap();
17 | impl_build_image_size(&ast)
18 | }
19 |
20 | fn impl_prompt(ast: &syn::DeriveInput) -> TokenStream {
21 | let name = &ast.ident;
22 |
23 | let fields_to_prompt: Vec<_> = match &ast.data {
24 | syn::Data::Struct(data) => match &data.fields {
25 | syn::Fields::Named(fields) => fields
26 | .named
27 | .iter()
28 | .filter_map(|f| {
29 | // Skip fields with #[serde(flatten)]
30 | let is_flattened = f.attrs.iter().any(|attr| {
31 | attr.path().is_ident("serde")
32 | && attr
33 | .parse_args::()
34 | .map(|i| i == "flatten")
35 | .unwrap_or(false)
36 | });
37 |
38 | if is_flattened {
39 | None
40 | } else {
41 | Some((f.ident.clone().unwrap(), f.ty.clone()))
42 | }
43 | })
44 | .collect(),
45 | _ => panic!("Prompt derive only works on structs with named fields"),
46 | },
47 | _ => panic!("Prompt derive only works on structs"),
48 | };
49 |
50 | let prompt_calls = fields_to_prompt.iter().map(|(field, ty)| {
51 | // Check if the type is an Option
52 | if is_option_type(ty) {
53 | quote! {
54 | if let Some(ref mut value) = self.#field {
55 | value.prompt(builder)?;
56 | }
57 | }
58 | } else {
59 | quote! {
60 | self.#field.prompt(builder)?;
61 | }
62 | }
63 | });
64 |
65 | let syntax = quote! {
66 | impl crate::cli::prompt::Prompt for #name {
67 | fn prompt(
68 | &mut self,
69 | builder: &crate::builder::Builder,
70 | ) -> anyhow::Result<()> {
71 | #(#prompt_calls)*
72 | Ok(())
73 | }
74 | }
75 | };
76 | syntax.into()
77 | }
78 |
79 | fn is_option_type(ty: &syn::Type) -> bool {
80 | if let syn::Type::Path(type_path) = ty {
81 | if let Some(segment) = type_path.path.segments.last() {
82 | return segment.ident == "Option";
83 | }
84 | }
85 | false
86 | }
87 |
88 | fn impl_build_image_size(ast: &syn::DeriveInput) -> TokenStream {
89 | let name = &ast.ident;
90 |
91 | // Verify that the struct has a field named "size"
92 | let has_size_field = match &ast.data {
93 | syn::Data::Struct(data) => match &data.fields {
94 | syn::Fields::Named(fields) => fields
95 | .named
96 | .iter()
97 | .any(|f| f.ident.as_ref().map(|i| i == "size").unwrap_or(false)),
98 | _ => false,
99 | },
100 | _ => false,
101 | };
102 |
103 | if !has_size_field {
104 | panic!("BuildImageSize derive requires a field named 'size'");
105 | }
106 |
107 | let syntax = quote! {
108 | impl BuildImage for #name {
109 | fn size(&self) -> &crate::builder::options::size::Size {
110 | &self.size
111 | }
112 | }
113 | };
114 | syntax.into()
115 | }
116 |
117 | // TODO add pyclass
118 |
--------------------------------------------------------------------------------
/goldboot-registry/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "goldboot-registry"
3 | description = "A web service for hosting goldboot images"
4 | version = "0.0.5"
5 | edition = "2024"
6 | license = "Unlicense"
7 | authors = ["Tyler Cook"]
8 | readme = "README.md"
9 | homepage = "https://goldboot.org"
10 | repository = "https://github.com/fossable/goldboot"
11 | rust-version = "1.85"
12 |
13 | [dependencies]
14 | anyhow = "1.0.76"
15 | axum = "0.8.3"
16 | clap = { version = "4.4.7", features = ["derive", "string"] }
17 | goldboot-image = { path = "../goldboot-image", version = "0.0.5" }
18 | goldboot = { path = "../goldboot", version = "0.0.10" }
19 | reqwest = { workspace = true, features = ["stream"] }
20 | tftpd = { version = "0.5.0", optional = true }
21 | tokio = { version = "1.34.0", features = ["full"] }
22 | tracing = "0.1.40"
23 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
24 |
25 | [features]
26 | pxe = ["dep:tftpd"]
27 |
--------------------------------------------------------------------------------
/goldboot-registry/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | ARG TARGETOS
4 | ARG TARGETARCH
5 | ARG TARGETVARIANT
6 |
7 | RUN apk add --no-cache qemu-system-aarch64 qemu-system-arm qemu-system-x86_64
8 |
9 | COPY ${TARGETOS}-${TARGETARCH}${TARGETVARIANT}/goldboot-registry /usr/bin/goldboot-registry
10 | RUN chmod +x /usr/bin/goldboot-registry
11 |
12 | WORKDIR /root
13 | ENTRYPOINT ["/usr/bin/goldboot-registry"]
14 |
--------------------------------------------------------------------------------
/goldboot-registry/README.md:
--------------------------------------------------------------------------------
1 | ## goldboot-registry
2 |
3 | Contains the web application that manages goldboot images.
4 |
--------------------------------------------------------------------------------
/goldboot-registry/src/api/build.rs:
--------------------------------------------------------------------------------
1 | use axum::extract::Path;
2 |
3 | /// Start a new build
4 | pub async fn start() {}
5 |
6 | /// List all builds
7 | pub async fn list() {}
8 |
9 | /// Get build info
10 | pub async fn info(Path(_id): Path) {}
11 |
12 | /// Cancel a build
13 | pub async fn cancel(Path(_id): Path) {}
14 |
--------------------------------------------------------------------------------
/goldboot-registry/src/api/image.rs:
--------------------------------------------------------------------------------
1 | use crate::extract::ImageHandle;
2 | use axum::{
3 | Json,
4 | extract::{Path, State},
5 | http::StatusCode,
6 | };
7 | use goldboot::registry::api::image::ImageInfoResponse;
8 |
9 | /// Get image info
10 | pub async fn info(image: ImageHandle) -> Json {
11 | Json(image.0.into())
12 | }
13 |
14 | /// Get image list
15 | pub async fn list() {}
16 |
17 | // Push an image
18 | /*
19 | pub async fn push(id: web::Path, rq: actix_web::HttpRequest) -> Result {
20 | let path = match ImageLibrary::find_by_id(&id) {
21 | Ok(image) => {
22 | // Delete if the image already exists
23 | if Path::new(&image.path).exists() {
24 | std::fs::remove_file(&image.path)?;
25 | }
26 | image.path
27 | },
28 | _ => format!("{}.gb", id),
29 | };
30 |
31 | let mut file = File::create(&path)?;
32 | std::io::copy(&mut rq, &mut file)?;
33 | ""
34 | }*/
35 |
36 | /// Get cluster data
37 | pub async fn clusters(Path(_id): Path, Path(_range): Path) {}
38 |
39 | /// Get cluster hashes
40 | pub async fn hashes(Path(_id): Path, Path(_range): Path) {}
41 |
--------------------------------------------------------------------------------
/goldboot-registry/src/api/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod build;
2 | pub mod image;
3 |
--------------------------------------------------------------------------------
/goldboot-registry/src/cmd/mod.rs:
--------------------------------------------------------------------------------
1 | #[derive(clap::Subcommand, Debug)]
2 | pub enum Commands {
3 | /// Run the registry server
4 | Start {},
5 | }
6 |
--------------------------------------------------------------------------------
/goldboot-registry/src/extract.rs:
--------------------------------------------------------------------------------
1 | use super::RegistryState;
2 | use axum::{
3 | extract::{FromRequest, Path, Request},
4 | http::StatusCode,
5 | };
6 | use goldboot::library::ImageLibrary;
7 | use std::collections::HashMap;
8 | use tracing::error;
9 |
10 | /// Newtype wrapper for ImageHandle.
11 | pub struct ImageHandle(pub goldboot_image::ImageHandle);
12 |
13 | impl FromRequest for ImageHandle {
14 | type Rejection = StatusCode;
15 |
16 | async fn from_request(req: Request, state: &RegistryState) -> Result {
17 | match Path::>::from_request(req, state).await {
18 | Ok(value) => match value.get("image_id") {
19 | Some(image_id) => match ImageLibrary::find_by_id(image_id) {
20 | Ok(image_handle) => {
21 | // TODO access control
22 | Ok(ImageHandle(image_handle))
23 | }
24 | Err(_) => Err(StatusCode::NOT_FOUND),
25 | },
26 | None => {
27 | error!("No image id");
28 | Err(StatusCode::INTERNAL_SERVER_ERROR)
29 | }
30 | },
31 | Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/goldboot-registry/src/main.rs:
--------------------------------------------------------------------------------
1 | use crate::cmd::Commands;
2 | use axum::{Router, routing::get};
3 | use clap::Parser;
4 | use std::{env, process::ExitCode};
5 |
6 | pub mod api;
7 | pub mod cmd;
8 | pub mod extract;
9 |
10 | #[derive(Parser, Debug)]
11 | #[clap(author, version, about, long_about = None)]
12 | struct CommandLine {
13 | #[clap(subcommand)]
14 | command: Option,
15 | }
16 |
17 | #[derive(Clone)]
18 | pub struct RegistryState {}
19 |
20 | #[tokio::main]
21 | async fn main() {
22 | let command_line = CommandLine::parse();
23 | tracing_subscriber::fmt()
24 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
25 | .init();
26 |
27 | let state = RegistryState {};
28 |
29 | let app = Router::new()
30 | .route("/image/list", get(api::image::list))
31 | .route("/image/info/:image_id", get(api::image::info))
32 | .with_state(state);
33 |
34 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
35 | axum::serve(listener, app).await.unwrap();
36 | }
37 |
--------------------------------------------------------------------------------
/goldboot/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
--------------------------------------------------------------------------------
/goldboot/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = ["Tyler Cook"]
3 | description = "A command-line application for building goldboot images"
4 | edition = "2024"
5 | homepage = "https://goldboot.org"
6 | license = "Unlicense"
7 | name = "goldboot"
8 | readme = "README.md"
9 | repository = "https://github.com/fossable/goldboot"
10 | rust-version = "1.85"
11 | version = "0.0.10"
12 |
13 | [dependencies]
14 | anyhow = { workspace = true }
15 | # It seems the next LTS will have a clang version new enough for bindgen
16 | # aws-lc-rs = { version = "1", features = ["bindgen"]}
17 | axum = { version = "0.8.1", optional = true }
18 | block-utils = { version = "0.11.1", optional = true }
19 | built = { version = "0.8.0", features = ["chrono", "semver"] }
20 | byte-unit = "5.1.2"
21 | chrono = "0.4.31"
22 | clap = { version = "4.4.7", features = ["derive", "string"] }
23 | console = "0.16.1"
24 | dialoguer = "0.12.0"
25 | eframe = { version = "0.29", optional = true, default-features = false, features = ["default_fonts", "glow", "persistence"] }
26 | egui = { version = "0.29", optional = true }
27 | enum_dispatch = "0.3.12"
28 | fatfs = { version = "0.3.6", optional = true }
29 | flate2 = "1.0.28"
30 | fossable = { workspace = true }
31 | fscommon = { version = "0.1.1", optional = true }
32 | goldboot-image = { path = "../goldboot-image", version = "0.0.5" }
33 | goldboot-macros = { path = "../goldboot-macros", version = "0.0.3" }
34 | hex = { workspace = true }
35 | image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
36 | indicatif = "0.18.0"
37 | png = { version = "0.18.0", optional = true }
38 | poll-promise = { version = "0.3", optional = true }
39 | pyo3 = { version = "0.26.0", features = ["anyhow"], optional = true }
40 | pythonize = { version = "0.26.0", optional = true }
41 | quick-xml = { version = "0.38.3", features = ["serialize"] }
42 | rand = { workspace = true }
43 | regex = { workspace = true }
44 | reqwest = { workspace = true, features = ["stream", "blocking", "json"] }
45 | ron = { version = "0.11.0", optional = true }
46 | rustls = { version = "0.23.23" }
47 | serde_json = { version = "1.0.108", optional = true }
48 | serde = { workspace = true }
49 | serde_win_unattend = { version = "0.3.3", optional = true }
50 | smart-default = "0.7.1"
51 | serde_yaml = { version = "0.9.27", optional = true }
52 | sha1 = "0.10.6"
53 | sha2 = { workspace = true }
54 | # TODO russh
55 | ssh2 = { version = "0.9.4" }
56 | ssh-key = { version = "0.7.0-pre.1", features = ["ed25519"] }
57 | strum = { workspace = true }
58 | tar = "0.4.40"
59 | tempfile = "3.10.0"
60 | tokio = { version = "1.36.0", features = ["full"] }
61 | toml = { version = "0.9.7", optional = true }
62 | tower-http = { version = "0.6.6", features = ["fs", "trace"] }
63 | tracing = { workspace = true }
64 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
65 | ubyte = "0.10.4"
66 | url = { version = "2.4.1", features = ["serde"] }
67 | uuid = { version = "1.7.0", features = ["v4"] }
68 | validator = { version = "0.20.0", features = ["derive"] }
69 | vnc = { version = "0.4.0", optional = true }
70 | whoami = "1.4.1"
71 | zstd = { workspace = true }
72 |
73 | [dev-dependencies]
74 |
75 | [build-dependencies]
76 | built = { version = "0.8.0", features = [
77 | "cargo-lock",
78 | "dependency-tree",
79 | "git2",
80 | "chrono",
81 | "semver",
82 | ] }
83 |
84 | [features]
85 | default = [
86 | "build",
87 | "include_ovmf",
88 | "config-json",
89 | "config-yaml",
90 | "config-ron",
91 | "config-toml",
92 | "config-python",
93 | ]
94 |
95 | # Support for registry server
96 | registry = []
97 |
98 | # Support for building images
99 | build = [
100 | "dep:fatfs",
101 | "dep:fscommon",
102 | "dep:png",
103 | "dep:vnc",
104 | "dep:serde_win_unattend",
105 | "dep:axum",
106 | ]
107 |
108 | # Bundled OVMF firmware
109 | include_ovmf = []
110 |
111 | # GUI support using egui/eframe
112 | gui = [
113 | "dep:eframe",
114 | "dep:egui",
115 | "dep:image",
116 | "dep:poll-promise",
117 | "dep:block-utils",
118 | ]
119 |
120 | # UKI (Unified Kernel Image) mode - GUI with automatic reboot
121 | uki = ["gui"]
122 |
123 | # Configuration formats
124 | config-json = ["dep:serde_json"]
125 | config-yaml = ["dep:serde_yaml"]
126 | config-ron = ["dep:ron"]
127 | config-toml = ["dep:toml"]
128 | config-python = ["dep:pyo3", "dep:pythonize"]
129 |
--------------------------------------------------------------------------------
/goldboot/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | ARG TARGETOS
4 | ARG TARGETARCH
5 | ARG TARGETVARIANT
6 |
7 | RUN apk add --no-cache \
8 | gtk4.0 \
9 | qemu \
10 | qemu-img \
11 | qemu-system-aarch64 \
12 | qemu-system-arm \
13 | qemu-system-x86_64
14 |
15 | COPY ${TARGETOS}-${TARGETARCH}${TARGETVARIANT}/goldboot /usr/bin/goldboot
16 | RUN chmod +x /usr/bin/goldboot
17 |
18 | WORKDIR /root
19 | ENTRYPOINT ["/usr/bin/goldboot"]
20 |
--------------------------------------------------------------------------------
/goldboot/README.md:
--------------------------------------------------------------------------------
1 | ## goldboot
2 |
3 | Contains the command-line application for building, pushing, pulling, and writing goldboot images.
--------------------------------------------------------------------------------
/goldboot/build.rs:
--------------------------------------------------------------------------------
1 | use std::{env, path::PathBuf};
2 |
3 | fn main() {
4 | if built::write_built_file().is_err() {
5 | let dest =
6 | std::path::Path::new(&env::var("OUT_DIR").expect("OUT_DIR not set")).join("built.rs");
7 | built::write_built_file_with_opts(Some(&PathBuf::from("..")), &dest)
8 | .expect("Failed to acquire build-time information");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/goldboot/examples/apply_image_demo.rs:
--------------------------------------------------------------------------------
1 | //! Demo of the apply_image screen with simulated image writing.
2 | //!
3 | //! This example demonstrates the block visualization and progress tracking in the GUI
4 | //! by simulating an image write operation with realistic speeds and progress updates.
5 | //!
6 | //! Run with: cargo run --example apply_image_demo --features gui
7 |
8 | use goldboot::gui::{
9 | app::GuiApp,
10 | state::{AppState, BlockState, WriteProgress},
11 | screens::Screen,
12 | };
13 | use std::{
14 | sync::{Arc, Mutex},
15 | thread,
16 | time::{Duration, Instant},
17 | };
18 |
19 | fn main() -> Result<(), eframe::Error> {
20 | // Initialize logging
21 | tracing_subscriber::fmt()
22 | .with_env_filter(
23 | tracing_subscriber::EnvFilter::from_default_env()
24 | .add_directive(tracing::Level::INFO.into()),
25 | )
26 | .init();
27 |
28 | // Create a custom app state that starts directly on the ApplyImage screen
29 | let mut state = AppState::new();
30 |
31 | // Simulate a 1GB image write
32 | let total_size = 1024 * 1024 * 1024u64;
33 | state.init_write_progress(total_size);
34 |
35 | // Clone the progress Arc for the write thread
36 | let progress_arc = state.write_progress.clone().unwrap();
37 |
38 | // Spawn a background thread to perform simulated write with realistic speeds
39 | thread::spawn(move || {
40 | let start_time = Instant::now();
41 | let mut last_update = Instant::now();
42 |
43 | // Simulate write speed of ~400 MB/s
44 | let target_write_speed = 400_000_000.0; // bytes per second
45 | let mut bytes_written = 0u64;
46 | let block_size = 4 * 1024 * 1024u64; // 4MB blocks
47 |
48 | while bytes_written < total_size {
49 | // Sleep to simulate realistic write speed
50 | thread::sleep(Duration::from_millis(10));
51 |
52 | let now = Instant::now();
53 | let elapsed_ms = now.duration_since(last_update).as_millis() as f64;
54 |
55 | // Calculate how many bytes we should have written in this interval
56 | let bytes_to_write = ((target_write_speed / 1000.0) * elapsed_ms) as u64;
57 | let bytes_to_write = bytes_to_write.min(total_size - bytes_written);
58 |
59 | bytes_written += bytes_to_write;
60 | last_update = now;
61 |
62 | if let Ok(mut progress) = progress_arc.lock() {
63 | // Update percentage
64 | progress.percentage = bytes_written as f32 / total_size as f32;
65 |
66 | // Calculate current block index
67 | let current_block = (bytes_written / block_size) as usize;
68 | let current_block = current_block.min(progress.blocks_total.saturating_sub(1));
69 |
70 | // Mark completed blocks as written
71 | for i in 0..current_block {
72 | if i < progress.block_states.len() && progress.block_states[i] != BlockState::Written {
73 | progress.block_states[i] = BlockState::Written;
74 | progress.blocks_written += 1;
75 | }
76 | }
77 |
78 | // Mark current block(s) as writing (simulate 2-3 blocks writing simultaneously)
79 | progress.blocks_writing = 0;
80 | for i in current_block..=(current_block + 2).min(progress.blocks_total.saturating_sub(1)) {
81 | if i < progress.block_states.len() && progress.block_states[i] == BlockState::Pending {
82 | progress.block_states[i] = BlockState::Writing;
83 | progress.blocks_writing += 1;
84 | }
85 | }
86 |
87 | // Update speeds with some realistic variation
88 | let speed_variation = (rand::random::() - 0.5) * 0.1; // ±10% variation
89 | progress.write_speed = target_write_speed * (1.0 + speed_variation);
90 | progress.read_speed = progress.write_speed * 1.05; // Read slightly faster
91 | }
92 | }
93 |
94 | // Mark all blocks as written when complete
95 | if let Ok(mut progress) = progress_arc.lock() {
96 | progress.percentage = 1.0;
97 | for i in 0..progress.blocks_total {
98 | if i < progress.block_states.len() && progress.block_states[i] != BlockState::Written {
99 | progress.block_states[i] = BlockState::Written;
100 | progress.blocks_written += 1;
101 | }
102 | }
103 | progress.blocks_writing = 0;
104 | }
105 |
106 | println!("Simulated write completed in {:.2}s", start_time.elapsed().as_secs_f64());
107 | });
108 |
109 | // Create and run the GUI app starting on ApplyImage screen
110 | let native_options = eframe::NativeOptions {
111 | viewport: egui::ViewportBuilder::default()
112 | .with_inner_size([1024.0, 768.0])
113 | .with_title("Goldboot - Apply Image Demo"),
114 | ..Default::default()
115 | };
116 |
117 | // Create app with custom initial state
118 | eframe::run_native(
119 | "goldboot",
120 | native_options,
121 | Box::new(move |cc| {
122 | Ok(Box::new(GuiApp {
123 | state,
124 | textures: goldboot::gui::resources::TextureCache::new(&cc.egui_ctx),
125 | theme: goldboot::gui::theme::Theme::default(),
126 | screen: Screen::ApplyImage, // Start directly on ApplyImage
127 | }))
128 | }),
129 | )
130 | }
131 |
--------------------------------------------------------------------------------
/goldboot/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1763966396,
24 | "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixos-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/goldboot/goldboot.json:
--------------------------------------------------------------------------------
1 | {
2 | "os": "ArchLinux",
3 | "hostname": "goldboot",
4 | "mirrorlist": null,
5 | "root_password": {
6 | "plaintext": "root"
7 | },
8 | "iso": {
9 | "url": "http://mirrors.edge.kernel.org/archlinux/iso/latest/archlinux-2025.10.01-x86_64.iso",
10 | "checksum": null
11 | }
12 | }
--------------------------------------------------------------------------------
/goldboot/src/builder/fabricators/ansible.rs:
--------------------------------------------------------------------------------
1 | use crate::builder::Builder;
2 | use crate::{builder::ssh::SshConnection, cli::prompt::Prompt};
3 | use anyhow::Result;
4 | use anyhow::bail;
5 | use serde::{Deserialize, Serialize};
6 | use std::{path::Path, process::Command};
7 | use tracing::info;
8 | use validator::Validate;
9 |
10 | use super::Fabricate;
11 |
12 | /// Runs an Ansible playbook on the image remotely.
13 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
14 | pub struct Ansible {
15 | /// The playbook file
16 | pub playbook: String,
17 |
18 | /// The inventory file
19 | pub inventory: Option,
20 | }
21 |
22 | impl Ansible {
23 | pub fn run(&self, ssh: &mut SshConnection) -> Result<()> {
24 | info!("Running ansible provisioner");
25 |
26 | if let Some(code) = Command::new("ansible-playbook")
27 | .arg("--ssh-common-args")
28 | .arg("-o StrictHostKeyChecking=no")
29 | .arg("-e")
30 | .arg(format!("ansible_port={}", ssh.port))
31 | .arg("-e")
32 | .arg(format!("ansible_user={}", ssh.username))
33 | .arg("-e")
34 | .arg(format!(
35 | "ansible_ssh_private_key_file={}",
36 | ssh.private_key.display()
37 | ))
38 | .arg("-e")
39 | .arg("ansible_connection=ssh")
40 | .arg(&self.playbook)
41 | .status()
42 | .expect("Failed to launch ansible-playbook")
43 | .code()
44 | {
45 | if code != 0 {
46 | bail!("Provisioning failed");
47 | }
48 | }
49 |
50 | Ok(())
51 | }
52 | }
53 |
54 | impl Fabricate for Ansible {
55 | fn run(&self, _ssh: &mut SshConnection) -> Result<()> {
56 | todo!()
57 | }
58 | }
59 |
60 | impl Prompt for Ansible {
61 | fn prompt(&mut self, _: &Builder) -> Result<()> {
62 | self.playbook = dialoguer::Input::with_theme(&crate::cli::cmd::init::theme())
63 | .with_prompt("Enter the playbook path relative to the current directory")
64 | .interact()?;
65 |
66 | if !Path::new(&self.playbook).exists() {
67 | if !dialoguer::Confirm::with_theme(&crate::cli::cmd::init::theme())
68 | .with_prompt("The path does not exist. Add anyway?")
69 | .interact()?
70 | {
71 | bail!("The playbook did not exist");
72 | }
73 | }
74 |
75 | self.validate()?;
76 | Ok(())
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/goldboot/src/builder/fabricators/exe.rs:
--------------------------------------------------------------------------------
1 | use super::Fabricate;
2 | use crate::builder::Builder;
3 | use crate::{builder::ssh::SshConnection, cli::prompt::Prompt};
4 | use anyhow::Result;
5 | use anyhow::bail;
6 | use serde::{Deserialize, Serialize};
7 | use std::path::Path;
8 | use tracing::info;
9 | use validator::Validate;
10 |
11 | /// Runs an executable file.
12 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
13 | pub struct HostExecutable {
14 | /// The path to the executable
15 | pub path: String,
16 | }
17 |
18 | impl Fabricate for HostExecutable {
19 | fn run(&self, ssh: &mut SshConnection) -> Result<()> {
20 | info!("Running executable");
21 |
22 | if ssh.upload_exec(&std::fs::read(&self.path)?, vec![])? != 0 {
23 | bail!("Executable failed");
24 | }
25 | Ok(())
26 | }
27 | }
28 |
29 | impl Prompt for HostExecutable {
30 | fn prompt(&mut self, _: &Builder) -> Result<()> {
31 | self.path = dialoguer::Input::with_theme(&crate::cli::cmd::init::theme())
32 | .with_prompt("Enter the script path relative to the current directory")
33 | .interact()?;
34 |
35 | if !Path::new(&self.path).exists() {
36 | if !dialoguer::Confirm::with_theme(&crate::cli::cmd::init::theme())
37 | .with_prompt("The path does not exist. Add anyway?")
38 | .interact()?
39 | {
40 | bail!("The playbook did not exist");
41 | }
42 | }
43 |
44 | self.validate()?;
45 | Ok(())
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/goldboot/src/builder/fabricators/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains various common provisioners which may be included in
2 | //! image templates. Templates may also specify their own specialized
3 | //! provisioners for specific tasks.
4 |
5 | use crate::builder::ssh::SshConnection;
6 | use ansible::Ansible;
7 | use anyhow::Result;
8 | use enum_dispatch::enum_dispatch;
9 | use serde::{Deserialize, Serialize};
10 |
11 | pub mod ansible;
12 | pub mod exe;
13 | pub mod shell;
14 |
15 | /// A `Fabricator` performs some custom operation on an image at the very end of
16 | /// the build process.
17 | #[enum_dispatch(Fabricator)]
18 | pub trait Fabricate {
19 | fn run(&self, ssh: &mut SshConnection) -> Result<()>;
20 | }
21 |
22 | #[enum_dispatch]
23 | #[derive(Clone, Serialize, Deserialize, Debug)]
24 | pub enum Fabricator {
25 | Ansible,
26 | }
27 |
--------------------------------------------------------------------------------
/goldboot/src/builder/fabricators/shell.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use anyhow::bail;
3 | use serde::{Deserialize, Serialize};
4 | use tracing::info;
5 | use validator::Validate;
6 |
7 | use crate::builder::ssh::SshConnection;
8 |
9 | /// Runs an inline shell command.
10 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
11 | pub struct ShellFabricator {
12 | /// The inline command to run
13 | pub command: String,
14 | }
15 |
16 | impl ShellFabricator {
17 | /// Create a new shell fabricator with inline command
18 | pub fn new(command: &str) -> Self {
19 | Self {
20 | command: command.to_string(),
21 | }
22 | }
23 |
24 | pub fn run(&self, ssh: &mut SshConnection) -> Result<()> {
25 | info!("Running shell commands");
26 |
27 | if ssh.exec(&self.command)? != 0 {
28 | bail!("Shell commands failed");
29 | }
30 | Ok(())
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/goldboot/src/builder/http.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use axum::{Router, extract::MatchedPath, http::Request};
3 | use std::path::Path;
4 | use tempfile::TempDir;
5 | use tokio::runtime::Runtime;
6 | use tower_http::{
7 | services::ServeFile,
8 | trace::TraceLayer,
9 | };
10 | use tracing::{debug, debug_span, info};
11 |
12 | /// Minimal HTTP server for serving files to virtual machines
13 | pub struct HttpServer {
14 | pub port: u16,
15 | pub address: String,
16 | pub directory: TempDir,
17 | }
18 |
19 | impl HttpServer {
20 | pub fn new() -> Result {
21 | Ok(HttpServerBuilder {
22 | router: Router::new(),
23 | directory: tempfile::tempdir()?,
24 | })
25 | }
26 | }
27 |
28 | pub struct HttpServerBuilder {
29 | router: Router,
30 | directory: TempDir,
31 | }
32 |
33 | impl HttpServerBuilder {
34 | pub fn file(mut self, path: &str, data: C) -> Result
35 | where
36 | C: AsRef<[u8]>,
37 | {
38 | let tmp_path = self.directory.path().join(Path::new(path));
39 | std::fs::create_dir_all(tmp_path.parent().expect("tempdir has a parent"))?;
40 | std::fs::write(&tmp_path, data)?;
41 |
42 | let path = format!("/{}", path.trim_start_matches("/"));
43 | debug!(path = %path, tmp_path = ?tmp_path, "Registered HTTP route");
44 | self.router = self.router.route_service(&path, ServeFile::new(tmp_path));
45 | Ok(self)
46 | }
47 |
48 | pub fn serve(self) -> HttpServer {
49 | let port = crate::find_open_port(8000, 9000);
50 | info!("Starting HTTP server on port: {}", port);
51 |
52 | let router = self.router.layer(TraceLayer::new_for_http().make_span_with(
53 | |request: &Request<_>| {
54 | // Log the matched route's path (with placeholders not filled in).
55 | // Use request.uri() or OriginalUri if you want the real path.
56 | let matched_path = request
57 | .extensions()
58 | .get::()
59 | .map(MatchedPath::as_str);
60 |
61 | debug_span!(
62 | "http_request",
63 | method = ?request.method(),
64 | matched_path,
65 | )
66 | },
67 | ));
68 |
69 | std::thread::spawn(move || {
70 | Runtime::new().unwrap().block_on(async move {
71 | let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port))
72 | .await
73 | .unwrap();
74 | axum::serve(listener, router).await.unwrap();
75 | });
76 | });
77 |
78 | HttpServer {
79 | port,
80 | address: "10.0.2.2".to_string(),
81 | directory: self.directory,
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/goldboot/src/builder/mod.rs:
--------------------------------------------------------------------------------
1 | use self::qemu::{Accel, detect_accel};
2 | use self::{fabricators::Fabricator, os::Os};
3 | use crate::builder::os::BuildImage;
4 | use crate::cli::cmd::Commands;
5 | use crate::library::ImageLibrary;
6 | use crate::size;
7 |
8 | use anyhow::{Result, anyhow, bail};
9 | use byte_unit::Byte;
10 | use dialoguer::Password;
11 | use goldboot_image::ElementHeader;
12 | use goldboot_image::{ImageArch, ImageHandle, qcow::Qcow3};
13 | use rand::Rng;
14 | use serde::{Deserialize, Serialize};
15 | use std::{
16 | path::{Path, PathBuf},
17 | thread,
18 | time::SystemTime,
19 | };
20 | use tracing::info;
21 | use validator::Validate;
22 |
23 | pub mod fabricators;
24 | pub mod http;
25 | pub mod options;
26 | pub mod os;
27 | pub mod ovmf;
28 | pub mod qemu;
29 | pub mod sources;
30 | pub mod ssh;
31 | pub mod vnc;
32 |
33 | /// Machinery that creates Goldboot images from image elements.
34 | #[derive(Validate)]
35 | pub struct Builder {
36 | pub elements: Vec,
37 |
38 | pub accel: Accel,
39 |
40 | pub debug: bool,
41 |
42 | pub record: bool,
43 |
44 | /// A general purpose temporary directory for the run
45 | pub tmp: tempfile::TempDir,
46 |
47 | pub ovmf_path: PathBuf,
48 | pub qcow_path: PathBuf,
49 |
50 | /// VNC port for the VM
51 | pub vnc_port: u16,
52 |
53 | /// End time of the run
54 | pub end_time: Option,
55 |
56 | /// Start time of the run
57 | pub start_time: Option,
58 | }
59 |
60 | impl Builder {
61 | pub fn new(elements: Vec) -> Self {
62 | // Allocate directory for the builder to store the intermediate qcow image
63 | // and any other supporting files.
64 | let tmp = tempfile::tempdir().unwrap();
65 |
66 | Self {
67 | accel: detect_accel(),
68 | debug: false,
69 | record: false,
70 | end_time: None,
71 | qcow_path: tmp.path().join("image.gb.qcow2"),
72 | start_time: None,
73 | vnc_port: rand::rng().random_range(5900..5999),
74 | elements,
75 | ovmf_path: tmp.path().join("OVMF.fd"),
76 | tmp,
77 | }
78 | }
79 |
80 | /// The system architecture
81 | pub fn arch(&self) -> Result {
82 | match self.elements.first() {
83 | Some(element) => {
84 | todo!()
85 | }
86 | None => bail!("No elements in builder"),
87 | }
88 | }
89 |
90 | /// Run the image build process according to the given command line.
91 | pub fn run(&mut self, cli: Commands) -> Result<()> {
92 | self.start_time = Some(SystemTime::now());
93 |
94 | let qcow_size: u64 = self
95 | .elements
96 | .iter()
97 | .map(|element| -> u64 { size!(element).into() })
98 | .sum();
99 |
100 | match cli {
101 | Commands::Build {
102 | record,
103 | debug,
104 | read_password,
105 | no_accel,
106 | output,
107 | path,
108 | ovmf_path,
109 | } => {
110 | self.debug = debug;
111 | self.record = record;
112 |
113 | // Set VNC port predictably in debug mode
114 | if debug {
115 | self.vnc_port = 5900;
116 | }
117 |
118 | // Prompt password
119 | let password = if read_password {
120 | Some(
121 | Password::with_theme(&crate::cli::cmd::init::theme())
122 | .with_prompt("Image encryption passphrase")
123 | .interact()?,
124 | )
125 | } else {
126 | None
127 | };
128 |
129 | // Disable VM acceleration if requested
130 | if no_accel {
131 | self.accel = Accel::Tcg;
132 | }
133 |
134 | // Override from command line
135 | if let Some(path) = ovmf_path {
136 | self.ovmf_path = PathBuf::from(path);
137 | } else {
138 | // Try to find OVMF firmware or unpack what's included
139 | if let Some(path) = crate::builder::ovmf::find() {
140 | self.ovmf_path = path;
141 | } else if cfg!(feature = "include_ovmf") {
142 | let path = self
143 | .tmp
144 | .path()
145 | .join("OVMF.fd")
146 | .to_string_lossy()
147 | .to_string();
148 |
149 | #[cfg(feature = "include_ovmf")]
150 | crate::builder::ovmf::write(self.arch()?, &path).unwrap();
151 | self.ovmf_path = PathBuf::from(path);
152 | }
153 | }
154 |
155 | // Check OVMF firmware path
156 | if !self.ovmf_path.exists() {
157 | bail!("No OVMF firmware found");
158 | }
159 |
160 | // Truncate the image size to a power of two for the qcow storage
161 | let qcow = Qcow3::create(&self.qcow_path, qcow_size - (qcow_size % 2))?;
162 | for element in self.elements.clone().into_iter() {
163 | element.build(&self)?;
164 | }
165 |
166 | // Convert into final immutable image
167 | let path = if let Some(output) = output.as_ref() {
168 | PathBuf::from(output)
169 | } else {
170 | ImageLibrary::open().temporary()
171 | };
172 |
173 | ImageHandle::from_qcow(Vec::new(), &qcow, &path, password, |_, _| {})?;
174 |
175 | if let None = output {
176 | ImageLibrary::open().add_move(path.clone())?;
177 | }
178 | }
179 | _ => panic!("Must be passed a Commands::Build"),
180 | }
181 |
182 | self.end_time = Some(SystemTime::now());
183 | info!(
184 | duration = ?self.start_time.unwrap().elapsed()?,
185 | "Build completed",
186 | );
187 |
188 | Ok(())
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/arch.rs:
--------------------------------------------------------------------------------
1 | use crate::{builder::Builder, cli::prompt::Prompt};
2 | use anyhow::Result;
3 | use goldboot_image::ImageArch;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Clone, Serialize, Deserialize, Debug)]
7 | pub struct Arch(pub ImageArch);
8 |
9 | impl Prompt for Arch {
10 | fn prompt(&mut self, builder: &Builder) -> Result<()> {
11 | todo!()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/dns_resolver.rs:
--------------------------------------------------------------------------------
1 | pub struct DnsResolverProvisioner {}
2 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/hostname.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use crate::{builder::Builder, cli::prompt::Prompt};
4 | use anyhow::Result;
5 | use serde::{Deserialize, Serialize};
6 | use validator::Validate;
7 |
8 | /// Sets the network hostname.
9 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
10 | pub struct Hostname {
11 | // TODO validate
12 | pub hostname: String,
13 | }
14 |
15 | impl Display for Hostname {
16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 | write!(f, "{}", self.hostname)
18 | }
19 | }
20 |
21 | impl Default for Hostname {
22 | fn default() -> Self {
23 | Self {
24 | hostname: String::from("goldboot"),
25 | }
26 | }
27 | }
28 |
29 | impl Prompt for Hostname {
30 | fn prompt(&mut self, builder: &Builder) -> Result<()> {
31 | self.hostname = dialoguer::Input::with_theme(&crate::cli::cmd::init::theme())
32 | .with_prompt("Enter network hostname")
33 | // .default(builder.name.clone())
34 | .interact()?;
35 |
36 | self.validate()?;
37 | Ok(())
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/iso.rs:
--------------------------------------------------------------------------------
1 | use crate::{builder::Builder, cli::prompt::Prompt};
2 | use anyhow::Result;
3 | use serde::{Deserialize, Serialize};
4 | use url::Url;
5 | use validator::Validate;
6 |
7 | /// Use an ISO image as a source.
8 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
9 | pub struct Iso {
10 | /// The installation media URL (http, https, or file)
11 | pub url: Url,
12 |
13 | /// A hash of the installation media
14 | pub checksum: Option,
15 | }
16 |
17 | impl Prompt for Iso {
18 | fn prompt(&mut self, _: &Builder) -> Result<()> {
19 | self.url = dialoguer::Input::with_theme(&crate::cli::cmd::init::theme())
20 | .with_prompt("Enter the ISO URL")
21 | .interact()?;
22 |
23 | self.validate()?;
24 | Ok(())
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/locale.rs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/luks.rs:
--------------------------------------------------------------------------------
1 | use crate::{builder::Builder, cli::prompt::Prompt};
2 | use anyhow::Result;
3 | use dialoguer::{Confirm, Password};
4 | use serde::{Deserialize, Serialize};
5 | use validator::Validate;
6 |
7 | /// Configures a LUKS encrypted root filesystem
8 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
9 | pub struct Luks {
10 | /// The LUKS passphrase
11 | pub passphrase: String,
12 |
13 | /// Whether the LUKS passphrase will be enrolled in a TPM
14 | pub tpm: bool,
15 | }
16 |
17 | impl Prompt for Luks {
18 | fn prompt(&mut self, _: &Builder) -> Result<()> {
19 | if Confirm::with_theme(&crate::cli::cmd::init::theme())
20 | .with_prompt("Do you want to encrypt the root partition with LUKS?")
21 | .interact()?
22 | {
23 | self.passphrase = Password::with_theme(&crate::cli::cmd::init::theme())
24 | .with_prompt("LUKS passphrase")
25 | .interact()?;
26 | }
27 |
28 | self.validate()?;
29 | Ok(())
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod arch;
2 | pub mod hostname;
3 | pub mod iso;
4 | pub mod luks;
5 | pub mod size;
6 | pub mod timezone;
7 | pub mod unix_account;
8 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/size.rs:
--------------------------------------------------------------------------------
1 | use crate::{builder::Builder, cli::prompt::Prompt};
2 | use anyhow::Result;
3 | use byte_unit::Byte;
4 | use serde::{Deserialize, Serialize};
5 | use validator::Validate;
6 |
7 | /// Absolute size of an image element.
8 | #[derive(Clone, Serialize, Deserialize, Debug)]
9 | pub struct Size(String);
10 |
11 | impl Default for Size {
12 | fn default() -> Self {
13 | Self("16G".to_string())
14 | }
15 | }
16 |
17 | impl Prompt for Size {
18 | fn prompt(&mut self, builder: &Builder) -> Result<()> {
19 | todo!()
20 | }
21 | }
22 |
23 | impl Validate for Size {
24 | fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> {
25 | // Try to parse the size string using byte-unit
26 | match self.0.parse::() {
27 | Ok(byte) => {
28 | // Ensure the size is greater than zero
29 | if byte.as_u64() == 0 {
30 | let mut errors = validator::ValidationErrors::new();
31 | errors.add(
32 | "size",
33 | validator::ValidationError::new("Size must be greater than zero"),
34 | );
35 | return Err(errors);
36 | }
37 | Ok(())
38 | }
39 | Err(_) => {
40 | let mut errors = validator::ValidationErrors::new();
41 | errors.add(
42 | "size",
43 | validator::ValidationError::new("Invalid size format. Expected format: number followed by unit (e.g., '16G', '512M', '1T')"),
44 | );
45 | Err(errors)
46 | }
47 | }
48 | }
49 | }
50 |
51 | impl Into for Size {
52 | fn into(self) -> u64 {
53 | // Assume Size was validated previously
54 | self.0
55 | .parse::()
56 | .expect("Size was not validated")
57 | .as_u64()
58 | }
59 | }
60 |
61 | #[cfg(test)]
62 | mod tests {
63 | use super::*;
64 |
65 | #[test]
66 | fn test_valid_sizes() {
67 | // Test various valid size formats
68 | let sizes = vec![
69 | "16G", "16GB", "512M", "512MB", "1T", "1TB", "2048K", "2048KB", "1024", "1 GB",
70 | "1.5 GB", "100 MiB", "16GiB",
71 | ];
72 |
73 | for size_str in sizes {
74 | let size = Size(size_str.to_string());
75 | assert!(
76 | size.validate().is_ok(),
77 | "Expected '{}' to be valid",
78 | size_str
79 | );
80 | }
81 | }
82 |
83 | #[test]
84 | fn test_invalid_sizes() {
85 | // Test various invalid size formats
86 | let invalid_sizes = vec![
87 | "", // Empty string
88 | "abc", // No numbers
89 | "G16", // Unit before number
90 | "16X", // Invalid unit
91 | "hello world", // Completely invalid
92 | ];
93 |
94 | for size_str in invalid_sizes {
95 | let size = Size(size_str.to_string());
96 | assert!(
97 | size.validate().is_err(),
98 | "Expected '{}' to be invalid",
99 | size_str
100 | );
101 | }
102 | }
103 |
104 | #[test]
105 | fn test_zero_size() {
106 | // Test that zero size is rejected
107 | let size = Size("0".to_string());
108 | assert!(size.validate().is_err(), "Expected zero size to be invalid");
109 |
110 | let size = Size("0GB".to_string());
111 | assert!(size.validate().is_err(), "Expected zero size to be invalid");
112 | }
113 |
114 | #[test]
115 | fn test_default_size() {
116 | // Test that the default size is valid
117 | let size = Size::default();
118 | assert!(
119 | size.validate().is_ok(),
120 | "Expected default size '{}' to be valid",
121 | size.0
122 | );
123 | }
124 |
125 | #[test]
126 | fn test_large_sizes() {
127 | // Test large sizes
128 | let sizes = vec!["1000T", "1000TB", "1PB", "100000GB"];
129 |
130 | for size_str in sizes {
131 | let size = Size(size_str.to_string());
132 | assert!(
133 | size.validate().is_ok(),
134 | "Expected large size '{}' to be valid",
135 | size_str
136 | );
137 | }
138 | }
139 |
140 | #[test]
141 | fn test_bytes_only() {
142 | // Test sizes specified in bytes only
143 | let size = Size("1073741824".to_string()); // 1GB in bytes
144 | assert!(
145 | size.validate().is_ok(),
146 | "Expected bytes-only size to be valid"
147 | );
148 | }
149 |
150 | #[test]
151 | fn test_binary_vs_decimal_units() {
152 | // Test both binary (GiB) and decimal (GB) units
153 | let size_binary = Size("16GiB".to_string());
154 | assert!(
155 | size_binary.validate().is_ok(),
156 | "Expected binary unit GiB to be valid"
157 | );
158 |
159 | let size_decimal = Size("16GB".to_string());
160 | assert!(
161 | size_decimal.validate().is_ok(),
162 | "Expected decimal unit GB to be valid"
163 | );
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/timezone.rs:
--------------------------------------------------------------------------------
1 | use crate::{builder::Builder, cli::prompt::Prompt};
2 | use anyhow::Result;
3 | use serde::{Deserialize, Serialize};
4 | use validator::Validate;
5 |
6 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
7 | pub struct Timezone {
8 | // TODO
9 | }
10 |
11 | impl Prompt for Timezone {
12 | fn prompt(&mut self, _: &Builder) -> Result<()> {
13 | todo!()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/goldboot/src/builder/options/unix_account.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use crate::{builder::Builder, cli::prompt::Prompt};
4 | use anyhow::Result;
5 | use dialoguer::Password;
6 | use serde::{Deserialize, Serialize};
7 | use validator::Validate;
8 |
9 | // impl UnixAccountProvisioners {
10 | // /// Get the root user's password
11 | // pub fn get_root_password(&self) -> Option {
12 | // self.users
13 | // .iter()
14 | // .filter(|u| u.username == "root")
15 | // .map(|u| u.password)
16 | // .next()
17 | // }
18 | // }
19 |
20 | /// This provisioner configures a UNIX-like user account.
21 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
22 | pub struct UnixAccountProvisioner {
23 | #[validate(length(max = 64))]
24 | pub username: String,
25 |
26 | #[validate(length(max = 64))]
27 | pub password: String,
28 | }
29 |
30 | impl Prompt for UnixAccountProvisioner {
31 | fn prompt(&mut self, _: &Builder) -> Result<()> {
32 | let theme = crate::cli::cmd::init::theme();
33 | self.password = Password::with_theme(&theme)
34 | .with_prompt("Root password")
35 | .interact()?;
36 |
37 | self.validate()?;
38 | Ok(())
39 | }
40 | }
41 |
42 | impl Default for UnixAccountProvisioner {
43 | fn default() -> Self {
44 | Self {
45 | username: String::from("root"),
46 | password: crate::random_password(),
47 | }
48 | }
49 | }
50 |
51 | #[derive(Clone, Serialize, Deserialize, Debug)]
52 | #[serde(rename_all = "snake_case")]
53 | pub enum RootPassword {
54 | /// Simple plaintext password
55 | Plaintext(String),
56 |
57 | /// Take plaintext password from environment variable
58 | PlaintextEnv(String),
59 | }
60 |
61 | impl Default for RootPassword {
62 | fn default() -> Self {
63 | Self::Plaintext("root".to_string())
64 | }
65 | }
66 |
67 | impl Prompt for RootPassword {
68 | fn prompt(&mut self, _: &Builder) -> Result<()> {
69 | let theme = crate::cli::cmd::init::theme();
70 | *self = RootPassword::Plaintext(
71 | Password::with_theme(&theme)
72 | .with_prompt("Root password")
73 | .interact()?,
74 | );
75 | Ok(())
76 | }
77 | }
78 |
79 | impl Display for RootPassword {
80 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 | write!(
82 | f,
83 | "{}",
84 | match &self {
85 | RootPassword::Plaintext(password) => format!("plain:{password}"),
86 | RootPassword::PlaintextEnv(name) => format!(
87 | "plain:{}",
88 | std::env::var(name).expect("environment variable not found")
89 | ),
90 | }
91 | )
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/alpine_linux/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/alpine_linux/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/alpine_linux/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/alpine_linux/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/alpine_linux/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use goldboot_image::ImageArch;
3 | use serde::{Deserialize, Serialize};
4 | use smart_default::SmartDefault;
5 | use std::fmt::Display;
6 | use strum::{Display, EnumIter, IntoEnumIterator};
7 | use validator::Validate;
8 |
9 | use crate::{
10 | builder::{
11 | Builder,
12 | options::{hostname::Hostname, iso::Iso, size::Size, unix_account::RootPassword},
13 | qemu::{OsCategory, QemuBuilder},
14 | },
15 | cli::prompt::Prompt,
16 | enter, wait, wait_screen_rect,
17 | };
18 |
19 | use super::BuildImage;
20 |
21 | /// Produces [Alpine Linux](https://www.alpinelinux.org) images.
22 | #[derive(Clone, Serialize, Deserialize, Validate, Debug, SmartDefault, goldboot_macros::Prompt)]
23 | pub struct AlpineLinux {
24 | pub size: Size,
25 | pub edition: AlpineEdition,
26 | #[serde(flatten)]
27 | pub hostname: Hostname,
28 | pub release: AlpineRelease,
29 | pub root_password: RootPassword,
30 | #[default(Iso {
31 | url: "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-standard-3.19.1-x86_64.iso".parse().unwrap(),
32 | checksum: Some("sha256:12addd7d4154df1caf5f258b80ad72e7a724d33e75e6c2e6adc1475298d47155".to_string()),
33 | })]
34 | pub iso: Iso,
35 | }
36 |
37 | impl BuildImage for AlpineLinux {
38 | fn build(&self, worker: &Builder) -> Result<()> {
39 | let mut qemu = QemuBuilder::new(&worker, OsCategory::Linux)
40 | .with_iso(&self.iso)?
41 | .prepare_ssh()?
42 | .start()?;
43 |
44 | // Send boot command
45 | #[rustfmt::skip]
46 | qemu.vnc.run(vec![
47 | // Initial wait
48 | wait!(30),
49 | // Root login
50 | enter!("root"),
51 | // Configure install
52 | enter!("export KEYMAPOPTS='us us'"),
53 | enter!(format!("export HOSTNAMEOPTS='-n {}'", self.hostname.hostname)),
54 | enter!("export INTERFACESOPTS='
55 | auto lo
56 | iface lo inet loopback
57 |
58 | auto eth0
59 | iface eth0 inet dhcp
60 | hostname alpine-test'"
61 | ),
62 | enter!("export DNSOPTS='1.1.1.1'"),
63 | enter!("export TIMEZONEOPTS='-z UTC'"),
64 | enter!("export PROXYOPTS='none'"),
65 | enter!("export APKREPOSOPTS='-r'"),
66 | enter!("export SSHDOPTS='-c openssh'"),
67 | enter!("export NTPOPTS='-c openntpd'"),
68 | enter!("export DISKOPTS='-m sys /dev/vda'"),
69 | // Start install
70 | enter!("echo -e 'root\nroot\ny' | setup-alpine"),
71 | wait_screen_rect!("6d7b9fc9229c4f4ae8bc84f0925d8479ccd3e7d2", 668, 0, 1024, 100),
72 | // Remount root partition
73 | enter!("mount -t ext4 /dev/vda3 /mnt"),
74 | // Reboot into installation
75 | enter!("apk add efibootmgr; efibootmgr -n 0003; reboot"),
76 | ])?;
77 |
78 | // Wait for SSH
79 | let ssh = qemu.ssh("root")?;
80 |
81 | // Run provisioners
82 | // TODO
83 |
84 | // Shutdown
85 | ssh.shutdown("poweroff")?;
86 | qemu.shutdown_wait()?;
87 | Ok(())
88 | }
89 | }
90 |
91 | #[derive(Clone, Copy, Serialize, Deserialize, Debug, EnumIter, Display, Default)]
92 | pub enum AlpineEdition {
93 | #[default]
94 | Standard,
95 | Extended,
96 | RaspberryPi,
97 | Xen,
98 | }
99 |
100 | impl Prompt for AlpineEdition {
101 | fn prompt(&mut self, _: &Builder) -> Result<()> {
102 | let theme = crate::cli::cmd::init::theme();
103 | let editions: Vec = AlpineEdition::iter().collect();
104 | let edition_index = dialoguer::Select::with_theme(&theme)
105 | .with_prompt("Choose an edition")
106 | .default(0)
107 | .items(&editions)
108 | .interact()?;
109 |
110 | *self = editions[edition_index];
111 | Ok(())
112 | }
113 | }
114 |
115 | #[derive(Clone, Copy, Serialize, Deserialize, Debug, EnumIter, Default)]
116 | pub enum AlpineRelease {
117 | #[default]
118 | Edge,
119 | #[serde(rename = "v3.19")]
120 | V3_19,
121 | #[serde(rename = "v3.18")]
122 | V3_18,
123 | #[serde(rename = "v3.17")]
124 | V3_17,
125 | #[serde(rename = "v3.16")]
126 | V3_16,
127 | #[serde(rename = "v3.15")]
128 | V3_15,
129 | }
130 |
131 | impl Prompt for AlpineRelease {
132 | fn prompt(&mut self, _: &Builder) -> Result<()> {
133 | Ok(())
134 | }
135 | }
136 |
137 | impl Display for AlpineRelease {
138 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 | write!(
140 | f,
141 | "{}",
142 | match &self {
143 | AlpineRelease::Edge => "Edge",
144 | AlpineRelease::V3_19 => "v3.19",
145 | AlpineRelease::V3_18 => "v3.18",
146 | AlpineRelease::V3_17 => "v3.17",
147 | AlpineRelease::V3_16 => "v3.16",
148 | AlpineRelease::V3_15 => "v3.15",
149 | }
150 | )
151 | }
152 | }
153 |
154 | // fn fetch_latest_iso(
155 | // edition: AlpineEdition,
156 | // release: AlpineRelease,
157 | // arch: Architecture,
158 | // ) -> Result {
159 | // let arch = match arch {
160 | // Architecture::amd64 => "x86_64",
161 | // Architecture::arm64 => "aarch64",
162 | // _ => bail!("Unsupported architecture"),
163 | // };
164 |
165 | // let edition = match edition {
166 | // AlpineEdition::Standard => "standard",
167 | // AlpineEdition::Extended => "extended",
168 | // AlpineEdition::Xen => "virt",
169 | // AlpineEdition::RaspberryPi => "rpi",
170 | // };
171 |
172 | // let url = format!("https://dl-cdn.alpinelinux.org/alpine/v3.16/releases/{arch}/alpine-{edition}-3.16.0-{arch}.iso");
173 |
174 | // // Download checksum
175 | // let rs = reqwest::blocking::get(format!("{url}.sha256"))?;
176 | // let checksum = if rs.status().is_success() { None } else { None };
177 |
178 | // Ok(IsoSource { url, checksum })
179 | // }
180 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/android/mod.rs:
--------------------------------------------------------------------------------
1 | use super::{CastImage, DefaultSource};
2 | use crate::builder::Foundry;
3 | use crate::builder::options::hostname::Hostname;
4 | use crate::builder::options::unix_account::RootPassword;
5 | use crate::builder::qemu::QemuBuilder;
6 | use crate::cli::prompt::Prompt;
7 | use crate::wait;
8 | use crate::{
9 | builder::{Builder, sources::ImageSource},
10 | wait_screen_rect,
11 | };
12 | use anyhow::Result;
13 | use anyhow::bail;
14 | use dialoguer::theme::Theme;
15 | use serde::{Deserialize, Serialize};
16 | use std::io::{BufRead, BufReader};
17 | use tracing::{debug, info};
18 | use validator::Validate;
19 |
20 | /// Produces an AOSP image.
21 | #[derive(Clone, Serialize, Deserialize, Validate, Debug, goldboot_macros::Prompt)]
22 | pub struct Android {}
23 |
24 | impl Default for Android {
25 | fn default() -> Self {
26 | Self {}
27 | }
28 | }
29 |
30 | impl DefaultSource for Android {
31 | fn default_source(&self, arch: ImageArch) -> Result {
32 | todo!()
33 | }
34 | }
35 |
36 | impl CastImage for Android {
37 | fn cast(&self, worker: &Builder) -> Result<()> {
38 | let mut qemu = QemuBuilder::new(&worker).start()?;
39 |
40 | // Send boot command
41 | #[rustfmt::skip]
42 | qemu.vnc.run(vec![
43 | // Initial wait
44 | wait!(30),
45 | // Wait for login
46 | wait_screen_rect!("5b3ca88689e9d671903b3040889c7fa1cb5f244a", 100, 0, 1024, 400),
47 | // Configure root password
48 | // enter!("passwd"), enter!(self.root_password), enter!(self.root_password),
49 | ])?;
50 |
51 | // Wait for SSH
52 | let mut ssh = qemu.ssh()?;
53 |
54 | // Run install script
55 | info!("Running base installation");
56 | match ssh.upload_exec(
57 | include_bytes!("install.sh"),
58 | vec![
59 | // ("GB_MIRRORLIST", &self.format_mirrorlist()),
60 | // ("GB_ROOT_PASSWORD", &self.root_password),
61 | ],
62 | ) {
63 | Ok(0) => debug!("Installation completed successfully"),
64 | _ => bail!("Installation failed"),
65 | }
66 |
67 | // Run provisioners
68 | // self.provisioners.run(&mut ssh)?;
69 |
70 | // Shutdown
71 | ssh.shutdown("poweroff")?;
72 | qemu.shutdown_wait()?;
73 | Ok(())
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/arch_linux/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 | set -x
5 |
6 | exec 1>&2
7 |
8 | # Don't block forever if we don't have enough entropy
9 | ln -sf /dev/urandom /dev/random
10 |
11 | # Synchronize time
12 | timedatectl set-ntp true
13 |
14 | # Wait for time sync
15 | while [ $(timedatectl show --property=NTPSynchronized --value) != "yes" ]; do
16 | sleep 5
17 | done
18 |
19 | # Wait for pacman-init to complete
20 | while ! systemctl show pacman-init.service | grep SubState=exited; do
21 | sleep 5
22 | done
23 |
24 | # Use a wrapper to run the install
25 | pacman -Sy --noconfirm --noprogressbar archinstall curl
26 | archinstall --debug --config <(curl "http://${GB_HTTP_HOST:?}:${GB_HTTP_PORT:?}/config.json") --creds <(curl "http://${GB_HTTP_HOST:?}:${GB_HTTP_PORT:?}/creds.json")
27 |
28 | exit
29 |
30 | # Create partitions
31 | parted --script -a optimal -- /dev/vda \
32 | mklabel gpt \
33 | mkpart primary 1MiB 256MiB \
34 | set 1 esp on \
35 | mkpart primary 256MiB 100%
36 |
37 | # Format boot partition
38 | mkfs.vfat /dev/vda1
39 |
40 | # Bootstrap filesystem
41 | pacstrap -K -M /mnt base linux linux-firmware efibootmgr grub dhcpcd ${GB_PACKAGES}
42 |
43 | if [ -e /dev/mapper/root ]; then
44 | cat <<-EOF >>/mnt/etc/default/grub
45 | GRUB_CMDLINE_LINUX="cryptdevice=UUID=$(blkid -s UUID -o value /dev/vda2):root root=/dev/mapper/root"
46 | EOF
47 |
48 | # Update initramfs
49 | echo 'HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt filesystems fsck)' >/mnt/etc/mkinitcpio.conf
50 | arch-chroot /mnt mkinitcpio -P
51 | else
52 | cat <<-EOF >>/mnt/etc/default/grub
53 | GRUB_CMDLINE_LINUX="root=UUID=$(blkid -s UUID -o value /dev/vda2)"
54 | EOF
55 | fi
56 |
57 | # Install bootloader
58 | arch-chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
59 | arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg
60 |
61 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/arch_linux/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/arch_linux/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/arch_linux/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/arch_linux/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/buildroot/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | build::BuildWorker,
3 | cache::{MediaCache, MediaFormat},
4 | provisioners::*,
5 | qemu::QemuArgs,
6 | templates::*,
7 | };
8 | use tracing::info;
9 | use serde::{Deserialize, Serialize};
10 | use anyhow::bail;
11 | use std::{
12 | error::Error,
13 | io::{BufRead, BufReader},
14 | };
15 | use validator::Validate;
16 |
17 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
18 | pub struct BuildrootTemplate {
19 | pub source: BuildrootSource,
20 | pub provisioners: Option>,
21 | }
22 |
23 | pub enum ArchSource {
24 | Iso(IsoSource),
25 | }
26 |
27 | #[derive(Clone, Serialize, Deserialize, Debug)]
28 | #[serde(tag = "type", rename_all = "snake_case")]
29 | pub enum ArchProvisioner {
30 | Ansible(AnsibleProvisioner)
31 | Mirrorlist(ArchMirrorlistProvisioner),
32 | Hostname(HostnameProvisioner),
33 | }
34 |
35 | impl Default for ArchTemplate {
36 | fn default() -> Self {
37 | Self {
38 | source: None,
39 | provisioners: None,
40 | }
41 | }
42 | }
43 |
44 | impl BuildTemplate for ArchTemplate {
45 | fn build(&self, context: &BuildWorker) -> Result<()> {
46 | info!("Starting {} build", console::style("ArchLinux").blue());
47 |
48 | let mut qemuargs = QemuArgs::new(&context);
49 |
50 | qemuargs.drive.push(format!(
51 | "file={},if=virtio,cache=writeback,discard=ignore,format=qcow2",
52 | context.image_path
53 | ));
54 | qemuargs.drive.push(format!(
55 | "file={},media=cdrom",
56 | MediaCache::get(self.iso.url.clone(), &self.iso.checksum, MediaFormat::Iso)?
57 | ));
58 |
59 | // Start VM
60 | let mut qemu = qemuargs.start_process()?;
61 |
62 | // Send boot command
63 | #[rustfmt::skip]
64 | qemu.vnc.boot_command(vec![
65 | // Initial wait
66 | wait!(30),
67 | // Wait for login
68 | wait_screen_rect!("5b3ca88689e9d671903b3040889c7fa1cb5f244a", 100, 0, 1024, 400),
69 | // Configure root password
70 | enter!("passwd"), enter!(self.root_password), enter!(self.root_password),
71 | // Configure SSH
72 | enter!("echo 'AcceptEnv *' >>/etc/ssh/sshd_config"),
73 | enter!("echo 'PermitRootLogin yes' >>/etc/ssh/sshd_config"),
74 | // Start sshd
75 | enter!("systemctl restart sshd"),
76 | ])?;
77 |
78 | // Wait for SSH
79 | let mut ssh = qemu.ssh_wait(context.ssh_port, "root", &self.root_password)?;
80 |
81 | // Run install script
82 | if let Some(resource) = Resources::get("install.sh") {
83 | info!("Running base installation");
84 | match ssh.upload_exec(
85 | resource.data.to_vec(),
86 | vec![
87 | ("GB_MIRRORLIST", &self.format_mirrorlist()),
88 | ("GB_ROOT_PASSWORD", &self.root_password),
89 | ],
90 | ) {
91 | Ok(0) => debug!("Installation completed successfully"),
92 | _ => bail!("Installation failed"),
93 | }
94 | }
95 |
96 | // Run provisioners
97 | self.provisioners.run(&mut ssh)?;
98 |
99 | // Shutdown
100 | ssh.shutdown("poweroff")?;
101 | qemu.shutdown_wait()?;
102 | Ok(())
103 | }
104 | }
105 |
106 | /// This provisioner configures the Archlinux mirror list.
107 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
108 | pub struct ArchMirrorlistProvisioner {
109 | pub mirrors: Vec,
110 | }
111 |
112 | impl Default for ArchMirrorlistProvisioner {
113 | fn default() -> Self {
114 | Self {
115 | mirrors: vec![
116 | String::from("https://geo.mirror.pkgbuild.com/"),
117 | String::from("https://mirror.rackspace.com/archlinux/"),
118 | String::from("https://mirrors.edge.kernel.org/archlinux/"),
119 | ],
120 | }
121 | }
122 | }
123 |
124 | impl ArchMirrorlistProvisioner {
125 | pub fn format_mirrorlist(&self) -> String {
126 | self.mirrors
127 | .iter()
128 | .map(|s| format!("Server = {}", s))
129 | .collect::>()
130 | .join("\n")
131 | }
132 | }
133 |
134 | /// Fetch the latest installation ISO
135 | fn fetch_latest_iso(mirrorlist: ArchMirrorlistProvisioner) -> Result {
136 | for mirror in mirrorlist.mirrors {
137 | let rs = reqwest::blocking::get(format!("{mirror}/iso/latest/sha1sums.txt"))?;
138 | if rs.status().is_success() {
139 | for line in BufReader::new(rs).lines().filter_map(|result| result.ok()) {
140 | if line.ends_with(".iso") {
141 | let split: Vec<&str> = line.split_whitespace().collect();
142 | if let [hash, filename] = split[..] {
143 | return Ok(IsoSource {
144 | url: format!("{mirror}/iso/latest/{filename}"),
145 | checksum: format!("sha1:{hash}"),
146 | });
147 | }
148 | }
149 | }
150 | }
151 | }
152 | bail!("Failed to request latest ISO");
153 | }
154 |
155 | #[cfg(test)]
156 | mod tests {
157 | use super::*;
158 |
159 | #[test]
160 | fn test_fetch_latest_iso() -> Result<()> {
161 | fetch_latest_iso(ArchMirrorlistProvisioner::default())?;
162 | Ok(())
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/debian/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/debian/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/debian/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/debian/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/debian/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Result, bail};
2 | use goldboot_image::ImageArch;
3 | use serde::{Deserialize, Serialize};
4 | use smart_default::SmartDefault;
5 | use std::io::{BufRead, BufReader};
6 | use strum::{Display, EnumIter, IntoEnumIterator};
7 | use validator::Validate;
8 |
9 | use crate::{
10 | builder::{
11 | Builder,
12 | http::HttpServer,
13 | options::{
14 | arch::Arch, hostname::Hostname, iso::Iso, size::Size, unix_account::RootPassword,
15 | },
16 | qemu::{OsCategory, QemuBuilder},
17 | },
18 | cli::prompt::Prompt,
19 | enter, input, wait_screen, wait_screen_rect,
20 | };
21 |
22 | use super::BuildImage;
23 |
24 | /// Debian is a Linux distribution composed of free and open-source software and
25 | /// optionally non-free firmware or software developed by the community-supported
26 | /// Debian Project.
27 | ///
28 | /// Upstream: https://www.debian.org
29 | /// Maintainer: cilki
30 | #[derive(Clone, Serialize, Deserialize, Validate, Debug, SmartDefault, goldboot_macros::Prompt)]
31 | pub struct Debian {
32 | #[default(Arch(ImageArch::Amd64))]
33 | pub arch: Arch,
34 | pub size: Size,
35 | pub edition: DebianEdition,
36 | #[serde(flatten)]
37 | pub hostname: Option,
38 | pub root_password: RootPassword,
39 | #[default(Iso {
40 | url: "http://example.com".parse().unwrap(),
41 | checksum: None,
42 | })]
43 | pub iso: Iso,
44 | }
45 |
46 | impl BuildImage for Debian {
47 | fn build(&self, worker: &Builder) -> Result<()> {
48 | let mut qemu = QemuBuilder::new(&worker, OsCategory::Linux)
49 | .vga("cirrus")
50 | .with_iso(&self.iso)?
51 | .prepare_ssh()?
52 | .start()?;
53 |
54 | // Start HTTP
55 | let http = HttpServer::new()?
56 | .file("preseed.cfg", include_bytes!("preseed.cfg"))?
57 | .serve();
58 |
59 | // Send boot command
60 | #[rustfmt::skip]
61 | qemu.vnc.run(vec![
62 | // Wait for boot
63 | wait_screen_rect!("f6852e8b6e072d15270b2b215bbada3da30fd733", 100, 100, 400, 400),
64 | // Trigger unattended install
65 | input!("aa"),
66 | // Wait for preseed URL to be prompted
67 | match self.edition {
68 | DebianEdition::Bullseye => todo!(),
69 | DebianEdition::Bookworm => wait_screen!("6ee7873098bceb5a2124db82dae6abdae214ce7e"),
70 | DebianEdition::Trixie => todo!(),
71 | DebianEdition::Sid => todo!(),
72 | },
73 | enter!(format!("http://{}:{}/preseed.cfg", http.address, http.port)),
74 | // Wait for login prompt
75 | match self.edition {
76 | DebianEdition::Bullseye => todo!(),
77 | DebianEdition::Bookworm => wait_screen!("2eb1ef517849c86a322ba60bb05386decbf00ba5"),
78 | DebianEdition::Trixie => todo!(),
79 | DebianEdition::Sid => todo!(),
80 | },
81 | // Login as root
82 | enter!("root"),
83 | enter!("r00tme"),
84 | ])?;
85 |
86 | // Wait for SSH
87 | let ssh = qemu.ssh("root")?;
88 |
89 | // Shutdown
90 | ssh.shutdown("poweroff")?;
91 | qemu.shutdown_wait()?;
92 | Ok(())
93 | }
94 | }
95 |
96 | #[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, EnumIter, Display)]
97 | pub enum DebianEdition {
98 | Bullseye,
99 | #[default]
100 | Bookworm,
101 | Trixie,
102 | Sid,
103 | }
104 |
105 | impl Prompt for DebianEdition {
106 | fn prompt(&mut self, builder: &Builder) -> Result<()> {
107 | let editions: Vec = DebianEdition::iter().collect();
108 | let edition_index = dialoguer::Select::with_theme(&crate::cli::cmd::init::theme())
109 | .with_prompt("Choose Debian edition")
110 | .default(0)
111 | .items(editions.iter())
112 | .interact()?;
113 |
114 | *self = editions[edition_index];
115 | Ok(())
116 | }
117 | }
118 |
119 | /// Fetch the latest ISO
120 | pub fn fetch_debian_iso(edition: DebianEdition, arch: ImageArch) -> Result {
121 | let arch = match arch {
122 | ImageArch::Amd64 => "amd64",
123 | ImageArch::Arm64 => "arm64",
124 | ImageArch::I386 => "i386",
125 | _ => bail!("Unsupported architecture"),
126 | };
127 | let version = match edition {
128 | DebianEdition::Bullseye => "archive/11.9.0",
129 | DebianEdition::Bookworm => "release/12.5.0",
130 | _ => bail!("Unsupported edition"),
131 | };
132 |
133 | let rs = reqwest::blocking::get(format!(
134 | "https://cdimage.debian.org/cdimage/{version}/{arch}/iso-cd/SHA256SUMS"
135 | ))?;
136 | if rs.status().is_success() {
137 | for line in BufReader::new(rs).lines().filter_map(|result| result.ok()) {
138 | if line.ends_with(".iso") {
139 | let split: Vec<&str> = line.split_whitespace().collect();
140 | if let [hash, filename] = split[..] {
141 | return Ok(Iso {
142 | url: format!(
143 | "https://cdimage.debian.org/cdimage/{version}/{arch}/iso-cd/{filename}"
144 | )
145 | .parse()
146 | .unwrap(),
147 | checksum: Some(format!("sha256:{hash}")),
148 | });
149 | }
150 | }
151 | }
152 | }
153 | bail!("Failed to request latest ISO");
154 | }
155 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/fedora/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/fedora/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/fedora/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/fedora/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/linux_mint/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/linux_mint/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/linux_mint/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/linux_mint/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/mac_os/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/mac_os/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/mac_os/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/mac_os/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/mac_os/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{build::BuildWorker, cache::MediaCache, provisioners::*, qemu::QemuArgs, templates::*};
2 | use serde::{Deserialize, Serialize};
3 | use std::error::Error;
4 | use validator::Validate;
5 |
6 | //#[derive(rust_embed::RustEmbed)]
7 | //#[folder = "res/MacOs/"]
8 | //struct Resources;
9 |
10 | #[derive(Clone, Serialize, Deserialize, Debug)]
11 | pub enum MacOsRelease {
12 | Catalina,
13 | BigSur,
14 | Monterey,
15 | }
16 |
17 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
18 | pub struct MacOsTemplate {
19 | pub id: TemplateId,
20 | pub release: MacOsRelease,
21 |
22 | pub iso: IsoSource,
23 | pub ansible: Option>,
24 | }
25 |
26 | impl Default for MacOsTemplate {
27 | fn default() -> Self {
28 | Self {
29 | id: TemplateId::MacOs,
30 | release: MacOsRelease::Monterey,
31 | provisioners: ProvisionersContainer {
32 | provisioners: Some(vec![
33 | serde_json::to_value(ShellProvisioner::inline("/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")).unwrap(),
34 | ])
35 | },
36 | version: MacOsVersion::Monterey,
37 | general: GeneralContainer{
38 | base: TemplateBase::MacOs,
39 | storage_size: String::from("50 GiB"),
40 | .. Default::default()
41 | },
42 | }
43 | }
44 | }
45 |
46 | impl Template for MacOsTemplate {
47 | fn build(&self, context: &BuildWorker) -> Result<()> {
48 | let mut qemuargs = QemuArgs::new(&context);
49 |
50 | // Copy OpenCore partition
51 | //if let Some(resource) = Resources::get("OpenCore.qcow2") {
52 | // std::fs::write(context.tmp.path().join("OpenCore.qcow2"), resource.data)?;
53 | //}
54 |
55 | // Convert dmg to img
56 | //qemu-img convert BaseSystem.dmg -O raw BaseSystem.img
57 |
58 | qemuargs.cpu = Some(format!("Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check"));
59 | qemuargs.machine = format!("q35,accel=kvm");
60 | qemuargs.smbios = Some(format!("type=2"));
61 | qemuargs.device.push(format!("ich9-ahci,id=sata"));
62 | qemuargs.device.push(format!("usb-ehci,id=ehci"));
63 | qemuargs.device.push(format!("nec-usb-xhci,id=xhci"));
64 | qemuargs.device.push(format!(
65 | "isa-applesmc,osk=ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"
66 | ));
67 | qemuargs.usbdevice.push(format!("keyboard"));
68 | qemuargs.usbdevice.push(format!("tablet"));
69 | qemuargs.global.push(format!("nec-usb-xhci.msi=off"));
70 |
71 | // Add boot partition
72 | qemuargs.drive.push(format!(
73 | "file={}/OpenCore.qcow2,id=OpenCore,if=none,format=qcow2",
74 | context.tmp.path().display()
75 | ));
76 | qemuargs
77 | .device
78 | .push(format!("ide-hd,bus=sata.2,drive=OpenCore"));
79 |
80 | // Add install media
81 | qemuargs.drive.push(format!(
82 | "file=/home/cilki/OSX-KVM/BaseSystem.img,id=InstallMedia,if=none,format=raw"
83 | ));
84 | qemuargs
85 | .device
86 | .push(format!("ide-hd,bus=sata.3,drive=InstallMedia"));
87 |
88 | // Add system drive
89 | qemuargs.drive.push(format!(
90 | "file={},id=System,if=none,cache=writeback,discard=ignore,format=qcow2",
91 | context.image_path,
92 | ));
93 | qemuargs
94 | .device
95 | .push(format!("ide-hd,bus=sata.4,drive=System"));
96 |
97 | // Start VM
98 | let mut qemu = qemuargs.start_process()?;
99 |
100 | // Send boot command
101 | match self.release {
102 | MacOsRelease::Monterey => {
103 | #[rustfmt::skip]
104 | qemu.vnc.boot_command(vec![
105 | enter!(),
106 | enter!("diskutil eraseDisk APFS System disk0"),
107 | // Wait for "Select your region" screen
108 | wait_screen_rect!("fa1aeec4a3d4436d9bdd99345b29256ce4d141c8", 50, 0, 1024, 700),
109 | // Configure region
110 | enter!("United States"), tab!(), tab!(), enter!(),
111 | // ...
112 | // Configure ssh
113 | enter!("echo 'PermitRootLogin yes' >>/etc/ssh/sshd_config"),
114 | // Start sshd
115 | enter!("launchctl load -w /System/Library/LaunchDaemons/ssh.plist"),
116 | ])?;
117 | }
118 | _ => {}
119 | }
120 |
121 | // Wait for SSH
122 | let mut ssh = qemu.ssh_wait(context.ssh_port, "root", "root")?;
123 |
124 | // Run provisioners
125 | self.provisioners.run(&mut ssh)?;
126 |
127 | // Shutdown
128 | ssh.shutdown("shutdown -h now")?;
129 | qemu.shutdown_wait()?;
130 | Ok(())
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::builder::Builder;
2 | use crate::cli::prompt::Prompt;
3 | use anyhow::Result;
4 | use clap::ValueEnum;
5 | use enum_dispatch::enum_dispatch;
6 | use goldboot_image::ImageArch;
7 | #[cfg(feature = "config-python")]
8 | use pyo3::prelude::*;
9 | use serde::{Deserialize, Serialize};
10 | use std::{fmt::Display, sync::OnceLock};
11 | use strum::{EnumIter, IntoEnumIterator};
12 |
13 | use alpine_linux::AlpineLinux;
14 | use arch_linux::ArchLinux;
15 | use debian::Debian;
16 | // use goldboot::Goldboot;
17 | use nix::Nix;
18 | use windows_10::Windows10;
19 | use windows_11::Windows11;
20 |
21 | pub mod alpine_linux;
22 | pub mod arch_linux;
23 | pub mod debian;
24 | // pub mod goldboot;
25 | pub mod nix;
26 | pub mod windows_10;
27 | pub mod windows_11;
28 |
29 | #[macro_export]
30 | macro_rules! size {
31 | ($os:expr) => {
32 | match $os.clone() {
33 | Os::AlpineLinux(inner) => inner.size,
34 | Os::ArchLinux(inner) => inner.size,
35 | Os::Debian(inner) => inner.size,
36 | Os::Nix(inner) => inner.size,
37 | Os::Windows10(inner) => inner.size,
38 | Os::Windows11(inner) => inner.size,
39 | }
40 | };
41 | }
42 |
43 | /// "Building" is the process of generating an immutable goldboot image from raw
44 | /// configuration data.
45 | #[enum_dispatch(Os)]
46 | pub trait BuildImage {
47 | /// Build an image.
48 | fn build(&self, builder: &Builder) -> Result<()>;
49 | }
50 |
51 | // TODO Element?
52 |
53 | /// Represents a "base configuration" that users can modify and use to build
54 | /// images.
55 | #[enum_dispatch]
56 | #[derive(Clone, Serialize, Deserialize, Debug, EnumIter)]
57 | #[serde(tag = "os")]
58 | pub enum Os {
59 | AlpineLinux,
60 | ArchLinux,
61 | // Artix,
62 | // BedrockLinux,
63 | // CentOs,
64 | Debian,
65 | // ElementaryOs,
66 | // Fedora,
67 | // FreeBsd,
68 | // Gentoo,
69 | // Goldboot,
70 | // Haiku,
71 | // Kali,
72 | // LinuxMint,
73 | // MacOs,
74 | // Manjaro,
75 | // NetBsd,
76 | Nix,
77 | // OpenBsd,
78 | // OpenSuse,
79 | // Oracle,
80 | // Parrot,
81 | // PopOs,
82 | // Qubes,
83 | // RedHat,
84 | // RockyLinux,
85 | // Slackware,
86 | // SteamDeck,
87 | // SteamOs,
88 | // Tails,
89 | // TrueNas,
90 | // Ubuntu,
91 | // VoidLinux,
92 | Windows10,
93 | Windows11,
94 | // Windows7,
95 | // Zorin,
96 | }
97 |
98 | impl Os {
99 | /// Supported system architectures
100 | pub fn architectures(&self) -> Vec {
101 | match self {
102 | Os::AlpineLinux(_) => vec![ImageArch::Amd64, ImageArch::Arm64],
103 | Os::ArchLinux(_) => vec![ImageArch::Amd64],
104 | Os::Debian(_) => vec![ImageArch::Amd64, ImageArch::Arm64],
105 | // Os::Goldboot(_) => vec![ImageArch::Amd64, ImageArch::Arm64],
106 | Os::Nix(_) => vec![ImageArch::Amd64, ImageArch::Arm64],
107 | Os::Windows10(_) => vec![ImageArch::Amd64],
108 | Os::Windows11(_) => vec![ImageArch::Amd64],
109 | }
110 | }
111 |
112 | /// Whether the template can be combined with others in the same image
113 | pub fn alloy(&self) -> bool {
114 | false
115 | }
116 |
117 | // pub fn default_source
118 | }
119 |
120 | impl Display for Os {
121 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 | write!(
123 | f,
124 | "{}",
125 | match self {
126 | Os::AlpineLinux(_) => "AlpineLinux",
127 | Os::ArchLinux(_) => "ArchLinux",
128 | Os::Debian(_) => "Debian",
129 | // Os::Goldboot(_) => "Goldboot",
130 | Os::Nix(_) => "NixOS",
131 | Os::Windows10(_) => "Windows10",
132 | Os::Windows11(_) => "Windows11",
133 | }
134 | )
135 | }
136 | }
137 |
138 | impl Default for Os {
139 | fn default() -> Self {
140 | Os::ArchLinux(ArchLinux::default())
141 | }
142 | }
143 |
144 | #[cfg(feature = "config-python")]
145 | impl<'py> FromPyObject<'py> for Os {
146 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult {
147 | pythonize::depythonize(ob)
148 | .map_err(|e| pyo3::exceptions::PyTypeError::new_err(e.to_string()))
149 | }
150 | }
151 |
152 | static VARIANTS: OnceLock> = OnceLock::new();
153 |
154 | impl ValueEnum for Os {
155 | fn value_variants<'a>() -> &'a [Self] {
156 | VARIANTS.get_or_init(|| Os::iter().collect())
157 | }
158 |
159 | fn to_possible_value(&self) -> Option {
160 | Some(clap::builder::PossibleValue::new(
161 | Into::::into(self.to_string()),
162 | ))
163 | }
164 | }
165 |
166 | // impl From for ElementHeader {
167 | // fn from(value: Element) -> ElementHeader {
168 | // todo!()
169 | // }
170 | // }
171 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/nix/mod.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use goldboot_image::ImageArch;
3 | use serde::{Deserialize, Serialize};
4 | use smart_default::SmartDefault;
5 | use std::{collections::HashMap, path::PathBuf};
6 | use validator::Validate;
7 |
8 | use crate::{
9 | builder::{
10 | Builder,
11 | options::{arch::Arch, iso::Iso, size::Size},
12 | qemu::{OsCategory, QemuBuilder},
13 | },
14 | cli::prompt::Prompt,
15 | enter, wait, wait_screen_rect,
16 | };
17 |
18 | use super::BuildImage;
19 |
20 | /// NixOS is a free and open source Linux distribution based on the Nix package
21 | /// manager. NixOS uses an immutable design and an atomic update model. Its use
22 | /// of a declarative configuration system allows reproducibility and
23 | /// portability.
24 | ///
25 | /// Upstream: https://www.nixos.org
26 | /// Maintainer: cilki
27 | #[derive(Clone, Serialize, Deserialize, Validate, Debug, SmartDefault, goldboot_macros::Prompt)]
28 | pub struct Nix {
29 | #[default(Arch(ImageArch::Amd64))]
30 | pub arch: Arch,
31 | pub size: Size,
32 |
33 | /// Path to /etc/nixos/configuration.nix
34 | #[default(ConfigurationPath("configuration.nix".parse().unwrap()))]
35 | pub configuration: ConfigurationPath,
36 |
37 | /// Path to /etc/nixos/hardware-configuration.nix
38 | pub hardware_configuration: Option,
39 |
40 | #[default(Iso {
41 | url: "http://example.com".parse().unwrap(),
42 | checksum: None,
43 | })]
44 | pub iso: Iso,
45 | }
46 |
47 | impl BuildImage for Nix {
48 | fn build(&self, worker: &Builder) -> Result<()> {
49 | let mut qemu = QemuBuilder::new(&worker, OsCategory::Linux)
50 | .with_iso(&self.iso)?
51 | // Add Nix config
52 | .drive_files(HashMap::from([(
53 | "configuration.nix".to_string(),
54 | self.configuration.load()?,
55 | )]))?
56 | .start()?;
57 |
58 | // Send boot command
59 | #[rustfmt::skip]
60 | qemu.vnc.run(vec![
61 | // Initial wait
62 | wait!(30),
63 | // Wait for automatic login
64 | wait_screen_rect!("94a2520c082650cc01a4b5eac8719b697a4bbf63", 100, 100, 100, 100),
65 | enter!("sudo su -"),
66 | // Mount config partition and copy configuration.nix
67 | enter!("mkdir /goldboot"),
68 | enter!("mount /dev/vdb /goldboot"),
69 | enter!("cp /goldboot/configuration.nix /mnt/etc/nixos/configuration.nix"),
70 | enter!("umount /goldboot"),
71 | // Run install
72 | enter!("nixos-install"),
73 | ])?;
74 |
75 | // Shutdown
76 | qemu.shutdown_wait()?;
77 | Ok(())
78 | }
79 | }
80 |
81 | #[derive(Clone, Serialize, Deserialize, Debug)]
82 | struct ConfigurationPath(PathBuf);
83 |
84 | impl ConfigurationPath {
85 | fn load(&self) -> Result> {
86 | if self.0.starts_with("http") {
87 | todo!()
88 | }
89 |
90 | let bytes = std::fs::read(&self.0)?;
91 | Ok(bytes)
92 | }
93 | }
94 |
95 | impl Prompt for ConfigurationPath {
96 | fn prompt(&mut self, _: &Builder) -> Result<()> {
97 | todo!()
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/open_suse/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/open_suse/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/open_suse/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/open_suse/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/pop_os/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | build::BuildWorker,
3 | cache::{MediaCache, MediaFormat},
4 | provisioners::*,
5 | qemu::QemuArgs,
6 | templates::*,
7 | };
8 | use serde::{Deserialize, Serialize};
9 | use std::error::Error;
10 | use validator::Validate;
11 |
12 | #[derive(Clone, Serialize, Deserialize, Default, Debug)]
13 | pub enum PopOsEdition {
14 | #[default]
15 | Amd,
16 | Nvidia,
17 | }
18 |
19 | #[derive(Clone, Serialize, Deserialize, Default, Debug)]
20 | pub enum PopOsRelease {
21 | #[serde(rename = "21.10")]
22 | #[default]
23 | V21_10,
24 |
25 | #[serde(rename = "22.04")]
26 | V22_04,
27 | }
28 |
29 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
30 | pub struct PopOsTemplate {
31 | pub id: TemplateId,
32 | pub edition: PopOsEdition,
33 | pub release: PopOsRelease,
34 |
35 | pub iso: IsoSource,
36 | pub hostname: HostnameProvisioner,
37 |
38 | pub username: String,
39 |
40 | pub password: String,
41 |
42 | pub root_password: String,
43 |
44 | pub ansible: Option>,
45 | }
46 |
47 | impl Default for PopOsTemplate {
48 | fn default() -> Self {
49 | Self {
50 | id: TemplateId::PopOs,
51 | edition: PopOsEdition::Amd,
52 | release: PopOsRelease::V21_10,
53 | username: whoami::username(),
54 | password: String::from("88Password;"),
55 | root_password: String::from("root"),
56 | iso: IsoContainer {
57 | url: String::from("https://pop-iso.sfo2.cdn.digitaloceanspaces.com/21.10/amd64/intel/7/pop-os_21.10_amd64_intel_7.iso"),
58 | checksum: String::from("sha256:93e8d3977d9414d7f32455af4fa38ea7a71170dc9119d2d1f8e1fba24826fae2"),
59 | },
60 | general: GeneralContainer{
61 | base: TemplateBase::PopOs,
62 | storage_size: String::from("15 GiB"),
63 | .. Default::default()
64 | },
65 | provisioners: ProvisionersContainer::default(),
66 | }
67 | }
68 | }
69 |
70 | impl Template for PopOsTemplate {
71 | fn build(&self, context: &BuildWorker) -> Result<()> {
72 | let mut qemuargs = QemuArgs::new(&context);
73 |
74 | qemuargs.drive.push(format!(
75 | "file={},if=virtio,cache=writeback,discard=ignore,format=qcow2",
76 | context.image_path
77 | ));
78 | qemuargs.drive.push(format!(
79 | "file={},media=cdrom",
80 | MediaCache::get(self.iso.url.clone(), &self.iso.checksum, MediaFormat::Iso)?
81 | ));
82 |
83 | // Start VM
84 | let mut qemu = qemuargs.start_process()?;
85 |
86 | // Send boot command
87 | qemu.vnc.boot_command(vec![
88 | // Wait for boot
89 | wait!(120),
90 | // Select language: English
91 | enter!(),
92 | // Select location: United States
93 | enter!(),
94 | // Select keyboard layout: US
95 | enter!(),
96 | enter!(),
97 | // Select clean install
98 | spacebar!(),
99 | enter!(),
100 | // Select disk
101 | spacebar!(),
102 | enter!(),
103 | // Configure username
104 | enter!(self.username),
105 | // Configure password
106 | input!(self.password),
107 | tab!(),
108 | enter!(self.password),
109 | // Enable disk encryption
110 | enter!(),
111 | // Wait for installation (avoiding screen timeouts)
112 | wait!(250),
113 | spacebar!(),
114 | wait!(250),
115 | // Reboot
116 | enter!(),
117 | wait!(30),
118 | // Unlock disk
119 | enter!(self.password),
120 | wait!(30),
121 | // Login
122 | enter!(),
123 | enter!(self.password),
124 | wait!(60),
125 | // Open terminal
126 | leftSuper!(),
127 | enter!("terminal"),
128 | // Root login
129 | enter!("sudo su -"),
130 | enter!(self.password),
131 | // Change root password
132 | enter!("passwd"),
133 | enter!(self.root_password),
134 | enter!(self.root_password),
135 | // Update package cache
136 | enter!("apt update"),
137 | wait!(30),
138 | // Install sshd
139 | enter!("apt install -y openssh-server"),
140 | wait!(30),
141 | // Configure sshd
142 | enter!("echo 'PermitRootLogin yes' >>/etc/ssh/sshd_config"),
143 | // Start sshd
144 | enter!("systemctl restart sshd"),
145 | ])?;
146 |
147 | // Wait for SSH
148 | let mut ssh = qemu.ssh_wait(context.ssh_port, "root", &self.root_password)?;
149 |
150 | // Run provisioners
151 | self.provisioners.run(&mut ssh)?;
152 |
153 | // Shutdown
154 | ssh.shutdown("poweroff")?;
155 | qemu.shutdown_wait()?;
156 | Ok(())
157 | }
158 |
159 | fn general(&self) -> GeneralContainer {
160 | self.general.clone()
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/slackware/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/slackware/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/slackware/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/slackware/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/steam_deck/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | build::BuildWorker,
3 | cache::{MediaCache, MediaFormat},
4 | provisioners::*,
5 | qemu::QemuArgs,
6 | templates::*,
7 | };
8 | use serde::{Deserialize, Serialize};
9 | use std::error::Error;
10 | use validator::Validate;
11 |
12 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
13 | pub struct SteamDeckTemplate {
14 | pub id: TemplateId,
15 | pub recovery_url: String,
16 |
17 | pub recovery_checksum: String,
18 | }
19 |
20 | impl Default for SteamDeckTemplate {
21 | fn default() -> Self {
22 | Self {
23 | recovery_url: String::from(
24 | "https://steamdeck-images.steamos.cloud/recovery/steamdeck-recovery-1.img.bz2",
25 | ),
26 | recovery_checksum: String::from(
27 | "sha256:5086bcc4fe0fb230dff7265ff6a387dd00045e3d9ae6312de72003e1e82d4526",
28 | ),
29 | general: GeneralContainer {
30 | base: TemplateBase::SteamDeck,
31 | storage_size: String::from("15 GiB"),
32 | ..Default::default()
33 | },
34 | }
35 | }
36 | }
37 |
38 | impl Template for SteamDeckTemplate {
39 | fn build(&self, context: &BuildWorker) -> Result<()> {
40 | let mut qemuargs = QemuArgs::new(&context);
41 |
42 | qemuargs.drive.push(format!(
43 | "file={},format=raw",
44 | MediaCache::get(
45 | self.recovery_url.clone(),
46 | &self.recovery_checksum,
47 | MediaFormat::Bzip2
48 | )?
49 | ));
50 | qemuargs.drive.push(format!(
51 | "file={},if=none,cache=writeback,discard=ignore,format=qcow2,id=nvme",
52 | context.image_path
53 | ));
54 |
55 | // Make the storage looks like an nvme drive
56 | qemuargs
57 | .device
58 | .push(String::from("nvme,serial=cafebabe,drive=nvme"));
59 |
60 | // Start VM
61 | let mut qemu = qemuargs.start_process()?;
62 |
63 | // Send boot command
64 | #[rustfmt::skip]
65 | qemu.vnc.boot_command(vec![
66 | // Initial wait
67 | wait!(20),
68 | // Wait for login
69 | wait_screen_rect!("ba99ede257ef4ee2056a328eb3feffa65e821e0d", 0, 0, 1024, 700),
70 | // Open terminal
71 | leftSuper!(), enter!("terminal"),
72 | // Disable Zenity prompt
73 | enter!("sed -i '/zenity/d' ./tools/repair_device.sh"),
74 | // Poweroff instead of reboot on completion
75 | enter!("sed -i 's/systemctl reboot/systemctl poweroff/' ./tools/repair_device.sh"),
76 | // Begin reimage
77 | enter!("./tools/repair_reimage.sh"),
78 | ])?;
79 |
80 | // Wait for shutdown
81 | qemu.shutdown_wait()?;
82 |
83 | Ok(())
84 | }
85 |
86 | fn general(&self) -> GeneralContainer {
87 | self.general.clone()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/steam_os/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | build::BuildWorker,
3 | cache::{MediaCache, MediaFormat},
4 | provisioners::*,
5 | qemu::QemuArgs,
6 | templates::*,
7 | };
8 | use serde::{Deserialize, Serialize};
9 | use std::error::Error;
10 | use validator::Validate;
11 |
12 | #[derive(Clone, Serialize, Deserialize, Debug)]
13 | pub enum SteamOsVersion {
14 | Brewmaster2_195,
15 | }
16 |
17 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
18 | pub struct SteamOsTemplate {
19 | pub id: TemplateId,
20 | pub version: SteamOsVersion,
21 |
22 | pub iso: IsoSource,
23 | pub hostname: HostnameProvisioner,
24 |
25 | pub root_password: String,
26 |
27 | pub ansible: Option>,
28 | }
29 |
30 | impl Default for SteamOsTemplate {
31 | fn default() -> Self {
32 | Self {
33 | iso: IsoContainer {
34 | url: String::from(
35 | "https://repo.steampowered.com/download/brewmaster/2.195/SteamOSDVD.iso",
36 | ),
37 | checksum: String::from("sha512:0ce55048d2c5e8a695f309abe22303dded003c93386ad28c6daafc977b3d5b403ed94d7c38917c8c837a2b1fe560184cf3cc12b9f2c4069fd70ed0deab47eb7c"),
38 | },
39 | root_password: String::from("root"),
40 | version: SteamOsVersion::Brewmaster2_195,
41 | general: GeneralContainer{
42 | base: TemplateBase::SteamOs,
43 | storage_size: String::from("15 GiB"),
44 | .. Default::default()
45 | },
46 | provisioners: ProvisionersContainer::default(),
47 | }
48 | }
49 | }
50 |
51 | impl Template for SteamOsTemplate {
52 | fn build(&self, context: &BuildWorker) -> Result<()> {
53 | let mut qemuargs = QemuArgs::new(&context);
54 |
55 | qemuargs.drive.push(format!(
56 | "file={},if=virtio,cache=writeback,discard=ignore,format=qcow2",
57 | context.image_path
58 | ));
59 | qemuargs.drive.push(format!(
60 | "file={},media=cdrom",
61 | MediaCache::get(self.iso.url.clone(), &self.iso.checksum, MediaFormat::Iso)?
62 | ));
63 |
64 | // Start VM
65 | let mut qemu = qemuargs.start_process()?;
66 |
67 | // Send boot command
68 | #[rustfmt::skip]
69 | qemu.vnc.boot_command(vec![
70 | // Wait for bootloader
71 | wait_screen!("28fe084e08242584908114a5d21960fdf072adf9"),
72 | // Start automated install
73 | enter!(),
74 | // Wait for completion
75 | wait_screen!(""),
76 | ])?;
77 |
78 | // Wait for SSH
79 | let mut ssh = qemu.ssh_wait(context.ssh_port, "root", &self.root_password)?;
80 |
81 | // Run provisioners
82 | self.provisioners.run(&mut ssh)?;
83 |
84 | // Shutdown
85 | ssh.shutdown("poweroff")?;
86 | qemu.shutdown_wait()?;
87 | Ok(())
88 | }
89 |
90 | fn general(&self) -> GeneralContainer {
91 | self.general.clone()
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/ubuntu/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/ubuntu/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/ubuntu/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/ubuntu/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/ubuntu/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | build::BuildWorker,
3 | cache::{MediaCache, MediaFormat},
4 | provisioners::*,
5 | qemu::QemuArgs,
6 | templates::*,
7 | };
8 | use serde::{Deserialize, Serialize};
9 | use std::error::Error;
10 | use strum::{Display, EnumIter, IntoEnumIterator};
11 | use validator::Validate;
12 |
13 | #[derive(Clone, Serialize, Deserialize, Debug, EnumIter)]
14 | pub enum UbuntuRelease {
15 | Jammy,
16 | Impish,
17 | Hirsute,
18 | Groovy,
19 | Focal,
20 | Eoan,
21 | Disco,
22 | Cosmic,
23 | Bionic,
24 | Artful,
25 | }
26 |
27 | impl Display for UbuntuRelease {
28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 | write!(
30 | f,
31 | "{}",
32 | match &self {
33 | UbuntuRelease::Jammy => "22.04 LTS (Jammy Jellyfish)",
34 | UbuntuRelease::Impish => "21.10 (Impish Indri)",
35 | UbuntuRelease::Hirsute => "21.04 (Hirsute Hippo)",
36 | UbuntuRelease::Groovy => "20.10 (Groovy Gorilla)",
37 | }
38 | )
39 | }
40 | }
41 |
42 | #[derive(Clone, Serialize, Deserialize, Debug, EnumIter, Display)]
43 | pub enum UbuntuEdition {
44 | Server,
45 | Desktop,
46 | }
47 |
48 | impl Prompt for UbuntuEdition {}
49 |
50 | /// Ubuntu is a Linux distribution derived from Debian and composed mostly of free
51 | /// and open-source software.
52 | ///
53 | /// Upstream: https://ubuntu.com
54 | /// Maintainer: cilki
55 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
56 | pub struct Ubuntu {
57 | pub edition: UbuntuEdition,
58 | pub release: UbuntuRelease,
59 |
60 | pub source: UbuntuSource,
61 | pub provisioners: Option>,
62 | }
63 |
64 | pub enum UbuntuSource {
65 | Iso(IsoSource),
66 | }
67 |
68 | pub enum UbuntuProvisioner {
69 | Ansible(AnsibleProvisioner),
70 | Hostname(HostnameProvisoner),
71 | }
72 |
73 | impl Default for UbuntuTemplate {
74 | fn default() -> Self {
75 | Self {
76 | edition: UbuntuEdition::Desktop,
77 | release: UbuntuRelease::Jammy,
78 | provisioners: None,
79 | }
80 | }
81 | }
82 |
83 | impl Template for UbuntuTemplate {
84 | fn build(&self, context: &BuildWorker) -> Result<()> {
85 | let mut qemuargs = QemuArgs::new(&context);
86 |
87 | qemuargs.drive.push(format!(
88 | "file={},if=virtio,cache=writeback,discard=ignore,format=qcow2",
89 | context.image_path
90 | ));
91 | qemuargs.drive.push(format!(
92 | "file={},media=cdrom",
93 | MediaCache::get(self.iso.url.clone(), &self.iso.checksum, MediaFormat::Iso)?
94 | ));
95 |
96 | // Start VM
97 | let mut qemu = qemuargs.start_process()?;
98 |
99 | // Send boot command
100 | #[rustfmt::skip]
101 | qemu.vnc.boot_command(vec![
102 | ])?;
103 |
104 | // Wait for SSH
105 | let mut ssh = qemu.ssh_wait(context.ssh_port, "root", &self.root_password)?;
106 |
107 | // Run provisioners
108 | self.provisioners.run(&mut ssh)?;
109 |
110 | // Shutdown
111 | ssh.shutdown("poweroff")?;
112 | qemu.shutdown_wait()?;
113 | Ok(())
114 | }
115 |
116 | fn general(&self) -> GeneralContainer {
117 | self.general.clone()
118 | }
119 | }
120 |
121 | impl Prompt for Ubuntu {
122 | fn prompt(&mut self, _builder: &Foundry) -> Result<()> {
123 | // Prompt edition
124 | {
125 | let editions: Vec = UbuntuEdition::iter().collect();
126 | let edition_index = dialoguer::Select::with_theme(&crate::cli::cmd::init::theme())
127 | .with_prompt("Choose Ubuntu edition")
128 | .default(0)
129 | .items(&editions)
130 | .interact()?;
131 |
132 | self.edition = editions[edition_index];
133 | }
134 |
135 | // Prompt release
136 | {
137 | let releases: Vec = UbuntuRelease::iter().collect();
138 | let release_index = dialoguer::Select::with_theme(&crate::cli::cmd::init::theme())
139 | .with_prompt("Choose Ubuntu release")
140 | .default(0)
141 | .items(&releases)
142 | .interact()?;
143 |
144 | self.release = releases[release_index];
145 | }
146 |
147 | Ok(())
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/goldboot/src/builder/os/windows_10/configure_winrm.ps1:
--------------------------------------------------------------------------------
1 | # Supress network location Prompt
2 | New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff" -Force
3 |
4 | # Set network to private
5 | $ifaceinfo = Get-NetConnectionProfile
6 | Set-NetConnectionProfile -InterfaceIndex $ifaceinfo.InterfaceIndex -NetworkCategory Private
7 |
8 | # Configure WinRM itself
9 | winrm quickconfig -q
10 | winrm s "winrm/config" '@{MaxTimeoutms="1800000"}'
11 | winrm s "winrm/config/winrs" '@{MaxMemoryPerShellMB="2048"}'
12 | winrm s "winrm/config/service" '@{AllowUnencrypted="true"}'
13 | winrm s "winrm/config/service/auth" '@{Basic="true"}'
14 |
15 | # Enable the WinRM Firewall rule, which will likely already be enabled due to the 'winrm quickconfig' command above
16 | Enable-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)"
17 |
18 | exit 0
--------------------------------------------------------------------------------
/goldboot/src/builder/os/windows_10/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/windows_10/icon.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/windows_10/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/os/windows_10/icon@2x.png
--------------------------------------------------------------------------------
/goldboot/src/builder/os/windows_7/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::provisioners::*;
2 | use serde::{Deserialize, Serialize};
3 | use std::error::Error;
4 | use validator::Validate;
5 |
6 | #[derive(Clone, Serialize, Deserialize, Validate, Debug)]
7 | pub struct Windows7Template {
8 | pub id: TemplateId,
9 |
10 | pub iso: IsoSource,
11 | pub ansible: Option>,
12 | }
13 |
--------------------------------------------------------------------------------
/goldboot/src/builder/ovmf/aarch64.fd.zst:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/ovmf/aarch64.fd.zst
--------------------------------------------------------------------------------
/goldboot/src/builder/ovmf/i386.fd.zst:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/ovmf/i386.fd.zst
--------------------------------------------------------------------------------
/goldboot/src/builder/ovmf/mod.rs:
--------------------------------------------------------------------------------
1 | // UEFI firmwares for various platforms. We include them here to avoid having
2 | // to depend on one provided by the system.
3 |
4 | use anyhow::Result;
5 | use anyhow::bail;
6 | use goldboot_image::ImageArch;
7 | use std::path::Path;
8 | use std::path::PathBuf;
9 |
10 | // TODO use build script to download these from: https://github.com/retrage/edk2-nightly
11 | #[cfg(feature = "include_ovmf")]
12 | pub fn write(arch: ImageArch, path: impl AsRef) -> Result<()> {
13 | match &arch {
14 | ImageArch::Amd64 => {
15 | std::fs::write(
16 | &path,
17 | zstd::decode_all(std::io::Cursor::new(include_bytes!("x86_64.fd.zst")))?,
18 | )?;
19 | }
20 | ImageArch::I386 => {
21 | std::fs::write(
22 | &path,
23 | zstd::decode_all(std::io::Cursor::new(include_bytes!("i386.fd.zst")))?,
24 | )?;
25 | }
26 | ImageArch::Arm64 => {
27 | std::fs::write(
28 | &path,
29 | zstd::decode_all(std::io::Cursor::new(include_bytes!("aarch64.fd.zst")))?,
30 | )?;
31 | }
32 | _ => bail!("Unsupported architecture"),
33 | }
34 | Ok(())
35 | }
36 |
37 | pub fn find() -> Option {
38 | // TODO
39 | None
40 | }
41 |
--------------------------------------------------------------------------------
/goldboot/src/builder/ovmf/x86_64.fd.zst:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/builder/ovmf/x86_64.fd.zst
--------------------------------------------------------------------------------
/goldboot/src/builder/sources.rs:
--------------------------------------------------------------------------------
1 | use crate::cli::progress::ProgressBar;
2 | use anyhow::Result;
3 | use anyhow::anyhow;
4 | use anyhow::bail;
5 | use sha1::{Digest, Sha1};
6 | use sha2::{Sha256, Sha512};
7 | use std::{
8 | fs::File,
9 | path::{Path, PathBuf},
10 | };
11 | use tracing::{debug, info};
12 |
13 | /// Simple cache for source installation media like ISOs.
14 | pub struct SourceCache {
15 | /// Cache location on disk
16 | pub directory: PathBuf,
17 | }
18 |
19 | impl SourceCache {
20 | /// Get the default platform-dependent source cache.
21 | pub fn default() -> Result {
22 | let directory = if cfg!(target_os = "linux") {
23 | PathBuf::from(format!(
24 | "/home/{}/.cache/goldboot/sources",
25 | whoami::username()
26 | ))
27 | } else if cfg!(target_os = "macos") {
28 | PathBuf::from(format!(
29 | "/Users/{}/.cache/goldboot/sources",
30 | whoami::username()
31 | ))
32 | } else if cfg!(target_os = "windows") {
33 | PathBuf::from(format!(
34 | "C:/Users/{}/AppData/Local/goldboot/cache/sources",
35 | whoami::username()
36 | ))
37 | } else {
38 | bail!("Unsupported platform");
39 | };
40 |
41 | // Make sure it exists before we return
42 | std::fs::create_dir_all(&directory)?;
43 | Ok(Self { directory })
44 | }
45 |
46 | pub fn get(&self, url: String, checksum: Option) -> Result {
47 | let id = hex::encode(Sha1::new().chain_update(&url).finalize());
48 | let path = self.directory.join(id);
49 |
50 | // Delete file if the checksum doesn't match
51 | if let Some(checksum) = checksum.clone() {
52 | if path.is_file() {
53 | if !Self::verify_checksum(path.to_string_lossy().to_string(), checksum.as_str())
54 | .is_ok()
55 | {
56 | info!("Deleting corrupt cached file");
57 | std::fs::remove_file(&path)?;
58 | }
59 | }
60 | }
61 |
62 | if !path.is_file() {
63 | // Check for local URL
64 | if !url.starts_with("http") && Path::new(&url).is_file() {
65 | return Ok(url);
66 | }
67 |
68 | // Try to download it
69 | let mut rs = reqwest::blocking::get(&url)?;
70 | if rs.status().is_success() {
71 | let length = rs
72 | .content_length()
73 | .ok_or_else(|| anyhow!("Failed to get content length"))?;
74 | let mut file = File::create(&path)?;
75 |
76 | info!("Saving install media");
77 | ProgressBar::Download.copy(&mut rs, &mut file, length)?;
78 | } else {
79 | bail!("Failed to download");
80 | }
81 |
82 | if let Some(checksum) = checksum {
83 | Self::verify_checksum(path.to_string_lossy().to_string(), checksum.as_str())?;
84 | }
85 | }
86 |
87 | Ok(path.to_string_lossy().to_string())
88 | }
89 |
90 | fn verify_checksum(path: String, checksum: &str) -> Result<()> {
91 | // "None" shortcut
92 | if checksum == "none" {
93 | return Ok(());
94 | }
95 |
96 | let c: Vec<&str> = checksum.split(":").collect();
97 | if c.len() != 2 {
98 | bail!("Invalid checksum: {}", checksum);
99 | }
100 |
101 | let mut file = File::open(&path)?;
102 |
103 | let hash = match c[0] {
104 | "sha1" | "SHA1" => {
105 | info!("Computing SHA1 checksum");
106 | let mut hasher = Sha1::new();
107 | ProgressBar::Hash.copy(&mut file, &mut hasher, std::fs::metadata(&path)?.len())?;
108 | hex::encode(hasher.finalize())
109 | }
110 | "sha256" | "SHA256" => {
111 | info!("Computing SHA256 checksum");
112 | let mut hasher = Sha256::new();
113 | ProgressBar::Hash.copy(&mut file, &mut hasher, std::fs::metadata(&path)?.len())?;
114 | hex::encode(hasher.finalize())
115 | }
116 | "sha512" | "SHA512" => {
117 | info!("Computing SHA512 checksum");
118 | let mut hasher = Sha512::new();
119 | ProgressBar::Hash.copy(&mut file, &mut hasher, std::fs::metadata(&path)?.len())?;
120 | hex::encode(hasher.finalize())
121 | }
122 | _ => bail!("Unsupported hash"),
123 | };
124 |
125 | debug!("Computed: {}", &hash);
126 | debug!("Expected: {}", &c[1]);
127 |
128 | if hash != c[1] {
129 | bail!("Hash mismatch");
130 | }
131 |
132 | Ok(())
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/goldboot/src/builder/sources/buildroot.rs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/build.rs:
--------------------------------------------------------------------------------
1 | use crate::builder::Builder;
2 | use crate::config::ConfigPath;
3 | use std::process::ExitCode;
4 | use tracing::{debug, error};
5 | use validator::Validate;
6 |
7 | pub fn run(cmd: super::Commands) -> ExitCode {
8 | match cmd.clone() {
9 | super::Commands::Build {
10 | read_password,
11 | path,
12 | ..
13 | } => {
14 | let config_path = match ConfigPath::from_dir(path) {
15 | Some(p) => {
16 | debug!("Loading config from {}", p);
17 | p
18 | }
19 | _ => {
20 | error!("Failed to find config file");
21 | return ExitCode::FAILURE;
22 | }
23 | };
24 |
25 | // Load config from current directory
26 | let elements = match config_path.load() {
27 | Ok(elements) => {
28 | debug!("Loaded: {:#?}", &elements);
29 | elements
30 | }
31 | Err(error) => {
32 | error!("Failed to load config: {:?}", error);
33 | return ExitCode::FAILURE;
34 | }
35 | };
36 |
37 | let mut builder = Builder::new(elements);
38 |
39 | // Include the encryption password if provided
40 | if read_password {
41 | print!("Enter password: ");
42 | let mut password = String::new();
43 | std::io::stdin().read_line(&mut password).unwrap();
44 | // config.password = Some(password);
45 | } else if let Ok(_password) = std::env::var("GOLDBOOT_PASSWORD") {
46 | // Wipe out the value since we no longer need it
47 | unsafe {
48 | std::env::set_var("GOLDBOOT_PASSWORD", "");
49 | }
50 | // config.password = Some(password);
51 | }
52 |
53 | // Fully verify config before proceeding
54 | match builder.validate() {
55 | Err(err) => {
56 | error!(error = ?err, "Failed to validate config file");
57 | return ExitCode::FAILURE;
58 | }
59 | _ => debug!("Validated config file"),
60 | };
61 |
62 | // Run the build finally
63 | match builder.run(cmd) {
64 | Err(err) => {
65 | error!(error = ?err, "Failed to build image");
66 | ExitCode::FAILURE
67 | }
68 | _ => ExitCode::SUCCESS,
69 | }
70 | }
71 | _ => panic!(),
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/deploy.rs:
--------------------------------------------------------------------------------
1 | use console::Style;
2 | use dialoguer::{Confirm, theme::ColorfulTheme};
3 | use goldboot_image::ImageHandle;
4 | use std::{path::Path, process::ExitCode};
5 | use tracing::error;
6 |
7 | use crate::{cli::progress::ProgressBar, library::ImageLibrary};
8 |
9 | pub fn run(cmd: super::Commands) -> ExitCode {
10 | match cmd {
11 | super::Commands::Deploy {
12 | image,
13 | output,
14 | confirm,
15 | } => {
16 | let theme = ColorfulTheme {
17 | values_style: Style::new().yellow().dim(),
18 | ..ColorfulTheme::default()
19 | };
20 |
21 | let mut image_handle = if Path::new(&image).exists() {
22 | match ImageHandle::open(&image) {
23 | Ok(image_handle) => image_handle,
24 | Err(_) => return ExitCode::FAILURE,
25 | }
26 | } else {
27 | match ImageLibrary::find_by_id(&image) {
28 | Ok(image_handle) => image_handle,
29 | Err(_) => return ExitCode::FAILURE,
30 | }
31 | };
32 | if image_handle.load(None).is_err() {
33 | return ExitCode::FAILURE;
34 | }
35 |
36 | if Path::new(&output).exists() && !confirm {
37 | if !Confirm::with_theme(&theme)
38 | .with_prompt("Do you want to continue?")
39 | .interact()
40 | .unwrap()
41 | {
42 | std::process::exit(0);
43 | }
44 | }
45 |
46 | // TODO special case for GBL; select images to include
47 |
48 | match image_handle.write(output, ProgressBar::Write.new_empty()) {
49 | Err(err) => {
50 | error!(error = ?err, "Failed to write image");
51 | ExitCode::FAILURE
52 | }
53 | _ => ExitCode::SUCCESS,
54 | }
55 | }
56 | _ => panic!(),
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/image.rs:
--------------------------------------------------------------------------------
1 | use std::process::ExitCode;
2 |
3 | use crate::library::ImageLibrary;
4 | use chrono::TimeZone;
5 |
6 | use ubyte::ToByteUnit;
7 |
8 | pub fn run(cmd: super::Commands) -> ExitCode {
9 | match cmd {
10 | super::Commands::Image { command } => match &command {
11 | super::ImageCommands::List {} => {
12 | let images = ImageLibrary::find_all().unwrap();
13 |
14 | println!(
15 | "Image Name Image Size Build Date Image ID Description"
16 | );
17 | for image in images {
18 | println!(
19 | "{:15} {:12} {:31} {:12} {}",
20 | todo!(),
21 | image.primary_header.size.bytes().to_string(),
22 | chrono::Utc
23 | .timestamp(image.primary_header.timestamp as i64, 0)
24 | .to_rfc2822(),
25 | &image.id[0..12],
26 | "TODO",
27 | );
28 | }
29 | ExitCode::SUCCESS
30 | }
31 | super::ImageCommands::Info { image } => {
32 | if let Some(image) = image {
33 | let _image = ImageLibrary::find_by_id(image).unwrap();
34 | // TODO
35 | }
36 |
37 | ExitCode::SUCCESS
38 | }
39 | },
40 | _ => panic!(),
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/init.rs:
--------------------------------------------------------------------------------
1 | use clap::ValueEnum;
2 | use console::Style;
3 | use dialoguer::{Confirm, Input, Password, Select, theme::ColorfulTheme};
4 | use goldboot_image::ImageArch;
5 | use std::process::ExitCode;
6 | use strum::IntoEnumIterator;
7 | use tracing::{error, info};
8 |
9 | use crate::{
10 | builder::{Builder, os::Os},
11 | cli::prompt::Prompt,
12 | config::ConfigPath,
13 | };
14 |
15 | fn print_banner() {
16 | if console::colors_enabled() {
17 | let style = Style::new().yellow();
18 |
19 | println!("{}", "");
20 | for line in fossable::goldboot_word() {
21 | println!(" {}", style.apply_to(line));
22 | }
23 | println!("{}", "");
24 | }
25 | }
26 |
27 | /// Get the current theme for prompts.
28 | pub fn theme() -> ColorfulTheme {
29 | ColorfulTheme {
30 | values_style: Style::new().yellow().dim(),
31 | ..ColorfulTheme::default()
32 | }
33 | }
34 |
35 | pub fn run(cmd: super::Commands) -> ExitCode {
36 | match cmd {
37 | super::Commands::Init {
38 | name,
39 | os,
40 | format,
41 | mimic_hardware: _,
42 | } => {
43 | let mut config_path = ConfigPath::from_dir(".").unwrap_or(format);
44 | let mut builder = Builder::new(os);
45 |
46 | if builder.elements.len() == 0 {
47 | // If no OS was given, begin interactive config
48 | print_banner();
49 |
50 | let theme = theme();
51 |
52 | println!("Get ready to create a new image configuration!");
53 | println!("(it can be further edited later)");
54 | println!();
55 |
56 | // Prompt config format
57 | {
58 | let formats: &[ConfigPath] = ConfigPath::value_variants();
59 | let choice_index = Select::with_theme(&theme)
60 | .with_prompt("Config format?")
61 | .default(0)
62 | .items(formats.iter())
63 | .interact()
64 | .unwrap();
65 | config_path = formats[choice_index].clone();
66 | }
67 |
68 | // Prompt image name
69 | // builder.name = Input::with_theme(&theme)
70 | // .with_prompt("Image name?")
71 | // .default(
72 | // std::env::current_dir()
73 | // .unwrap()
74 | // .file_name()
75 | // .unwrap()
76 | // .to_str()
77 | // .unwrap()
78 | // .to_string(),
79 | // )
80 | // .interact()
81 | // .unwrap();
82 |
83 | // Prompt image architecture
84 | let arch = {
85 | let architectures: Vec = ImageArch::iter().collect();
86 | let choice_index = Select::with_theme(&theme)
87 | .with_prompt("Image architecture?")
88 | .default(0)
89 | .items(&architectures)
90 | .interact()
91 | .unwrap();
92 |
93 | architectures[choice_index]
94 | };
95 |
96 | // Prompt OS
97 | loop {
98 | // Find operating systems suitable for the architecture
99 | let mut supported_os: Vec = Os::iter()
100 | .filter(|os| os.architectures().contains(&arch))
101 | .filter(|os| builder.elements.len() == 0 || os.alloy())
102 | .collect();
103 |
104 | let choice_index = Select::with_theme(&theme)
105 | .with_prompt("Operating system?")
106 | .items(&supported_os)
107 | .interact()
108 | .unwrap();
109 |
110 | let os = &mut supported_os[choice_index];
111 |
112 | if Confirm::with_theme(&theme)
113 | .with_prompt("Edit OS configuration?")
114 | .interact()
115 | .unwrap()
116 | {
117 | // TODO show some kind of banner
118 | os.prompt(&builder).unwrap();
119 | }
120 |
121 | builder.elements.push(os.clone());
122 |
123 | if !os.alloy()
124 | || !Confirm::with_theme(&theme)
125 | .with_prompt("Create an alloy image (multiboot)?")
126 | .interact()
127 | .unwrap()
128 | {
129 | break;
130 | }
131 | }
132 | }
133 |
134 | // Finally write out the config
135 | match config_path.write(&builder.elements) {
136 | Err(err) => {
137 | error!(error = ?err, "Failed to write config file");
138 | ExitCode::FAILURE
139 | }
140 | _ => {
141 | info!(path = %config_path, "Wrote goldboot config successfully");
142 | ExitCode::SUCCESS
143 | }
144 | }
145 | }
146 | _ => panic!(),
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/liveusb.rs:
--------------------------------------------------------------------------------
1 | use console::Style;
2 | use dialoguer::{Confirm, theme::ColorfulTheme};
3 | use std::{path::Path, process::ExitCode};
4 | use tracing::error;
5 |
6 | use crate::{cli::progress::ProgressBar, builder::os::Os, library::ImageLibrary};
7 |
8 | pub fn run(cmd: super::Commands) -> ExitCode {
9 | match cmd {
10 | super::Commands::Liveusb {
11 | dest,
12 | include,
13 | confirm,
14 | } => {
15 | let theme = ColorfulTheme {
16 | values_style: Style::new().yellow().dim(),
17 | ..ColorfulTheme::default()
18 | };
19 |
20 | if !Path::new(&dest).exists() {
21 | return ExitCode::FAILURE;
22 | }
23 |
24 | // Load from library or download
25 | let mut image_handles = match ImageLibrary::find_by_os("Goldboot") {
26 | Ok(image_handles) => image_handles,
27 | Err(_) => return ExitCode::FAILURE,
28 | };
29 |
30 | // TODO prompt password
31 | if image_handles[0].load(None).is_err() {
32 | return ExitCode::FAILURE;
33 | }
34 |
35 | if !confirm {
36 | if !Confirm::with_theme(&theme)
37 | .with_prompt(format!("Do you want to overwrite: {}?", dest))
38 | .interact()
39 | .unwrap()
40 | {
41 | return ExitCode::FAILURE;
42 | }
43 | }
44 |
45 | match image_handles[0].write(dest, ProgressBar::Write.new_empty()) {
46 | Err(err) => {
47 | error!(error = ?err, "Failed to write image");
48 | ExitCode::FAILURE
49 | }
50 | _ => ExitCode::SUCCESS,
51 | }
52 | }
53 | _ => panic!(),
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/mod.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use crate::builder::os::Os;
4 | use crate::config::ConfigPath;
5 |
6 | pub mod build;
7 | pub mod deploy;
8 | pub mod image;
9 | pub mod init;
10 | pub mod liveusb;
11 | pub mod registry;
12 |
13 | #[derive(clap::Subcommand, Debug, Clone)]
14 | pub enum Commands {
15 | /// Build a new image
16 | Build {
17 | /// Save a screenshot to ./screenshots after each boot command for
18 | /// debugging
19 | #[clap(long, num_args = 0)]
20 | record: bool,
21 |
22 | /// Insert a breakpoint after each boot command
23 | #[clap(long, num_args = 0)]
24 | debug: bool,
25 |
26 | /// Read the encryption password from STDIN
27 | #[clap(long, num_args = 0)]
28 | read_password: bool,
29 |
30 | /// Disable virtual machine acceleration even when available
31 | #[clap(long, num_args = 0)]
32 | no_accel: bool,
33 |
34 | /// The optional output destination (defaults to image library)
35 | #[clap(long)]
36 | output: Option,
37 |
38 | #[clap(long)]
39 | ovmf_path: Option,
40 |
41 | /// The context directory (containing a goldboot config file)
42 | #[clap(index = 1)]
43 | path: String,
44 | // The image will be run as a virtual machine for testing
45 | // #[clap(long, num_args = 0)]
46 | // virtual: bool
47 | },
48 |
49 | /// Manage local images
50 | Image {
51 | #[clap(subcommand)]
52 | command: ImageCommands,
53 | },
54 |
55 | /// Write images to storage
56 | Deploy {
57 | /// The ID or path of the image to write
58 | #[clap(index = 1)]
59 | image: String,
60 |
61 | /// The output destination
62 | #[clap(long)]
63 | output: String,
64 |
65 | /// Do not prompt for confirmation (be extremely careful with this)
66 | #[clap(long, num_args = 0)]
67 | confirm: bool,
68 | },
69 |
70 | /// Initialize the current directory as a new goldboot project
71 | Init {
72 | /// New image name
73 | #[clap(long)]
74 | name: Option,
75 |
76 | /// Base operating system(s)
77 | #[clap(long, value_enum)]
78 | os: Vec,
79 |
80 | #[clap(long, default_value_t, value_enum)]
81 | format: ConfigPath,
82 |
83 | // #[clap(long, num_args = 0)]
84 | // list: bool,
85 | /// Attempt to copy the configuration of the current hardware as closely
86 | /// as possible
87 | #[clap(long, num_args = 0)]
88 | mimic_hardware: bool,
89 | },
90 |
91 | /// Manage image registries
92 | Registry {
93 | #[clap(subcommand)]
94 | command: RegistryCommands,
95 | },
96 |
97 | /// Create a bootable live USB
98 | Liveusb {
99 | /// Destination device path
100 | #[clap(long)]
101 | dest: String,
102 |
103 | /// Images to include in the live USB
104 | #[clap(long, value_enum)]
105 | include: Vec,
106 |
107 | /// Do not prompt for confirmation (be extremely careful with this)
108 | #[clap(long, num_args = 0)]
109 | confirm: bool,
110 | },
111 | }
112 |
113 | #[derive(clap::Subcommand, Debug, Clone)]
114 | pub enum RegistryCommands {
115 | /// Enter a token for a registry
116 | Login {},
117 |
118 | /// Upload a local image to a remote registry
119 | Push { url: String },
120 |
121 | /// Download an image from a remote registry
122 | Pull { url: String },
123 | }
124 |
125 | #[derive(clap::Subcommand, Debug, Clone)]
126 | pub enum ImageCommands {
127 | /// List local images
128 | List {},
129 |
130 | /// Get detailed image info
131 | Info { image: Option },
132 | }
133 |
--------------------------------------------------------------------------------
/goldboot/src/cli/cmd/registry.rs:
--------------------------------------------------------------------------------
1 | use console::Style;
2 | use dialoguer::{Input, theme::ColorfulTheme};
3 | use std::process::ExitCode;
4 |
5 | use super::RegistryCommands;
6 |
7 | pub fn run(cmd: super::Commands) -> ExitCode {
8 | let theme = ColorfulTheme {
9 | values_style: Style::new().yellow().dim(),
10 | ..ColorfulTheme::default()
11 | };
12 |
13 | match cmd {
14 | super::Commands::Registry { command } => match &command {
15 | RegistryCommands::Push { url: _ } => todo!(),
16 | RegistryCommands::Pull { url: _ } => todo!(),
17 | RegistryCommands::Login {} => {
18 | // Prompt registry URL
19 | let _registry_url: String = Input::with_theme(&theme)
20 | .with_prompt("Enter registry URL")
21 | .interact()
22 | .unwrap();
23 |
24 | // Prompt registry token
25 | let _registry_token: String = Input::with_theme(&theme)
26 | .with_prompt("Enter registry token")
27 | .interact()
28 | .unwrap();
29 |
30 | // Prompt token passphrase
31 | let _token_passphrase: String = Input::with_theme(&theme)
32 | .with_prompt(
33 | "Enter a passphrase to encrypt the token or nothing to store plaintext",
34 | )
35 | .interact()
36 | .unwrap();
37 | ExitCode::SUCCESS
38 | }
39 | },
40 | _ => panic!(),
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/goldboot/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod cmd;
2 | pub mod progress;
3 | pub mod prompt;
4 |
--------------------------------------------------------------------------------
/goldboot/src/cli/progress.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use std::io::IsTerminal;
3 | use std::{
4 | cmp::min,
5 | io::{Read, Write},
6 | time::Duration,
7 | };
8 |
9 | pub enum ProgressBar {
10 | /// A hashing operation
11 | Hash,
12 |
13 | /// An image conversion operation
14 | Convert,
15 |
16 | /// A download operation
17 | Download,
18 |
19 | /// An image write operation
20 | Write,
21 | }
22 |
23 | impl ProgressBar {
24 | fn create_progressbar(&self, len: u64) -> indicatif::ProgressBar {
25 | match self {
26 | ProgressBar::Hash => {
27 | let progress = indicatif::ProgressBar::new(len);
28 | progress.set_style(indicatif::ProgressStyle::default_bar().template("{spinner:.blue} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap().progress_chars("=>-"));
29 | progress.enable_steady_tick(Duration::from_millis(50));
30 | progress
31 | }
32 | ProgressBar::Convert => {
33 | let progress = indicatif::ProgressBar::new(len);
34 | progress.set_style(indicatif::ProgressStyle::default_bar().template("{spinner:.yellow} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap().progress_chars("=>-"));
35 | progress.enable_steady_tick(Duration::from_millis(50));
36 | progress
37 | }
38 | ProgressBar::Download => {
39 | let progress = indicatif::ProgressBar::new(len);
40 | progress.set_style(indicatif::ProgressStyle::default_bar().template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap().progress_chars("=>-"));
41 | progress.enable_steady_tick(Duration::from_millis(50));
42 | progress
43 | }
44 | ProgressBar::Write => {
45 | let progress = indicatif::ProgressBar::new(len);
46 | progress.set_style(indicatif::ProgressStyle::default_bar().template("{spinner:.red} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap().progress_chars("=>-"));
47 | progress.enable_steady_tick(Duration::from_millis(50));
48 | progress
49 | }
50 | }
51 | }
52 |
53 | pub fn new(&self, len: u64) -> Box {
54 | if !show_progress() {
55 | // No progress bar
56 | return Box::new(|_| {});
57 | }
58 |
59 | let progress = self.create_progressbar(len);
60 | Box::new(move |v| {
61 | if progress.position() + v >= len {
62 | progress.finish_and_clear();
63 | } else {
64 | progress.inc(v);
65 | }
66 | })
67 | }
68 |
69 | pub fn new_empty(&self) -> Box {
70 | if !show_progress() {
71 | // No progress bar
72 | return Box::new(|_, _| {});
73 | }
74 |
75 | let progress = self.create_progressbar(0);
76 | Box::new(move |v, t| {
77 | progress.set_length(t);
78 | if progress.position() + v >= t {
79 | progress.finish_and_clear();
80 | } else {
81 | progress.inc(v);
82 | }
83 | })
84 | }
85 |
86 | /// Fully copy the given reader to the given writer and display a
87 | /// progressbar if running in interactive mode.
88 | pub fn copy(&self, reader: &mut dyn Read, writer: &mut dyn Write, len: u64) -> Result<()> {
89 | if !show_progress() {
90 | // No progress bar
91 | std::io::copy(reader, writer)?;
92 | return Ok(());
93 | }
94 |
95 | let progress = self.create_progressbar(len);
96 |
97 | let mut buffer = [0u8; 1024 * 1024];
98 | let mut copied: u64 = 0;
99 |
100 | loop {
101 | if let Ok(size) = reader.read(&mut buffer) {
102 | if size == 0 {
103 | break;
104 | }
105 | writer.write(&buffer[0..size])?;
106 | let new = min(copied + (size as u64), len);
107 | copied = new;
108 | progress.set_position(new);
109 | } else {
110 | break;
111 | }
112 | }
113 |
114 | progress.finish_and_clear();
115 | Ok(())
116 | }
117 | }
118 | fn show_progress() -> bool {
119 | std::io::stdout().is_terminal() && !std::env::var("CI").is_ok()
120 | }
121 |
--------------------------------------------------------------------------------
/goldboot/src/cli/prompt.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use enum_dispatch::enum_dispatch;
3 |
4 | use crate::builder::Builder;
5 |
6 | /// Prompt the user for additional information on the command line.
7 | #[enum_dispatch(Os)]
8 | pub trait Prompt {
9 | fn prompt(&mut self, builder: &Builder) -> Result<()>;
10 | }
11 |
--------------------------------------------------------------------------------
/goldboot/src/gbl.rs:
--------------------------------------------------------------------------------
1 |
2 | pub fn merge(qcow_images: Vec) {
3 |
4 | }
--------------------------------------------------------------------------------
/goldboot/src/gui/app.rs:
--------------------------------------------------------------------------------
1 | use super::{
2 | resources::TextureCache,
3 | screens::{registry_login, Screen},
4 | state::AppState,
5 | theme::Theme,
6 | };
7 |
8 | pub struct GuiApp {
9 | pub screen: Screen,
10 | pub state: AppState,
11 | pub theme: Theme,
12 | pub textures: TextureCache,
13 | }
14 |
15 | impl GuiApp {
16 | pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
17 | let theme = Theme::default();
18 | theme.apply_to_context(&cc.egui_ctx);
19 |
20 | Self {
21 | screen: Screen::SelectImage,
22 | state: AppState::new(),
23 | theme,
24 | textures: TextureCache::new(&cc.egui_ctx),
25 | }
26 | }
27 | }
28 |
29 | impl eframe::App for GuiApp {
30 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
31 | // Render grid background
32 | self.theme.render_background(ctx);
33 |
34 | // Handle global hotkeys
35 | self.handle_hotkeys(ctx);
36 |
37 | // Main panel
38 | egui::CentralPanel::default()
39 | .frame(egui::Frame::none())
40 | .show(ctx, |ui| {
41 | self.screen
42 | .render(ui, &mut self.state, &self.textures, &self.theme);
43 | });
44 |
45 | // Render registry login dialog if open
46 | registry_login::render(ctx, &mut self.state, &self.theme);
47 | }
48 | }
49 |
50 | impl GuiApp {
51 | fn handle_hotkeys(&mut self, ctx: &egui::Context) {
52 | ctx.input(|i| {
53 | // Esc - Quit application
54 | if i.key_pressed(egui::Key::Escape) && !self.state.show_registry_dialog {
55 | std::process::exit(0);
56 | }
57 |
58 | // F5 - Open registry login dialog
59 | if i.key_pressed(egui::Key::F5) && self.screen == Screen::SelectImage {
60 | self.state.show_registry_dialog = true;
61 | }
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/goldboot/src/gui/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod app;
2 | pub mod resources;
3 | pub mod screens;
4 | pub mod state;
5 | pub mod theme;
6 | pub mod widgets;
7 |
8 | use std::process::ExitCode;
9 |
10 | pub fn run_gui(fullscreen: bool) -> ExitCode {
11 | let native_options = eframe::NativeOptions {
12 | viewport: egui::ViewportBuilder::default()
13 | .with_inner_size([1920.0, 1080.0])
14 | .with_fullscreen(fullscreen)
15 | .with_decorations(!fullscreen)
16 | .with_title("goldboot"),
17 | ..Default::default()
18 | };
19 |
20 | match eframe::run_native(
21 | "goldboot",
22 | native_options,
23 | Box::new(|cc| Ok(Box::new(app::GuiApp::new(cc)))),
24 | ) {
25 | Ok(_) => ExitCode::SUCCESS,
26 | Err(e) => {
27 | eprintln!("GUI error: {}", e);
28 | ExitCode::FAILURE
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/goldboot/src/gui/resources.rs:
--------------------------------------------------------------------------------
1 | use image::ImageReader;
2 | use std::io::Cursor;
3 |
4 | pub const LOGO_BYTES: &[u8] = include_bytes!("resources/logo-512.png");
5 | pub const ICON_HDD: &[u8] = include_bytes!("resources/icons/hdd.png");
6 | pub const ICON_SSD: &[u8] = include_bytes!("resources/icons/ssd.png");
7 | pub const ICON_NVME: &[u8] = include_bytes!("resources/icons/nvme.png");
8 | pub const ICON_RAM: &[u8] = include_bytes!("resources/icons/ram.png");
9 |
10 | pub fn load_image_from_bytes(bytes: &[u8]) -> Result {
11 | let image = ImageReader::new(Cursor::new(bytes))
12 | .with_guessed_format()
13 | .map_err(|e| format!("Failed to guess format: {}", e))?
14 | .decode()
15 | .map_err(|e| format!("Failed to decode: {}", e))?;
16 |
17 | let size = [image.width() as usize, image.height() as usize];
18 | let rgba = image.to_rgba8();
19 | let pixels = rgba.as_flat_samples();
20 |
21 | Ok(egui::ColorImage::from_rgba_unmultiplied(
22 | size,
23 | pixels.as_slice(),
24 | ))
25 | }
26 |
27 | pub struct TextureCache {
28 | pub logo: egui::TextureHandle,
29 | pub icon_hdd: egui::TextureHandle,
30 | pub icon_ssd: egui::TextureHandle,
31 | pub icon_nvme: egui::TextureHandle,
32 | pub icon_ram: egui::TextureHandle,
33 | }
34 |
35 | impl TextureCache {
36 | pub fn new(ctx: &egui::Context) -> Self {
37 | Self {
38 | logo: ctx.load_texture(
39 | "logo",
40 | load_image_from_bytes(LOGO_BYTES).expect("Failed to load logo"),
41 | Default::default(),
42 | ),
43 | icon_hdd: ctx.load_texture(
44 | "icon_hdd",
45 | load_image_from_bytes(ICON_HDD).expect("Failed to load HDD icon"),
46 | Default::default(),
47 | ),
48 | icon_ssd: ctx.load_texture(
49 | "icon_ssd",
50 | load_image_from_bytes(ICON_SSD).expect("Failed to load SSD icon"),
51 | Default::default(),
52 | ),
53 | icon_nvme: ctx.load_texture(
54 | "icon_nvme",
55 | load_image_from_bytes(ICON_NVME).expect("Failed to load NVME icon"),
56 | Default::default(),
57 | ),
58 | icon_ram: ctx.load_texture(
59 | "icon_ram",
60 | load_image_from_bytes(ICON_RAM).expect("Failed to load RAM icon"),
61 | Default::default(),
62 | ),
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/goldboot/src/gui/resources/icons/hdd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/gui/resources/icons/hdd.png
--------------------------------------------------------------------------------
/goldboot/src/gui/resources/icons/nvme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/gui/resources/icons/nvme.png
--------------------------------------------------------------------------------
/goldboot/src/gui/resources/icons/ram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/gui/resources/icons/ram.png
--------------------------------------------------------------------------------
/goldboot/src/gui/resources/icons/ssd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/gui/resources/icons/ssd.png
--------------------------------------------------------------------------------
/goldboot/src/gui/resources/logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fossable/goldboot/HEAD/goldboot/src/gui/resources/logo-512.png
--------------------------------------------------------------------------------
/goldboot/src/gui/screens/confirm.rs:
--------------------------------------------------------------------------------
1 | use super::super::{resources::TextureCache, state::AppState, theme::Theme, widgets};
2 | use super::Screen;
3 |
4 | pub fn render(
5 | ui: &mut egui::Ui,
6 | state: &mut AppState,
7 | textures: &TextureCache,
8 | theme: &Theme,
9 | screen: &mut Screen,
10 | ) {
11 | ui.vertical(|ui| {
12 | // Header with logo
13 | widgets::header::render(ui, textures, theme);
14 |
15 | // Warning
16 | ui.vertical_centered(|ui| {
17 | ui.label(
18 | egui::RichText::new("Are you sure?")
19 | .color(theme.text_secondary)
20 | .strong()
21 | .size(16.0),
22 | );
23 | });
24 |
25 | ui.add_space(20.0);
26 |
27 | // Progress bar (400px wide as per GTK, centered)
28 | ui.vertical_centered(|ui| {
29 | let progress_text = format!("{}%", (state.confirm_progress * 100.0) as i32);
30 |
31 | let progress_bar = egui::ProgressBar::new(state.confirm_progress)
32 | .show_percentage()
33 | .text(progress_text);
34 |
35 | ui.add_sized([400.0, 20.0], progress_bar);
36 |
37 | ui.add_space(10.0);
38 |
39 | ui.label(
40 | egui::RichText::new("Press Enter 100 times to confirm")
41 | .color(theme.text_secondary)
42 | .size(12.0),
43 | );
44 | });
45 |
46 | // Check for Enter key press
47 | if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
48 | state.confirm_progress += 0.01;
49 | if state.confirm_progress >= 1.0 {
50 | state.confirm_progress = 1.0;
51 |
52 | // Initialize write progress for demo (10GB image)
53 | // TODO: Use actual image size from selected_image
54 | state.init_write_progress(10 * 1024 * 1024 * 1024);
55 |
56 | // Navigate to ApplyImage screen
57 | *screen = Screen::ApplyImage;
58 | }
59 | }
60 |
61 | ui.add_space(20.0);
62 |
63 | // Hotkeys footer
64 | let hotkeys = vec![("Esc", "Quit"), ("Enter", "Confirm (hold)")];
65 | widgets::hotkeys::render(ui, &hotkeys, theme);
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/goldboot/src/gui/screens/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod apply_image;
2 | pub mod confirm;
3 | pub mod registry_login;
4 | pub mod select_device;
5 | pub mod select_image;
6 |
7 | use super::{resources::TextureCache, state::AppState, theme::Theme};
8 |
9 | #[derive(Debug, Clone, PartialEq)]
10 | pub enum Screen {
11 | SelectImage,
12 | SelectDevice,
13 | Confirm,
14 | ApplyImage,
15 | }
16 |
17 | impl Screen {
18 | pub fn render(
19 | &mut self,
20 | ui: &mut egui::Ui,
21 | state: &mut AppState,
22 | textures: &TextureCache,
23 | theme: &Theme,
24 | ) {
25 | match self {
26 | Screen::SelectImage => select_image::render(ui, state, textures, theme, self),
27 | Screen::SelectDevice => select_device::render(ui, state, textures, theme, self),
28 | Screen::Confirm => confirm::render(ui, state, textures, theme, self),
29 | Screen::ApplyImage => apply_image::render(ui, state, textures, theme, self),
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/goldboot/src/gui/screens/registry_login.rs:
--------------------------------------------------------------------------------
1 | use super::super::{state::AppState, theme::Theme};
2 |
3 | pub fn render(ctx: &egui::Context, state: &mut AppState, theme: &Theme) {
4 | if !state.show_registry_dialog {
5 | return;
6 | }
7 |
8 | egui::Window::new("Registry Login")
9 | .collapsible(false)
10 | .resizable(false)
11 | .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
12 | .show(ctx, |ui| {
13 | ui.label("Registry Address:");
14 | ui.text_edit_singleline(&mut state.registry_address);
15 |
16 | ui.add_space(10.0);
17 |
18 | ui.label("Password:");
19 | let password_edit = egui::TextEdit::singleline(&mut state.registry_password)
20 | .password(true);
21 | ui.add(password_edit);
22 |
23 | ui.add_space(20.0);
24 |
25 | ui.horizontal(|ui| {
26 | if ui.button("Login").clicked() {
27 | // TODO: Implement registry login
28 | state.show_registry_dialog = false;
29 | }
30 |
31 | if ui.button("Cancel").clicked() {
32 | state.show_registry_dialog = false;
33 | }
34 | });
35 | });
36 |
37 | // Check for Escape key to close dialog
38 | if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
39 | state.show_registry_dialog = false;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/goldboot/src/gui/screens/select_device.rs:
--------------------------------------------------------------------------------
1 | use super::super::{resources::TextureCache, state::AppState, theme::Theme, widgets};
2 | use super::Screen;
3 | use ubyte::ToByteUnit;
4 |
5 | pub fn render(
6 | ui: &mut egui::Ui,
7 | state: &mut AppState,
8 | textures: &TextureCache,
9 | theme: &Theme,
10 | screen: &mut Screen,
11 | ) {
12 | // Load devices if not already loaded
13 | if state.devices.is_empty() {
14 | if let Ok(block_devices) = block_utils::get_block_devices() {
15 | if let Ok(devices) = block_utils::get_all_device_info(block_devices) {
16 | state.devices = devices;
17 | }
18 | }
19 | }
20 |
21 | ui.vertical(|ui| {
22 | // Header with logo
23 | widgets::header::render(ui, textures, theme);
24 |
25 | // Warning prompt
26 | ui.vertical_centered(|ui| {
27 | ui.label(
28 | egui::RichText::new("Select a device below to OVERWRITE")
29 | .color(theme.text_secondary)
30 | .strong()
31 | .size(16.0),
32 | );
33 | });
34 |
35 | ui.add_space(10.0);
36 |
37 | // Device list with horizontal margins (100px as per GTK)
38 | ui.horizontal(|ui| {
39 | ui.add_space(100.0);
40 |
41 | egui::ScrollArea::vertical()
42 | .auto_shrink([false, false])
43 | .show(ui, |ui| {
44 | ui.push_id("device_list", |ui| {
45 | let available_width = ui.available_width() - 200.0;
46 |
47 | egui::Frame::none()
48 | .stroke(egui::Stroke::new(3.0, theme.border.linear_multiply(0.75)))
49 | .fill(theme.list_bg)
50 | .inner_margin(15.0)
51 | .show(ui, |ui| {
52 | ui.set_width(available_width);
53 |
54 | if state.devices.is_empty() {
55 | ui.label(
56 | egui::RichText::new("No devices found")
57 | .color(theme.text_secondary),
58 | );
59 | } else {
60 | for device in state.devices.iter() {
61 | let is_selected =
62 | state.selected_device.as_ref() == Some(&device.name);
63 |
64 | let response = ui.horizontal(|ui| {
65 | ui.add_space(5.0);
66 |
67 | // Device icon (32x32)
68 | let icon = match device.media_type {
69 | block_utils::MediaType::SolidState => &textures.icon_ssd,
70 | block_utils::MediaType::Rotational => &textures.icon_hdd,
71 | block_utils::MediaType::NVME => &textures.icon_nvme,
72 | block_utils::MediaType::Ram => &textures.icon_ram,
73 | _ => &textures.icon_hdd, // Fallback
74 | };
75 |
76 | ui.add(egui::Image::new(icon).max_width(32.0));
77 | ui.add_space(5.0);
78 |
79 | // Device name and serial
80 | let device_label = if let Some(serial) = &device.serial_number {
81 | format!("{} ({})", device.name, serial)
82 | } else {
83 | device.name.clone()
84 | };
85 |
86 | ui.label(
87 | egui::RichText::new(device_label)
88 | .color(theme.text_primary),
89 | );
90 |
91 | ui.with_layout(
92 | egui::Layout::right_to_left(egui::Align::Center),
93 | |ui| {
94 | ui.add_space(5.0);
95 |
96 | // Device capacity
97 | ui.label(
98 | egui::RichText::new(device.capacity.bytes().to_string())
99 | .color(theme.text_primary),
100 | );
101 | },
102 | );
103 | });
104 |
105 | let response = response.response.interact(egui::Sense::click());
106 |
107 | if response.clicked() {
108 | state.selected_device = Some(device.name.clone());
109 | // Navigate to Confirm screen
110 | *screen = Screen::Confirm;
111 | }
112 |
113 | if response.hovered() {
114 | ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
115 | }
116 |
117 | // Check for Enter key to select
118 | if is_selected && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
119 | *screen = Screen::Confirm;
120 | }
121 |
122 | ui.add_space(5.0);
123 | }
124 | }
125 | });
126 | });
127 | });
128 |
129 | ui.add_space(100.0);
130 | });
131 |
132 | ui.add_space(20.0);
133 |
134 | // Hotkeys footer
135 | let hotkeys = vec![("Esc", "Quit"), ("Enter", "Overwrite")];
136 | widgets::hotkeys::render(ui, &hotkeys, theme);
137 | });
138 | }
139 |
--------------------------------------------------------------------------------
/goldboot/src/gui/screens/select_image.rs:
--------------------------------------------------------------------------------
1 | use super::super::{resources::TextureCache, state::AppState, theme::Theme, widgets};
2 | use super::Screen;
3 | use ubyte::ToByteUnit;
4 |
5 | pub fn render(
6 | ui: &mut egui::Ui,
7 | state: &mut AppState,
8 | textures: &TextureCache,
9 | theme: &Theme,
10 | screen: &mut Screen,
11 | ) {
12 | ui.vertical(|ui| {
13 | // Header with logo
14 | widgets::header::render(ui, textures, theme);
15 |
16 | // Prompt
17 | ui.vertical_centered(|ui| {
18 | ui.label(
19 | egui::RichText::new("Select an available image below")
20 | .color(theme.text_secondary)
21 | .strong()
22 | .size(16.0),
23 | );
24 | });
25 |
26 | ui.add_space(10.0);
27 |
28 | // Image list with horizontal margins (100px as per GTK)
29 | ui.horizontal(|ui| {
30 | ui.add_space(100.0);
31 |
32 | egui::ScrollArea::vertical()
33 | .auto_shrink([false, false])
34 | .show(ui, |ui| {
35 | ui.push_id("image_list", |ui| {
36 | let available_width = ui.available_width() - 200.0; // Account for margins
37 |
38 | egui::Frame::none()
39 | .stroke(egui::Stroke::new(3.0, theme.border.linear_multiply(0.75)))
40 | .fill(theme.list_bg)
41 | .inner_margin(15.0)
42 | .show(ui, |ui| {
43 | ui.set_width(available_width);
44 |
45 | if state.images.is_empty() {
46 | ui.label(
47 | egui::RichText::new("No images found")
48 | .color(theme.text_secondary),
49 | );
50 | } else {
51 | for image in state.images.iter() {
52 | let is_selected =
53 | state.selected_image.as_ref() == Some(&image.id);
54 |
55 | let response = ui.horizontal(|ui| {
56 | ui.add_space(5.0);
57 |
58 | // Image name
59 | ui.label(
60 | egui::RichText::new(image.primary_header.name())
61 | .color(theme.text_primary),
62 | );
63 |
64 | ui.add_space(20.0);
65 |
66 | // Image path
67 | ui.label(
68 | egui::RichText::new(image.path.to_string_lossy())
69 | .color(theme.text_primary),
70 | );
71 |
72 | ui.with_layout(
73 | egui::Layout::right_to_left(egui::Align::Center),
74 | |ui| {
75 | ui.add_space(5.0);
76 |
77 | // Image size
78 | ui.label(
79 | egui::RichText::new(
80 | image.primary_header.size.bytes().to_string(),
81 | )
82 | .color(theme.text_primary),
83 | );
84 | },
85 | );
86 | });
87 |
88 | let response = response.response.interact(egui::Sense::click());
89 |
90 | if response.clicked() {
91 | state.selected_image = Some(image.id.clone());
92 | // Navigate to SelectDevice screen
93 | *screen = Screen::SelectDevice;
94 | }
95 |
96 | if response.hovered() {
97 | ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
98 | }
99 |
100 | // Check for Enter key to select
101 | if is_selected && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
102 | *screen = Screen::SelectDevice;
103 | }
104 |
105 | ui.add_space(5.0);
106 | }
107 | }
108 | });
109 | });
110 | });
111 |
112 | ui.add_space(100.0);
113 | });
114 |
115 | ui.add_space(20.0);
116 |
117 | // Hotkeys footer
118 | let hotkeys = vec![
119 | ("Esc", "Quit"),
120 | ("F5", "Registry Login"),
121 | ("Enter", "Select Image"),
122 | ];
123 | widgets::hotkeys::render(ui, &hotkeys, theme);
124 | });
125 | }
126 |
--------------------------------------------------------------------------------
/goldboot/src/gui/state.rs:
--------------------------------------------------------------------------------
1 | use goldboot_image::ImageHandle;
2 | use std::sync::{Arc, Mutex};
3 | use std::time::Instant;
4 |
5 | #[derive(Debug, Clone, Copy, PartialEq)]
6 | pub enum BlockState {
7 | Pending, // Not yet written
8 | Writing, // Currently being written
9 | Written, // Successfully written
10 | }
11 |
12 | pub struct WriteProgress {
13 | pub percentage: f32, // 0.0 to 1.0
14 | pub read_speed: f64, // Bytes per second
15 | pub write_speed: f64, // Bytes per second
16 | pub blocks_total: usize, // Total number of blocks
17 | pub blocks_written: usize, // Number of blocks written
18 | pub blocks_writing: usize, // Number of blocks currently being written
19 | pub block_states: Vec, // State of each block
20 | pub start_time: Instant,
21 | }
22 |
23 | impl WriteProgress {
24 | pub fn new(total_blocks: usize) -> Self {
25 | Self {
26 | percentage: 0.0,
27 | read_speed: 0.0,
28 | write_speed: 0.0,
29 | blocks_total: total_blocks,
30 | blocks_written: 0,
31 | blocks_writing: 0,
32 | block_states: vec![BlockState::Pending; total_blocks],
33 | start_time: Instant::now(),
34 | }
35 | }
36 |
37 | pub fn elapsed_seconds(&self) -> f64 {
38 | self.start_time.elapsed().as_secs_f64()
39 | }
40 | }
41 |
42 | pub struct AppState {
43 | // Image selection
44 | pub images: Vec,
45 | pub selected_image: Option,
46 |
47 | // Device selection
48 | pub devices: Vec,
49 | pub selected_device: Option,
50 |
51 | // Confirmation
52 | pub confirm_progress: f32, // 0.0 to 1.0
53 |
54 | // Registry login
55 | pub registry_address: String,
56 | pub registry_password: String,
57 | pub show_registry_dialog: bool,
58 |
59 | // Image writing - detailed progress tracking
60 | pub write_progress: Option>>,
61 | }
62 |
63 | impl AppState {
64 | pub fn new() -> Self {
65 | Self {
66 | images: crate::library::ImageLibrary::find_all().unwrap_or_default(),
67 | selected_image: None,
68 | devices: Vec::new(), // Loaded on-demand in select_device screen
69 | selected_device: None,
70 | confirm_progress: 0.0,
71 | registry_address: String::new(),
72 | registry_password: String::new(),
73 | show_registry_dialog: false,
74 | write_progress: None,
75 | }
76 | }
77 |
78 | /// Initialize write progress when entering ApplyImage screen
79 | pub fn init_write_progress(&mut self, total_size_bytes: u64) {
80 | // Calculate number of blocks (using 4MB blocks for visualization)
81 | const BLOCK_SIZE: u64 = 4 * 1024 * 1024; // 4MB blocks
82 | let total_blocks = ((total_size_bytes + BLOCK_SIZE - 1) / BLOCK_SIZE) as usize;
83 |
84 | // Cap at reasonable number for visualization (e.g., 1000 blocks max)
85 | let total_blocks = total_blocks.min(1000);
86 |
87 | self.write_progress = Some(Arc::new(Mutex::new(WriteProgress::new(total_blocks))));
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/goldboot/src/gui/theme.rs:
--------------------------------------------------------------------------------
1 | use egui::{Color32, Style, Visuals};
2 |
3 | pub struct Theme {
4 | pub bg_primary: Color32, // #333333
5 | pub bg_grid: Color32, // #4a4a4a
6 | pub accent_gold: Color32, // #c8ab37
7 | pub text_primary: Color32, // #ffffff (white)
8 | pub text_secondary: Color32, // #aea79f (beige)
9 | pub list_bg: Color32, // #333333 with 0.75 opacity
10 | pub border: Color32, // #c8ab37
11 | }
12 |
13 | impl Default for Theme {
14 | fn default() -> Self {
15 | Self {
16 | bg_primary: Color32::from_rgb(0x33, 0x33, 0x33),
17 | bg_grid: Color32::from_rgb(0x4a, 0x4a, 0x4a),
18 | accent_gold: Color32::from_rgb(0xc8, 0xab, 0x37),
19 | text_primary: Color32::WHITE,
20 | text_secondary: Color32::from_rgb(0xae, 0xa7, 0x9f),
21 | list_bg: Color32::from_rgba_unmultiplied(0x33, 0x33, 0x33, 191),
22 | border: Color32::from_rgb(0xc8, 0xab, 0x37),
23 | }
24 | }
25 | }
26 |
27 | impl Theme {
28 | pub fn apply_to_context(&self, ctx: &egui::Context) {
29 | let mut style = Style::default();
30 | style.visuals = Visuals::dark();
31 | style.visuals.override_text_color = Some(self.text_primary);
32 | style.visuals.selection.bg_fill = self.accent_gold;
33 | style.visuals.selection.stroke.color = self.accent_gold;
34 | style.visuals.widgets.noninteractive.bg_stroke.color = self.border;
35 | ctx.set_style(style);
36 | }
37 |
38 | pub fn render_background(&self, ctx: &egui::Context) {
39 | let painter = ctx.layer_painter(egui::LayerId::background());
40 | let rect = ctx.screen_rect();
41 |
42 | // Fill with primary background color
43 | painter.rect_filled(rect, 0.0, self.bg_primary);
44 |
45 | // Draw 80x80px grid
46 | let grid_size = 80.0;
47 | let stroke = egui::Stroke::new(1.0, self.bg_grid);
48 |
49 | // Vertical lines
50 | let mut x = (rect.min.x / grid_size).floor() * grid_size;
51 | while x <= rect.max.x {
52 | painter.line_segment(
53 | [egui::pos2(x, rect.min.y), egui::pos2(x, rect.max.y)],
54 | stroke,
55 | );
56 | x += grid_size;
57 | }
58 |
59 | // Horizontal lines
60 | let mut y = (rect.min.y / grid_size).floor() * grid_size;
61 | while y <= rect.max.y {
62 | painter.line_segment(
63 | [egui::pos2(rect.min.x, y), egui::pos2(rect.max.x, y)],
64 | stroke,
65 | );
66 | y += grid_size;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/goldboot/src/gui/widgets/header.rs:
--------------------------------------------------------------------------------
1 | use super::super::{resources::TextureCache, theme::Theme};
2 |
3 | pub fn render(ui: &mut egui::Ui, textures: &TextureCache, theme: &Theme) {
4 | ui.vertical_centered(|ui| {
5 | ui.add_space(20.0);
6 |
7 | // Display logo (512px wide as per GTK version)
8 | let logo_size = egui::vec2(512.0, textures.logo.size()[1] as f32 * (512.0 / textures.logo.size()[0] as f32));
9 | ui.add(egui::Image::new(&textures.logo).max_width(logo_size.x));
10 |
11 | ui.add_space(10.0);
12 |
13 | // Version info (debug builds only)
14 | #[cfg(debug_assertions)]
15 | {
16 | let version = env!("CARGO_PKG_VERSION");
17 | let git_hash = option_env!("GIT_HASH").unwrap_or("unknown");
18 | let build_date = option_env!("BUILD_DATE").unwrap_or("unknown");
19 |
20 | let version_text = format!("goldboot v{}-{} ({})", version, git_hash, build_date);
21 |
22 | ui.label(
23 | egui::RichText::new(version_text)
24 | .color(theme.text_secondary)
25 | .monospace(),
26 | );
27 |
28 | ui.add_space(10.0);
29 | }
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/goldboot/src/gui/widgets/hotkeys.rs:
--------------------------------------------------------------------------------
1 | use super::super::theme::Theme;
2 |
3 | pub fn render(ui: &mut egui::Ui, hotkeys: &[(&str, &str)], theme: &Theme) {
4 | ui.add_space(20.0);
5 |
6 | ui.horizontal(|ui| {
7 | ui.add_space(10.0);
8 |
9 | for (key, description) in hotkeys {
10 | ui.label(
11 | egui::RichText::new(format!("[{}] {}", key, description))
12 | .color(theme.text_secondary)
13 | .monospace()
14 | .strong(),
15 | );
16 |
17 | ui.add_space(20.0);
18 | }
19 | });
20 |
21 | ui.add_space(10.0);
22 | }
23 |
--------------------------------------------------------------------------------
/goldboot/src/gui/widgets/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod header;
2 | pub mod hotkeys;
3 |
--------------------------------------------------------------------------------
/goldboot/src/lib.rs:
--------------------------------------------------------------------------------
1 | use rand::Rng;
2 |
3 | use std::net::TcpListener;
4 |
5 | pub mod cli;
6 | pub mod config;
7 | pub mod builder;
8 | #[cfg(feature = "gui")]
9 | pub mod gui;
10 | pub mod library;
11 | pub mod registry;
12 |
13 | /// Build info
14 | pub mod built_info {
15 | include!(concat!(env!("OUT_DIR"), "/built.rs"));
16 | }
17 |
18 | /// Find a random open TCP port in the given range.
19 | pub fn find_open_port(lower: u16, upper: u16) -> u16 {
20 | loop {
21 | let port = rand::rng().random_range(lower..upper);
22 | match TcpListener::bind(format!("0.0.0.0:{port}")) {
23 | Ok(_) => break port,
24 | Err(_) => continue,
25 | }
26 | }
27 | }
28 |
29 | /// Generate a random password
30 | pub fn random_password() -> String {
31 | // TODO check for a dictionary to generate something memorable
32 |
33 | // Fallback to random letters and numbers
34 | rand::rng()
35 | .sample_iter(&rand::distr::Alphanumeric)
36 | .take(12)
37 | .map(char::from)
38 | .collect()
39 | }
40 |
41 | #[cfg(test)]
42 | mod tests {
43 | use super::*;
44 |
45 | #[test]
46 | fn test_find_open_port() {
47 | let port = find_open_port(9000, 9999);
48 |
49 | assert!(port < 9999);
50 | assert!(port >= 9000);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/goldboot/src/library.rs:
--------------------------------------------------------------------------------
1 | use crate::{cli::progress::ProgressBar, builder::os::Os};
2 | use anyhow::{Result, anyhow, bail};
3 | use goldboot_image::ImageHandle;
4 | use rand::Rng;
5 | use sha1::Digest;
6 | use sha2::Sha256;
7 | use std::{
8 | fs::File,
9 | path::{Path, PathBuf},
10 | };
11 | use tracing::info;
12 |
13 | /// Represents the local image library.
14 | ///
15 | /// Depending on the platform, the directory will be located at:
16 | /// - /var/lib/goldboot/images (linux)
17 | ///
18 | /// Images are named according to their SHA256 hash (ID) and have a file
19 | /// extension of ".gb".
20 | pub struct ImageLibrary {
21 | pub directory: PathBuf,
22 | }
23 |
24 | impl ImageLibrary {
25 | pub fn open() -> Self {
26 | let directory = if cfg!(target_os = "linux") {
27 | PathBuf::from("/var/lib/goldboot/images")
28 | } else if cfg!(target_os = "macos") {
29 | PathBuf::from("/var/lib/goldboot/images")
30 | } else {
31 | panic!("Unsupported platform");
32 | };
33 |
34 | std::fs::create_dir_all(&directory).expect("failed to create image library");
35 | ImageLibrary { directory }
36 | }
37 |
38 | pub fn temporary(&self) -> PathBuf {
39 | let name: String = rand::rng()
40 | .sample_iter(&rand::distr::Alphanumeric)
41 | .take(12)
42 | .map(char::from)
43 | .collect();
44 | self.directory.join(name)
45 | }
46 |
47 | /// Add an image to the library. The image will be hashed and copied to the
48 | /// library with the appropriate name.
49 | pub fn add_copy(&self, image_path: impl AsRef) -> Result<()> {
50 | info!("Saving image to library");
51 |
52 | let mut hasher = Sha256::new();
53 | ProgressBar::Hash.copy(
54 | &mut File::open(&image_path)?,
55 | &mut hasher,
56 | std::fs::metadata(&image_path)?.len(),
57 | )?;
58 | let hash = hex::encode(hasher.finalize());
59 |
60 | std::fs::copy(&image_path, self.directory.join(format!("{hash}.gb")))?;
61 | Ok(())
62 | }
63 |
64 | /// Add an image to the library. The image will be hashed and moved to the
65 | /// library with the appropriate name.
66 | pub fn add_move(&self, image_path: impl AsRef) -> Result<()> {
67 | info!("Saving image to library");
68 |
69 | let mut hasher = Sha256::new();
70 | ProgressBar::Hash.copy(
71 | &mut File::open(&image_path)?,
72 | &mut hasher,
73 | std::fs::metadata(&image_path)?.len(),
74 | )?;
75 | let hash = hex::encode(hasher.finalize());
76 |
77 | std::fs::rename(&image_path, self.directory.join(format!("{hash}.gb")))?;
78 | Ok(())
79 | }
80 |
81 | /// Remove an image from the library by ID.
82 | pub fn delete(&self, image_id: &str) -> Result<()> {
83 | for p in self.directory.read_dir()? {
84 | let path = p?.path();
85 | let filename = path.file_name().unwrap().to_str().unwrap();
86 |
87 | if filename == format!("{image_id}.gb")
88 | || filename == format!("{}.gb", &image_id[0..12])
89 | {
90 | std::fs::remove_file(path)?;
91 | return Ok(());
92 | }
93 | }
94 |
95 | Ok(())
96 | }
97 |
98 | /// Download a goldboot image over HTTP.
99 | pub fn download(&self, url: String) -> Result {
100 | let path = self.directory.join("goldboot-uki.gb");
101 |
102 | let mut rs = reqwest::blocking::get(&url)?;
103 | if rs.status().is_success() {
104 | let length = rs
105 | .content_length()
106 | .ok_or_else(|| anyhow!("Failed to get content length"))?;
107 |
108 | let mut file = File::create(&path)?;
109 |
110 | info!("Saving goldboot image");
111 | ProgressBar::Download.copy(&mut rs, &mut file, length)?;
112 | ImageHandle::open(&path)
113 | } else {
114 | bail!("Failed to download");
115 | }
116 | }
117 |
118 | /// Find images in the library by ID.
119 | pub fn find_by_id(image_id: &str) -> Result {
120 | Ok(Self::find_all()?
121 | .into_iter()
122 | .find(|image| image.id == image_id || image.id[0..12] == image_id[0..12])
123 | .ok_or_else(|| anyhow!("Image not found"))?)
124 | }
125 |
126 | /// Find images in the library that have the given OS.
127 | pub fn find_by_os(os: &str) -> Result> {
128 | Ok(Self::find_all()?
129 | .into_iter()
130 | .filter(|image| {
131 | image
132 | .primary_header
133 | .elements
134 | .iter()
135 | .any(|element| element.os() == os)
136 | })
137 | .collect())
138 | }
139 |
140 | /// Find all images present in the local image library.
141 | pub fn find_all() -> Result> {
142 | let mut images = Vec::new();
143 |
144 | for p in Self::open().directory.read_dir()? {
145 | let path = p?.path();
146 |
147 | if let Some(ext) = path.extension() {
148 | if ext == "gb" {
149 | images.push(ImageHandle::open(&path)?);
150 | }
151 | }
152 | }
153 |
154 | Ok(images)
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/goldboot/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use goldboot::cli::cmd::Commands;
3 | use std::process::ExitCode;
4 |
5 | #[cfg(not(feature = "uki"))]
6 | use std::env;
7 |
8 | #[derive(Parser, Debug)]
9 | #[clap(author, version, about, long_about = None)]
10 | struct CommandLine {
11 | #[clap(subcommand)]
12 | command: Option,
13 |
14 | /// Run the GUI in fullscreen mode
15 | #[cfg(feature = "gui")]
16 | #[clap(long, num_args = 0)]
17 | fullscreen: bool,
18 | }
19 |
20 | /// Determine whether builds should be headless or not for debugging.
21 | pub fn build_headless_debug() -> bool {
22 | if env::var("CI").is_ok() {
23 | return true;
24 | }
25 | if env::var("GOLDBOOT_DEBUG").is_ok() {
26 | return false;
27 | }
28 | return true;
29 | }
30 |
31 | pub fn main() -> ExitCode {
32 | // UKI mode: Run fullscreen GUI with automatic environment checks and reboot
33 | #[cfg(feature = "uki")]
34 | return uki_main();
35 |
36 | // Parse command line options before we configure logging so we can set the
37 | // default level
38 | #[cfg(not(feature = "uki"))]
39 | let command_line = CommandLine::parse();
40 |
41 | // Configure logging
42 | #[cfg(not(feature = "uki"))]
43 | {
44 | let _default_filter = match &command_line.command {
45 | Some(Commands::Build { debug, .. }) => {
46 | if *debug {
47 | "debug"
48 | } else {
49 | "info"
50 | }
51 | }
52 | _ => "info",
53 | };
54 |
55 | tracing_subscriber::fmt()
56 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
57 | .init();
58 | }
59 |
60 | // Dispatch command
61 | #[cfg(not(feature = "uki"))]
62 | match &command_line.command {
63 | Some(Commands::Init { .. }) => goldboot::cli::cmd::init::run(command_line.command.unwrap()),
64 | Some(Commands::Build { .. }) => {
65 | goldboot::cli::cmd::build::run(command_line.command.unwrap())
66 | }
67 | Some(Commands::Image { .. }) => {
68 | goldboot::cli::cmd::image::run(command_line.command.unwrap())
69 | }
70 | Some(Commands::Registry { .. }) => {
71 | goldboot::cli::cmd::registry::run(command_line.command.unwrap())
72 | }
73 | Some(Commands::Deploy { .. }) => {
74 | goldboot::cli::cmd::deploy::run(command_line.command.unwrap())
75 | }
76 | Some(Commands::Liveusb { .. }) => {
77 | goldboot::cli::cmd::liveusb::run(command_line.command.unwrap())
78 | }
79 | None => {
80 | #[cfg(feature = "gui")]
81 | {
82 | return goldboot::gui::run_gui(command_line.fullscreen);
83 | }
84 |
85 | #[cfg(not(feature = "gui"))]
86 | {
87 | eprintln!("No command specified. Use --help for usage information.");
88 | eprintln!("Note: GUI requires building with --features gui");
89 | return ExitCode::FAILURE;
90 | }
91 | }
92 | }
93 | }
94 |
95 | #[cfg(feature = "uki")]
96 | fn uki_main() -> ExitCode {
97 | use tracing::info;
98 |
99 | // Initialize logging for UKI mode
100 | tracing_subscriber::fmt()
101 | .with_env_filter(
102 | tracing_subscriber::EnvFilter::from_default_env()
103 | .add_directive(tracing::Level::INFO.into()),
104 | )
105 | .init();
106 |
107 | info!("Starting goldboot in UKI mode");
108 |
109 | // Check environment
110 | if let Err(e) = check_uki_environment() {
111 | eprintln!("Environment check failed: {}", e);
112 | return ExitCode::FAILURE;
113 | }
114 |
115 | // Run GUI in fullscreen mode
116 | let result = goldboot::gui::run_gui(true);
117 |
118 | // After GUI exits, reboot the system
119 | info!("GUI exited, initiating system reboot");
120 | if let Err(e) = reboot_system() {
121 | eprintln!("Failed to reboot system: {}", e);
122 | return ExitCode::FAILURE;
123 | }
124 |
125 | result
126 | }
127 |
128 | #[cfg(feature = "uki")]
129 | fn check_uki_environment() -> Result<(), String> {
130 | // Verify we have access to block devices
131 | if !std::path::Path::new("/sys/class/block").exists() {
132 | return Err("Block device sysfs not available".to_string());
133 | }
134 |
135 | // Verify the image library directory exists
136 | let lib_path = std::path::Path::new("/var/lib/goldboot/images");
137 | if !lib_path.exists() {
138 | std::fs::create_dir_all(lib_path)
139 | .map_err(|e| format!("Failed to create library directory: {}", e))?;
140 | }
141 |
142 | Ok(())
143 | }
144 |
145 | #[cfg(feature = "uki")]
146 | fn reboot_system() -> Result<(), String> {
147 | use std::process::Command;
148 |
149 | // Try systemctl first (if systemd is available)
150 | let result = Command::new("systemctl")
151 | .arg("reboot")
152 | .status();
153 |
154 | if result.is_ok() {
155 | return Ok(());
156 | }
157 |
158 | // Fallback to direct reboot command
159 | Command::new("reboot")
160 | .status()
161 | .map_err(|e| format!("Failed to execute reboot: {}", e))?;
162 |
163 | Ok(())
164 | }
165 |
--------------------------------------------------------------------------------
/goldboot/src/pivot_root.rs:
--------------------------------------------------------------------------------
1 | use std::process::Command;
2 |
3 | /// Check that the current system is a candidate for pivoting.
4 | #[cfg(target_os = "linux")]
5 | pub fn check_pivot_root() -> bool {
6 | false
7 | }
8 |
9 | fn exec_silent(exe: &str, args: &[&str]) {
10 | Command::new(exe)
11 | .args(args)
12 | .status()
13 | .expect("Failed to exec");
14 | }
15 |
16 | /// Pivot the root mountpoint to a temporary directory so we can reimage the root
17 | /// partition on-the-fly. We have no intention of ever pivoting back, so it's OK
18 | /// to break stuff. Taken from: https://unix.stackexchange.com/a/227318.
19 | #[cfg(target_os = "linux")]
20 | pub fn pivot_root() {
21 |
22 | // Unmount everything we can
23 | exec_silent("umount", &["-a"]);
24 |
25 | // Create a temporary root
26 | exec_silent("mkdir", &["/tmp/tmproot"]);
27 | exec_silent("mount", &["-t", "tmpfs", "none", "/tmp/tmproot"]);
28 | exec_silent("mkdir", &["/tmp/tmproot/proc"]);
29 | exec_silent("mkdir", &["/tmp/tmproot/sys"]);
30 | exec_silent("mkdir", &["/tmp/tmproot/dev"]);
31 | exec_silent("mkdir", &["/tmp/tmproot/run"]);
32 | exec_silent("mkdir", &["/tmp/tmproot/usr"]);
33 | exec_silent("mkdir", &["/tmp/tmproot/var"]);
34 | exec_silent("mkdir", &["/tmp/tmproot/tmp"]);
35 | exec_silent("mkdir", &["/tmp/tmproot/oldroot"]);
36 | exec_silent("cp", &["-ax", "/bin", "/tmp/tmproot/"]);
37 | exec_silent("cp", &["-ax", "/etc", "/tmp/tmproot/"]);
38 | exec_silent("cp", &["-ax", "/sbin", "/tmp/tmproot/"]);
39 | exec_silent("cp", &["-ax", "/lib", "/tmp/tmproot/"]);
40 | exec_silent("cp", &["-ax", "/lib64", "/tmp/tmproot/"]);
41 |
42 | // Run pivot root
43 | exec_silent("mount", &["--make-rprivate", "/"]);
44 | exec_silent("pivot_root", &["/tmp/tmproot", "/tmp/tmproot/oldroot"]);
45 | exec_silent("mount", &["--move", "/oldroot/dev", "/dev"]);
46 | exec_silent("mount", &["--move", "/oldroot/proc", "/proc"]);
47 | exec_silent("mount", &["--move", "/oldroot/sys", "/sys"]);
48 | exec_silent("mount", &["--move", "/oldroot/run", "/run"]);
49 |
50 | // Clean up processes holding onto files in the old root
51 | exec_silent("systemctl", &["daemon-reexec"]);
52 | exec_silent("fuser", &["-mk", "/oldroot"]);
53 |
54 | // Lastly unmount the original root filesystem
55 | exec_silent("umount", &["/oldroot"]);
56 | }
--------------------------------------------------------------------------------
/goldboot/src/registry/api/image.rs:
--------------------------------------------------------------------------------
1 | use goldboot_image::{ImageArch, ImageHandle};
2 | use serde::{Deserialize, Serialize};
3 |
4 | #[derive(Serialize, Deserialize)]
5 | pub struct ImageInfoResponse {
6 | pub version: u8,
7 |
8 | /// The total size of all blocks combined in bytes
9 | pub size: u64,
10 |
11 | /// Image creation time
12 | pub timestamp: u64,
13 |
14 | /// A copy of the name field from the config
15 | pub name: String,
16 |
17 | /// System architecture
18 | pub arch: ImageArch,
19 | }
20 |
21 | impl From for ImageInfoResponse {
22 | fn from(value: ImageHandle) -> Self {
23 | Self {
24 | version: value.primary_header.version,
25 | size: value.primary_header.size,
26 | timestamp: value.primary_header.timestamp,
27 | name: todo!(),
28 | arch: value.primary_header.arch,
29 | }
30 | }
31 | }
32 |
33 | #[derive(Serialize, Deserialize)]
34 | pub struct ImageListResponse {
35 | pub results: Vec,
36 | }
37 |
--------------------------------------------------------------------------------
/goldboot/src/registry/api/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod image;
2 |
3 | pub struct RegistryTokenPermissions {
4 | // TODO
5 | }
6 |
7 | pub struct RegistryToken {
8 | /// The token value
9 | pub token: String,
10 |
11 | /// Whether the token value has been hashed with PBKDF2
12 | pub hashed: bool,
13 |
14 | /// Whether the token value has been encrypted with AES256
15 | pub encrypted: bool,
16 |
17 | /// A time-based second factor secret URL associated with the token
18 | pub totp_secret_url: Option,
19 |
20 | /// The expiration timestamp
21 | pub expiration: Option,
22 |
23 | /// The token's associated permissions
24 | pub permissions: RegistryTokenPermissions,
25 | }
26 |
--------------------------------------------------------------------------------
/goldboot/src/registry/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod api;
2 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | comment_width = 80
2 | imports_granularity = "Crate"
3 | reorder_imports = true
4 | unstable_features = true
5 | wrap_comments = true
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import { } }:
2 |
3 | with pkgs;
4 |
5 | mkShell rec {
6 | nativeBuildInputs = [ pkg-config cargo rustc rust-analyzer rustfmt clippy ];
7 | buildInputs = [ cmake libclang openssl udev pango gtk4 ];
8 | LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
9 | }
10 |
11 |
--------------------------------------------------------------------------------