From e2f70c736103d6a30f90120fb1681f5d59d7fdea Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:21:11 -0700 Subject: [PATCH 1/7] Fix image runtime dependencies, installer targets, and signature trust --- .github/workflows/build-disk.yml | 29 ++----- .github/workflows/build.yml | 20 +++-- .github/workflows/check.yml | 21 +++++ .gitignore | 3 + Containerfile | 1 + Justfile | 12 +-- README.md | 133 ++++++++++++++++++++----------- build_files/build.sh | 76 ++++++++++-------- build_files/configure-trust.py | 52 ++++++++++++ build_files/flutter-launcher.sh | 23 ++++++ build_files/smoke-test.sh | 35 ++++++++ disk_config/iso-gnome.toml | 4 +- disk_config/iso-kde.toml | 4 +- tests/test_helpers.py | 60 ++++++++++++++ 14 files changed, 354 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/check.yml create mode 100644 build_files/configure-trust.py create mode 100755 build_files/flutter-launcher.sh create mode 100755 build_files/smoke-test.sh create mode 100644 tests/test_helpers.py diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index d8bc599..6cf25f7 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -9,19 +9,12 @@ on: required: false default: false type: boolean - platform: - required: true - type: choice - options: - - amd64 - - arm64 pull_request: branches: - main paths: - - './disk_config/disk.toml' - - './disk_config/iso.toml' - - './.github/workflows/build-disk.yml' + - 'disk_config/**' + - '.github/workflows/build-disk.yml' env: IMAGE_NAME: ${{ github.event.repository.name }} # output of build.yml, keep in sync @@ -36,7 +29,7 @@ concurrency: jobs: build: name: Build disk images - runs-on: ${{ inputs.platform == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -60,16 +53,7 @@ jobs: echo "USER_UID=${USER_UID}" >> ${GITHUB_ENV} echo "USER_GID=${USER_GID}" >> ${GITHUB_ENV} - - name: Install dependencies - if: inputs.platform == 'arm64' - run: | - set -x - sudo apt update -y - sudo apt install -y \ - podman - - name: Maximize build space - if: inputs.platform != 'arm64' uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9 with: remove-codeql: true @@ -79,10 +63,10 @@ jobs: - name: Build disk images id: build - uses: osbuild/bootc-image-builder-action@main + uses: osbuild/bootc-image-builder-action@028e4d28330efe546229eb9cb52aee78174d2347 with: builder-image: ${{ env.BIB_IMAGE }} - config-file: ${{ matrix.disk-type == 'anaconda-iso' && './disk_config/iso.toml' || './disk_config/disk.toml' }} + config-file: ${{ matrix.disk-type == 'anaconda-iso' && './disk_config/iso-kde.toml' || 'disk_config/**' }} image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} chown: ${{ env.USER_UID }}:${{ env.USER_GID }} types: ${{ matrix.disk-type }} @@ -92,6 +76,7 @@ jobs: if: inputs.upload-to-s3 != true && github.event_name != 'pull_request' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: + name: tuxlinux-${{ matrix.disk-type }}-amd64 path: ${{ steps.build.outputs.output-directory }} if-no-files-found: error retention-days: 0 @@ -112,4 +97,4 @@ jobs: run: | sudo apt-get update sudo apt-get install -y rclone - rclone copy $SOURCE_DIR S3:${{ secrets.S3_BUCKET_NAME }} + rclone copy $SOURCE_DIR S3:${{ secrets.S3_BUCKET_NAME }}/${{ matrix.disk-type }}/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6074202..a5098cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ on: workflow_dispatch: env: - IMAGE_DESC: "My Customized Universal Blue Image" + IMAGE_DESC: "TuxLinux: Bazzite NVIDIA-open KDE with development tools" IMAGE_KEYWORDS: "bootc,ublue,universal-blue" IMAGE_LOGO_URL: "https://avatars.githubusercontent.com/u/120078124?s=200&v=4" # Put your own image here for a fancy profile on https://artifacthub.io/! IMAGE_NAME: "${{ github.event.repository.name }}" # output image name, usually same as repo name @@ -22,7 +22,7 @@ env: DEFAULT_TAG: "latest" concurrency: - group: ${{ github.workflow }}-${{ github.ref || github.run_id }}-${{ inputs.brand_name}}-${{ inputs.stream_name }} + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} cancel-in-progress: true jobs: @@ -83,7 +83,7 @@ jobs: type=raw,value=${{ env.DEFAULT_TAG }} type=raw,value=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}} type=raw,value={{date 'YYYYMMDD'}} - type=sha,enable=${{ github.event_name == 'pull_request' }} + type=sha type=ref,event=pr labels: | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md @@ -117,6 +117,13 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} oci: false + - name: Smoke test as an ordinary user with a read-only image + run: | + podman run --rm --read-only --user 1000:1000 \ + --tmpfs /tmp:rw,mode=1777 --env HOME=/tmp/home \ + --volume "$PWD/build_files/smoke-test.sh:/smoke-test.sh:ro" \ + --entrypoint /bin/bash "${IMAGE_NAME}:${DEFAULT_TAG}" /smoke-test.sh + # Rechunk is a script that we use on Universal Blue to make sure there isnt a single huge layer when your image gets published. # This does not make your image faster to download, just provides better resumability and fixes a few errors. # Documentation for Rechunk is provided on their github repository at https://github.com/hhd-dev/rechunk @@ -179,10 +186,9 @@ jobs: if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) run: | IMAGE_FULL="${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}" - for tag in ${{ steps.metadata.outputs.tags }}; do - cosign sign -y --key env://COSIGN_PRIVATE_KEY $IMAGE_FULL:$tag - done + cosign sign -y --key env://COSIGN_PRIVATE_KEY "$IMAGE_FULL@$IMAGE_DIGEST" + cosign verify --key cosign.pub "$IMAGE_FULL@$IMAGE_DIGEST" env: - TAGS: ${{ steps.push.outputs.digest }} + IMAGE_DIGEST: ${{ steps.push.outputs.digest }} COSIGN_EXPERIMENTAL: false COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..1eb4fbf --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,21 @@ +name: Validate source +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - run: | + bash -n build_files/build.sh + shellcheck build_files/*.sh + python3 -m unittest discover -s tests -v + python3 - <<'PY' + import pathlib, tomllib + for path in pathlib.Path('disk_config').glob('*.toml'): + tomllib.loads(path.read_text()) + PY diff --git a/.gitignore b/.gitignore index 9eee28b..0139107 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ cosign.key _build_* output _build-*/** + +__pycache__/ +*.pyc diff --git a/Containerfile b/Containerfile index 9f76ee6..45a0865 100644 --- a/Containerfile +++ b/Containerfile @@ -1,6 +1,7 @@ # Allow build scripts to be referenced without being copied into the final image FROM scratch AS ctx COPY build_files / +COPY cosign.pub /cosign.pub # Base Image FROM ghcr.io/ublue-os/bazzite-nvidia-open:stable diff --git a/Justfile b/Justfile index 976c4ee..b475520 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,4 @@ -export image_name := env("IMAGE_NAME", "TuxLinux") # output image name, usually same as repo name, change as needed +export image_name := env("IMAGE_NAME", "tuxlinux") # output image name, usually same as repo name, change as needed export default_tag := env("DEFAULT_TAG", "latest") export bib_image := env("BIB_IMAGE", "quay.io/centos-bootc/bootc-image-builder:latest") @@ -42,7 +42,7 @@ clean: rm -f previous.manifest.json rm -f changelog.md rm -f output.env - rm -f output/ + rm -rf -- output/ # Sudo Clean Repo [group('Utility')] @@ -141,7 +141,7 @@ _rootful_load_image $target_image=image_name $tag=default_tag: if [[ "$ID" != "$USER_IMG_ID" ]]; then # If the image ID is not found or different from user, copy the image from user podman to root podman COPYTMP=$(mktemp -p "${PWD}" -d -t _build_podman_scp.XXXXXXXXXX) - just sudoif TMPDIR=${COPYTMP} podman image scp ${UID}@localhost::"${target_image}:${tag}" root@localhost::"${target_image}:${tag}" + just sudoif env TMPDIR="${COPYTMP}" podman image scp ${UID}@localhost::"${target_image}:${tag}" root@localhost::"${target_image}:${tag}" rm -rf "${COPYTMP}" fi else @@ -207,7 +207,7 @@ build-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_build # Build an ISO virtual machine image [group('Build Virtal Machine Image')] -build-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "iso" "disk_config/iso.toml") +build-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "anaconda-iso" "disk_config/iso-kde.toml") # Rebuild a QCOW2 virtual machine image [group('Build Virtal Machine Image')] @@ -219,7 +219,7 @@ rebuild-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_reb # Rebuild an ISO virtual machine image [group('Build Virtal Machine Image')] -rebuild-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "iso" "disk_config/iso.toml") +rebuild-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "anaconda-iso" "disk_config/iso-kde.toml") # Run a virtual machine with the specified image type and configuration _run-vm $target_image $tag $type $config: @@ -273,7 +273,7 @@ run-vm-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_run- # Run a virtual machine from an ISO [group('Run Virtal Machine')] -run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "iso" "disk_config/iso.toml") +run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "anaconda-iso" "disk_config/iso-kde.toml") # Run a virtual machine using systemd-vmspawn [group('Run Virtal Machine')] diff --git a/README.md b/README.md index 50bea30..8a740a9 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,111 @@ # 🐧 TuxLinux -An immutable, atomic Linux distro built for work and play. Based on [Bazzite](https://bazzite.gg/) and powered by [bootc](https://github.com/bootc-dev/bootc). +A personal development and gaming image based on **Bazzite NVIDIA-open, KDE, +x86-64**. This is not a generic AMD/Intel image, an ARM image, or a Bazzite Deck +variant. Check [Bazzite's hardware guidance](https://docs.bazzite.gg/Gaming/Hardware_compatibility_for_gaming/) +before switching, especially for older NVIDIA GPUs. + +## What's included + +- Go, Rust/Cargo, Node.js/npm, Python/pip, and Fedora's current default JDK +- VS Code, Vim, WezTerm, tmux, Zsh, Starship, Git/GitHub CLI, and CLI utilities +- C/C++ build tools, Tauri/GTK/WebKitGTK, Qt6, Protobuf, PipeWire and GBM development dependencies +- Podman, podman-compose, Distrobox, Chromium, cloudflared, and Kvantum +- Steam and the graphics/gaming stack inherited from Bazzite +- Flutter 3.29.3 with its bundled Dart SDK, seeded from a checksum-verified archive + +Required packages must resolve during the build; missing dependencies fail CI. +Java 17/21 are not promised by this image: those packages are no longer available +in the current Fedora base. Use a project-specific container or JDK manager when +an older Java version is required. + +Flutter and Dart launchers extract an SDK on first use into +`${XDG_DATA_HOME:-$HOME/.local/share}/tuxlinux/flutter/`. This directory +is writable by the user, allowing cache downloads and tooling to function on an +immutable OS. Allow space for the extracted SDK in each user's home. New image +SDK versions use new directories; older SDKs are retained. IDEs should point at +that extracted SDK. Host rollback does not undo changes to the user's SDK. + +The system Podman socket is enabled for rootful workloads. Rootless containers +use their own user service; do not expose either socket to untrusted clients. + +## Installation and signature trust + +Back up personal data and retain a working deployment. Use `rpm-ostree status` +and `bootc status` to identify the current deployment and supported backend. +Switching images requires a reboot and does not roll back home or `/var` data. +Do not switch desktop environments or assume mutable Fedora supports rebasing. + +Images are signed with the repository's `cosign.pub`. Before the first switch, +obtain this repository at a reviewed commit and verify the public key through a +trusted copy/channel. Review the trust helper, then run it from the checkout: -TuxLinux ships a full developer toolchain, gaming-ready GPU support, and a curated set of tools — all baked into an atomic image that updates reliably and rolls back instantly. - -## What's Inside - -### Development -- **Languages:** Go, Rust, Node.js, Python, Java 17/21 -- **Editors:** VS Code, Vim -- **Terminal:** WezTerm, tmux, Zsh, Starship prompt -- **Tools:** Git, GitHub CLI, ripgrep, fzf, bat, eza, fd, jq, yq, htop, btop -- **Build:** GCC, C development tools, development-tools group -- **Tauri:** Full build deps (WebKitGTK, OpenSSL, GTK3, librsvg, pango) - -### Containers & Virtualization -- Podman + podman-compose -- Distrobox - -### Theming -- Kvantum - -### System -- Immutable base — OS updates are atomic images, not package-by-package -- Rollback on boot if anything breaks -- Podman socket enabled by default - -## Based On +```bash +sudo python3 build_files/configure-trust.py --key cosign.pub +``` -TuxLinux is built on top of [Bazzite](https://bazzite.gg/), which brings: -- AMD/NVIDIA GPU drivers out of the box -- Steam, Lutris, and gaming optimizations -- Wayland + KDE Plasma desktop -- Hardware-accelerated codecs +This merges a repository-scoped signature rule into `/etc/containers/policy.json`, +installs `/etc/pki/containers/tuxlinux.pub`, and enables signature discovery. +It preserves other registry rules. The image includes the same policy for future +updates. Existing installations using an unverified origin must explicitly switch +to a verified origin; merely installing the policy does not change that origin. -## Installation +For a compatible bootc-managed installation: -### Switch from an existing Bazzite/Fedora Atomic system: ```bash -sudo bootc switch ghcr.io/eosdev-x/tuxlinux +sudo bootc switch --enforce-container-sigpolicy ghcr.io/eosdev-x/tuxlinux:latest ``` -### Rebase from an existing Fedora Atomic system: +For an rpm-ostree-managed Fedora Atomic/Bazzite installation: + ```bash -rpm-ostree rebase ostree-unverified-registry:ghcr.io/eosdev-x/tuxlinux:latest +rpm-ostree rebase ostree-image-signed:docker://ghcr.io/eosdev-x/tuxlinux:latest ``` -Then reboot. +Use the command appropriate to your deployment, not both. Check staged status, +then reboot when ready. Secure Boot firmware/module keys are a separate matter +from image signatures; follow Bazzite's Secure Boot guide for your hardware. -## Building Locally +## Updates and recovery -```bash -just build -``` +The container workflow builds daily and on main-branch changes. Published tags +include `latest`, date tags, and a source SHA tag. Date tags can be replaced by +another build that day; use an image digest to identify an exact artifact. + +Use Bazzite's updater for ordinary updates, checking that the source remains +`ghcr.io/eosdev-x/tuxlinux`. Keep the previous deployment for recovery. A boot-menu +selection can boot an older deployment; a persistent rollback must be selected +with the appropriate deployment tool. Rollback restores the OS deployment, not +personal files, application databases, containers, or every mutable setting. + +## Building and validation -Or with Podman directly: ```bash +just build +# Or: podman build -t tuxlinux . + +python3 -m unittest discover -s tests -v +shellcheck build_files/*.sh ``` -## Customization +CI checks required tools/development metadata during the build, runs bootc lint, +and exercises Flutter as an ordinary user in a read-only container. It also +compiles against PipeWire/GBM headers. These checks do not replace a booted VM or +real NVIDIA hardware test. Test login, graphics, networking, update, and rollback +on a disposable deployment before relying on a release. + +`just build-iso` and the disk workflow use `disk_config/iso-kde.toml`. Only amd64 +is supported. The disk workflow consumes the published `latest` image; a PR disk +build checks installer configuration, not an unpublished PR container. Installer +post-install configuration retains the TuxLinux origin with signature enforcement. +QCOW2 and installer artifacts have separate names to prevent matrix overwrites. -All packages and customizations live in [`build_files/build.sh`](./build_files/build.sh). Edit that file to add or remove packages from the image. +Packages live in `build_files/build.sh`; launchers, trust setup, and smoke checks +live alongside it. Flutter and Starship versions/checksums must be updated +together from their official release metadata. Preserve the existing signing key +unless intentionally planning a key rotation for installed clients. ## License -MIT +Apache-2.0. See [LICENSE](LICENSE). Bundled software retains its own licenses. diff --git a/build_files/build.sh b/build_files/build.sh index f0b883c..70e7aeb 100755 --- a/build_files/build.sh +++ b/build_files/build.sh @@ -35,19 +35,19 @@ EOF dnf5 install -y @c-development @development-tools # Terminal & shell -dnf5 install -y tmux vim zsh fd-find zoxide wezterm --skip-unavailable +dnf5 install -y tmux vim zsh fd-find zoxide wezterm # Browsers (Chromium required for OpenClaw browser automation / CDP) -dnf5 install -y chromium --skip-unavailable +dnf5 install -y chromium # Editor -dnf5 install -y code --skip-unavailable +dnf5 install -y code # Version control & dev essentials -dnf5 install -y git git-lfs gh --skip-unavailable +dnf5 install -y git git-lfs gh # Languages & runtimes -dnf5 install -y golang rust cargo nodejs nodejs-full-i18n npm python3-pip java-17-openjdk-devel java-21-openjdk-devel qt6-qtbase-devel cmake --skip-unavailable +dnf5 install -y golang rust cargo nodejs nodejs-full-i18n npm python3-pip java-devel qt6-qtbase-devel cmake # Build tools & native deps (Tauri, Flutter, Rust bindings, Protobuf, Qt6) dnf5 install -y \ @@ -56,56 +56,62 @@ dnf5 install -y \ lld pkg-config \ protobuf-compiler protobuf-devel \ sqlite-devel ninja-build \ - qt6-qtdeclarative-devel qt6-qtsvg-devel qt6-qtshadertools-devel libgit2-devel qtkeychain-qt6-devel \ - --skip-unavailable - -# PipeWire & GBM headers (can't dnf install — Bazzite custom builds conflict with Fedora repos) -# Extract headers and pkg-config from matching RPM versions -PIPEWIRE_DEVEL_RPM="pipewire-devel-1.4.10-1.fc43.x86_64" -MESA_GBM_DEVEL_RPM="mesa-libgbm-devel-25.2.7-3.fc43.x86_64" -dnf5 download -y "${PIPEWIRE_DEVEL_RPM}" "${MESA_GBM_DEVEL_RPM}" --destdir=/tmp/rpm-extract 2>/dev/null || true -if [ -d /tmp/rpm-extract ]; then - for rpm_file in /tmp/rpm-extract/*.rpm; do - rpm2cpio "$rpm_file" | (cd / && cpio -idm 2>/dev/null) || true - done - rm -rf /tmp/rpm-extract -fi + qt6-qtdeclarative-devel qt6-qtsvg-devel qt6-qtshadertools-devel libgit2-devel qtkeychain-qt6-devel + +# Install development headers through RPM so versions and ownership stay coherent. +dnf5 install -y pipewire-devel mesa-libgbm-devel # Containers & virtualization -dnf5 install -y distrobox podman-compose --skip-unavailable +dnf5 install -y distrobox podman-compose # Utilities -dnf5 install -y htop btop ripgrep jq yq fzf bat eza curl tar gzip unzip zstd shellcheck --skip-unavailable +dnf5 install -y htop btop ripgrep jq yq fzf bat eza curl tar gzip unzip zstd shellcheck # Lazygit (needs COPR) dnf5 -y copr enable atim/lazygit -dnf5 install -y lazygit --skip-unavailable +dnf5 install -y lazygit dnf5 -y copr disable atim/lazygit # CLI tools (benchmarking, code stats, git diffs) -dnf5 install -y git-delta hyperfine yt-dlp --skip-unavailable +dnf5 install -y git-delta hyperfine yt-dlp # Theming -dnf5 install -y kvantum --skip-unavailable +dnf5 install -y kvantum -# Networking tools — cloudflared RPM post-install needs /usr/local/bin for symlink -install -d -m 0755 /usr/local/bin 2>/dev/null || true -dnf5 install -y cloudflared --skip-unavailable || true +# The vendor RPM's post-install creates a compatibility link in /usr/local/bin. +# /usr/local points into /var on Atomic systems; create it for the scriptlet. +install -d -m 0755 /var/usrlocal/bin +dnf5 install -y cloudflared +test -x /usr/bin/cloudflared -# Flutter SDK (not available in Fedora repos) +# Keep a verified SDK seed in the image. The launcher extracts a writable copy +# for each user/version; Flutter must not write into the immutable image. FLUTTER_VERSION="3.29.3" -curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" -o /tmp/flutter.tar.xz -tar -xf /tmp/flutter.tar.xz -C /opt/ -ln -sf /opt/flutter/bin/flutter /usr/bin/flutter -ln -sf /opt/flutter/bin/dart /usr/bin/dart -rm -f /tmp/flutter.tar.xz +FLUTTER_SHA256="8a908a5add53c1dfc2031da29e58daefd59a6d1d52fb5cb61f5ee52c73e36e15" +install -d /usr/share/tuxlinux +curl -fSL --retry 3 "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" -o /usr/share/tuxlinux/flutter.tar.xz +printf '%s %s\n' "$FLUTTER_SHA256" /usr/share/tuxlinux/flutter.tar.xz | sha256sum --check --strict +printf '%s\n' "$FLUTTER_VERSION" > /usr/share/tuxlinux/flutter-version +install -m 0755 /ctx/flutter-launcher.sh /usr/bin/flutter +ln -sf flutter /usr/bin/dart # Disable COPRs & cleanup repos dnf5 -y copr disable wezfurlong/wezterm-nightly rm -f /etc/yum.repos.d/cloudflared.repo -# Install Starship prompt -curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b /usr/bin +# Pin and verify the executable instead of executing a mutable installer. +STARSHIP_VERSION="1.26.0" +STARSHIP_SHA256="b7c232b0e8249d8e55a40beb79c5c43a7d370f3f9408bd215deb0170daeaadf3" +curl -fSL --retry 3 "https://github.com/starship/starship/releases/download/v${STARSHIP_VERSION}/starship-x86_64-unknown-linux-musl.tar.gz" -o /tmp/starship.tar.gz +printf '%s %s\n' "$STARSHIP_SHA256" /tmp/starship.tar.gz | sha256sum --check --strict +tar -xzf /tmp/starship.tar.gz -C /usr/bin starship +rm /tmp/starship.tar.gz + +# Preserve upstream registry policies and add our repository's verification key. +python3 /ctx/configure-trust.py --key /ctx/cosign.pub ### Enable services systemctl enable podman.socket + +# Fail the build if a promised tool or development interface is absent. +/ctx/smoke-test.sh --build diff --git a/build_files/configure-trust.py b/build_files/configure-trust.py new file mode 100644 index 0000000..2199e24 --- /dev/null +++ b/build_files/configure-trust.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Merge TuxLinux trust without replacing upstream registry policies.""" +import argparse +import json +import os +from pathlib import Path +import tempfile + + +def configure(root: Path, key: Path) -> None: + public_key = key.read_bytes() + if not public_key.startswith(b"-----BEGIN PUBLIC KEY-----\n"): + raise ValueError("Expected a PEM public key") + policy_path = root / "etc/containers/policy.json" + policy = json.loads(policy_path.read_text()) + policy.setdefault("transports", {}).setdefault("docker", {})[ + "ghcr.io/eosdev-x/tuxlinux" + ] = [{ + "type": "sigstoreSigned", + "keyPath": "/etc/pki/containers/tuxlinux.pub", + "signedIdentity": {"type": "matchRepository"}, + }] + key_path = root / "etc/pki/containers/tuxlinux.pub" + key_path.parent.mkdir(parents=True, exist_ok=True) + key_path.write_bytes(public_key) + key_path.chmod(0o644) + registry_path = root / "etc/containers/registries.d/tuxlinux.yaml" + registry_path.parent.mkdir(parents=True, exist_ok=True) + registry_path.write_text( + "docker:\n ghcr.io/eosdev-x/tuxlinux:\n use-sigstore-attachments: true\n" + ) + registry_path.chmod(0o644) + # Replace atomically so interruption cannot leave a truncated policy. + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", dir=policy_path.parent, delete=False) as stream: + temporary = Path(stream.name) + json.dump(policy, stream, indent=2) + stream.write("\n") + temporary.chmod(policy_path.stat().st_mode & 0o777) + os.replace(temporary, policy_path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path("/")) + parser.add_argument("--key", type=Path, required=True) + args = parser.parse_args() + configure(args.root, args.key) diff --git a/build_files/flutter-launcher.sh b/build_files/flutter-launcher.sh new file mode 100755 index 0000000..5117011 --- /dev/null +++ b/build_files/flutter-launcher.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +seed=/usr/share/tuxlinux +version=$(cat "$seed/flutter-version") +# A versioned directory preserves older SDKs for users who still need them. +sdk_parent="${XDG_DATA_HOME:-$HOME/.local/share}/tuxlinux/flutter" +sdk="$sdk_parent/$version" +mkdir -p "$sdk_parent" +( + flock -x 9 + if [[ ! -x "$sdk/bin/flutter" ]]; then + staging=$(mktemp -d "$sdk_parent/.extract.XXXXXXXX") + trap 'rm -rf -- "$staging"' EXIT + tar -xf "$seed/flutter.tar.xz" -C "$staging" --no-same-owner + mv -- "$staging/flutter" "$sdk" + fi +) 9>"$sdk_parent/.install.lock" + +case "$(basename "$0")" in + dart) exec "$sdk/bin/dart" "$@" ;; + *) exec "$sdk/bin/flutter" "$@" ;; +esac diff --git a/build_files/smoke-test.sh b/build_files/smoke-test.sh new file mode 100755 index 0000000..a545d97 --- /dev/null +++ b/build_files/smoke-test.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +for tool in git gh go rustc cargo node npm python3 pip3 java javac code \ + wezterm chromium cloudflared podman distrobox starship flutter dart \ + protoc cmake ninja pkg-config; do + command -v "$tool" +done +pkg-config --exists libpipewire-0.3 gbm webkit2gtk-4.1 openssl gtk+-3.0 sqlite3 +cloudflared --version +starship --version +javac -version +python3 - <<'PY' +import json +from pathlib import Path +policy = json.loads(Path('/etc/containers/policy.json').read_text()) +rule = policy['transports']['docker']['ghcr.io/eosdev-x/tuxlinux'][0] +assert rule['type'] == 'sigstoreSigned' +assert Path(rule['keyPath']).is_file() +PY + +if [[ ${1:-} != --build ]]; then + mkdir -p "$HOME" + # Run on a read-only root as a non-root user in CI, with disposable HOME. + flutter --version + dart --version + flutter config --no-analytics + flutter create --platforms=linux --no-pub /tmp/flutter-smoke + test -f /tmp/flutter-smoke/linux/CMakeLists.txt + # Compile/link representative headers against the actual image libraries. + printf '#include \n#include \nint main(void) { pw_init(0, 0); return gbm_create_device(-1) != 0; }\n' > /tmp/headers-smoke.c + # pkg-config deliberately emits separate compiler arguments. + # shellcheck disable=SC2046 + cc /tmp/headers-smoke.c $(pkg-config --cflags --libs libpipewire-0.3 gbm) -o /tmp/headers-smoke +fi diff --git a/disk_config/iso-gnome.toml b/disk_config/iso-gnome.toml index 0a9c84f..2dcbf6e 100644 --- a/disk_config/iso-gnome.toml +++ b/disk_config/iso-gnome.toml @@ -1,7 +1,7 @@ [customizations.installer.kickstart] contents = """ -%post -bootc switch --mutate-in-place --transport registry ghcr.io/ublue-os/image-template:latest +%post --erroronfail +bootc switch --mutate-in-place --enforce-container-sigpolicy --transport registry ghcr.io/eosdev-x/tuxlinux:latest %end """ diff --git a/disk_config/iso-kde.toml b/disk_config/iso-kde.toml index 17d2787..a9eb689 100644 --- a/disk_config/iso-kde.toml +++ b/disk_config/iso-kde.toml @@ -1,7 +1,7 @@ [customizations.installer.kickstart] contents = """ -%post -bootc switch --mutate-in-place --transport registry ghcr.io/ublue-os/image-template:latest +%post --erroronfail +bootc switch --mutate-in-place --enforce-container-sigpolicy --transport registry ghcr.io/eosdev-x/tuxlinux:latest %end """ diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..9fe8d76 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,60 @@ +import importlib.util +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +REPO = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('trust', REPO / 'build_files/configure-trust.py') +trust = importlib.util.module_from_spec(spec) +spec.loader.exec_module(trust) + + +class HelpersTest(unittest.TestCase): + def test_trust_preserves_other_registries_and_is_idempotent(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + policy = root / 'etc/containers/policy.json' + policy.parent.mkdir(parents=True) + original = {'default': [{'type': 'reject'}], 'transports': {'docker': { + 'example.org': [{'type': 'reject'}]}, 'dir': {'': [{'type': 'reject'}]}}} + policy.write_text(json.dumps(original)) + for _ in range(2): + trust.configure(root, REPO / 'cosign.pub') + actual = json.loads(policy.read_text()) + self.assertEqual(actual['default'], original['default']) + self.assertEqual(actual['transports']['dir'], original['transports']['dir']) + self.assertEqual(actual['transports']['docker']['example.org'], [{'type': 'reject'}]) + self.assertEqual(actual['transports']['docker']['ghcr.io/eosdev-x/tuxlinux'][0]['type'], 'sigstoreSigned') + self.assertEqual((root / 'etc/pki/containers/tuxlinux.pub').read_bytes(), (REPO / 'cosign.pub').read_bytes()) + + def test_flutter_extracts_once_and_dispatches_both_commands(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + seed = root / 'seed' + seed.mkdir() + (seed / 'flutter-version').write_text('test-version\n') + payload = root / 'payload/flutter/bin' + payload.mkdir(parents=True) + for name in ('flutter', 'dart'): + executable = payload / name + executable.write_text(f'#!/bin/sh\nprintf "%s:%s\\n" "{name}" "$1"\n') + executable.chmod(0o755) + subprocess.run(['tar', '-cJf', str(seed / 'flutter.tar.xz'), '-C', str(root / 'payload'), 'flutter'], check=True) + launcher = root / 'flutter' + launcher.write_text((REPO / 'build_files/flutter-launcher.sh').read_text().replace('seed=/usr/share/tuxlinux', f'seed="{seed}"')) + launcher.chmod(0o755) + (root / 'dart').symlink_to('flutter') + import os + env = dict(os.environ, HOME=str(root / 'home'), XDG_DATA_HOME=str(root / 'data')) + for name in ('flutter', 'dart'): + output = subprocess.check_output([str(root / name), 'hello world'], env=env, text=True) + self.assertEqual(output, f'{name}:hello world\n') + # An existing SDK must work without the seed archive and keep changes. + (seed / 'flutter.tar.xz').unlink() + subprocess.run([str(launcher), 'again'], env=env, check=True, capture_output=True) + + +if __name__ == '__main__': + unittest.main() From 69965addc0aa17bc217ddc163cb94d5647bcb282 Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:21:56 -0700 Subject: [PATCH 2/7] Preserve QCOW2 config and ISO runtime recipe selection --- .github/workflows/build-disk.yml | 2 +- Justfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index 6cf25f7..3c326d1 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -66,7 +66,7 @@ jobs: uses: osbuild/bootc-image-builder-action@028e4d28330efe546229eb9cb52aee78174d2347 with: builder-image: ${{ env.BIB_IMAGE }} - config-file: ${{ matrix.disk-type == 'anaconda-iso' && './disk_config/iso-kde.toml' || 'disk_config/**' }} + config-file: ${{ matrix.disk-type == 'anaconda-iso' && './disk_config/iso-kde.toml' || './disk_config/disk.toml' }} image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} chown: ${{ env.USER_UID }}:${{ env.USER_GID }} types: ${{ matrix.disk-type }} diff --git a/Justfile b/Justfile index b475520..05168ae 100644 --- a/Justfile +++ b/Justfile @@ -273,7 +273,7 @@ run-vm-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_run- # Run a virtual machine from an ISO [group('Run Virtal Machine')] -run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "anaconda-iso" "disk_config/iso-kde.toml") +run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "iso" "disk_config/iso-kde.toml") # Run a virtual machine using systemd-vmspawn [group('Run Virtal Machine')] From 828675fddaf1271f6e44e04ee610cae1c2c43b98 Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:24:48 -0700 Subject: [PATCH 3/7] Validate publishing key against client trust and clean up workflow shell --- .github/workflows/build-disk.yml | 15 +++++++++------ .github/workflows/build.yml | 8 +++++--- .github/workflows/check.yml | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index 3c326d1..b649468 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -47,11 +47,13 @@ jobs: # Concatenate the types with a hyphen DISK_TYPE=$(echo "${{ matrix.disk-type }}" | tr ' ' '-') # Lowercase the image uri - echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" >> ${GITHUB_ENV} - echo "IMAGE_NAME=${IMAGE_NAME,,}" >> ${GITHUB_ENV} - echo "DISK_TYPE=${DISK_TYPE}" >> ${GITHUB_ENV} - echo "USER_UID=${USER_UID}" >> ${GITHUB_ENV} - echo "USER_GID=${USER_GID}" >> ${GITHUB_ENV} + { + echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" + echo "IMAGE_NAME=${IMAGE_NAME,,}" + echo "DISK_TYPE=${DISK_TYPE}" + echo "USER_UID=${USER_UID}" + echo "USER_GID=${USER_GID}" + } >> "$GITHUB_ENV" - name: Maximize build space uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9 @@ -93,8 +95,9 @@ jobs: RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} RCLONE_CONFIG_S3_REGION: ${{ secrets.S3_REGION }} RCLONE_CONFIG_S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }} + S3_BUCKET: ${{ secrets.S3_BUCKET_NAME }} SOURCE_DIR: ${{ steps.build.outputs.output-directory }} run: | sudo apt-get update sudo apt-get install -y rclone - rclone copy $SOURCE_DIR S3:${{ secrets.S3_BUCKET_NAME }}/${{ matrix.disk-type }}/ + rclone copy "$SOURCE_DIR" "S3:${S3_BUCKET}/${DISK_TYPE}/" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5098cf..b5a973b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,8 +39,10 @@ jobs: - name: Prepare environment run: | # Lowercase the image uri - echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" >> ${GITHUB_ENV} - echo "IMAGE_NAME=${IMAGE_NAME,,}" >> ${GITHUB_ENV} + { + echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" + echo "IMAGE_NAME=${IMAGE_NAME,,}" + } >> "$GITHUB_ENV" # These stage versions are pinned by https://github.com/renovatebot/renovate - name: Checkout @@ -69,7 +71,7 @@ jobs: # E.G: 2022-02-08T15:38:15Z' # https://artifacthub.io/docs/topics/repositories/container-images/ # https://linux.die.net/man/1/date - echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" >> $GITHUB_OUTPUT + echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" # Image metadata for https://artifacthub.io/ - This is optional but is highly recommended so we all can get a index of all the custom images # The metadata by itself is not going to do anything, you choose if you want your image to be on ArtifactHub or not. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1eb4fbf..2163589 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,3 +19,19 @@ jobs: for path in pathlib.Path('disk_config').glob('*.toml'): tomllib.loads(path.read_text()) PY + signing-key: + # Fork PRs cannot access repository secrets. Same-repository PRs validate + # that the shipped trust key matches the key used by the publishing job. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + with: + cosign-release: 'v2.6.1' + - name: Check publishing key matches the committed public key + env: + COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} + run: | + cosign public-key --key env://COSIGN_PRIVATE_KEY > /tmp/signing-key.pub + diff -u cosign.pub /tmp/signing-key.pub From fef76a54cb5b19e87839e8a57d1263c0367a117a Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:25:32 -0700 Subject: [PATCH 4/7] Ship the public key matching the existing CI signer --- cosign.pub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cosign.pub b/cosign.pub index 8a1f712..22dd6ec 100644 --- a/cosign.pub +++ b/cosign.pub @@ -1,4 +1,4 @@ -----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUi6MhpBbnhWEz5RVYztY1NTVesdB -o7I/w3F8PKW5sUbOXgp79hYmxLLIpEx1EaYzJqklC/2KVvYVLNHDcXr4Tw== +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEezDbD0bsaUi9OfyEL2bAiTAEP7Yq +agFf6dacnYaWNUBN+sA6RWji8Lph8S3fLtx+AtnDn8Yvyf4U2L1JT7c9ew== -----END PUBLIC KEY----- From 71c363d52bcced5d0b70fb3a0975ed64deaf7e3c Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:35:54 -0700 Subject: [PATCH 5/7] Isolate graphics SDK dependencies and set disk root filesystem --- .github/workflows/build-disk.yml | 2 +- .github/workflows/check.yml | 6 ++++++ Containerfile.devel | 10 ++++++++++ README.md | 22 ++++++++++++++++++++-- build_files/build.sh | 5 +++-- build_files/smoke-test.sh | 7 +------ 6 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 Containerfile.devel diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index b649468..6304170 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -72,7 +72,7 @@ jobs: image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} chown: ${{ env.USER_UID }}:${{ env.USER_GID }} types: ${{ matrix.disk-type }} - additional-args: --use-librepo=True + additional-args: --use-librepo=True --rootfs=btrfs - name: Upload disk images and Checksum to Job Artifacts if: inputs.upload-to-s3 != true && github.event_name != 'pull_request' diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2163589..e3efcda 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -35,3 +35,9 @@ jobs: run: | cosign public-key --key env://COSIGN_PRIVATE_KEY > /tmp/signing-key.pub diff -u cosign.pub /tmp/signing-key.pub + native-sdk: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - name: Build and link native SDK dependencies + run: podman build -f Containerfile.devel -t localhost/tuxlinux-native-sdk:test . diff --git a/Containerfile.devel b/Containerfile.devel new file mode 100644 index 0000000..b78c3ee --- /dev/null +++ b/Containerfile.devel @@ -0,0 +1,10 @@ +# A coherent Fedora userspace for native development without replacing Bazzite's +# custom, epoch-bumped host graphics stack. Update this alongside Fedora rebases. +FROM registry.fedoraproject.org/fedora:44 +RUN dnf5 install -y gcc gcc-c++ cmake ninja-build git pkgconf-pkg-config \ + pipewire-devel mesa-libgbm-devel \ + && dnf5 clean all +# Link against the libraries supplied by the same package manager as the headers. +RUN printf '#include \n#include \nint main(void) { pw_init(0, 0); return gbm_create_device(-1) != 0; }\n' > /tmp/headers-smoke.c \ + && cc /tmp/headers-smoke.c $(pkg-config --cflags --libs libpipewire-0.3 gbm) -o /tmp/headers-smoke \ + && rm /tmp/headers-smoke.c /tmp/headers-smoke diff --git a/README.md b/README.md index 8a740a9..746c32a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ before switching, especially for older NVIDIA GPUs. - Go, Rust/Cargo, Node.js/npm, Python/pip, and Fedora's current default JDK - VS Code, Vim, WezTerm, tmux, Zsh, Starship, Git/GitHub CLI, and CLI utilities -- C/C++ build tools, Tauri/GTK/WebKitGTK, Qt6, Protobuf, PipeWire and GBM development dependencies +- C/C++ build tools, Tauri/GTK/WebKitGTK, Qt6 and Protobuf development dependencies - Podman, podman-compose, Distrobox, Chromium, cloudflared, and Kvantum - Steam and the graphics/gaming stack inherited from Bazzite - Flutter 3.29.3 with its bundled Dart SDK, seeded from a checksum-verified archive @@ -29,6 +29,24 @@ that extracted SDK. Host rollback does not undo changes to the user's SDK. The system Podman socket is enabled for rootful workloads. Rootless containers use their own user service; do not expose either socket to untrusted clients. +## PipeWire / GBM development + +Bazzite's custom Mesa packages do not satisfy Fedora development RPMs' exact +runtime requirements. Build projects needing these interfaces in the supplied +Fedora SDK container, preserving the host gaming drivers: + +```bash +podman build -f Containerfile.devel -t localhost/tuxlinux-native-sdk:latest . +distrobox create --name tuxlinux-native-sdk --image localhost/tuxlinux-native-sdk:latest +distrobox enter tuxlinux-native-sdk +pkg-config --modversion libpipewire-0.3 gbm +``` + +The SDK includes matching package-managed runtime libraries and headers. It is +for compilation, not validation of host NVIDIA/graphics integration. A binary +built there still needs compatibility testing if deployed outside the container. +Distrobox shares your home by default; it is not an untrusted-code sandbox. + ## Installation and signature trust Back up personal data and retain a working deployment. Use `rpm-ostree status` @@ -91,7 +109,7 @@ shellcheck build_files/*.sh CI checks required tools/development metadata during the build, runs bootc lint, and exercises Flutter as an ordinary user in a read-only container. It also -compiles against PipeWire/GBM headers. These checks do not replace a booted VM or +builds a separate native SDK and compiles against its PipeWire/GBM headers. These checks do not replace a booted VM or real NVIDIA hardware test. Test login, graphics, networking, update, and rollback on a disposable deployment before relying on a release. diff --git a/build_files/build.sh b/build_files/build.sh index 70e7aeb..7bf215e 100755 --- a/build_files/build.sh +++ b/build_files/build.sh @@ -58,8 +58,9 @@ dnf5 install -y \ sqlite-devel ninja-build \ qt6-qtdeclarative-devel qt6-qtsvg-devel qt6-qtshadertools-devel libgit2-devel qtkeychain-qt6-devel -# Install development headers through RPM so versions and ownership stay coherent. -dnf5 install -y pipewire-devel mesa-libgbm-devel +# Bazzite ships an epoch-bumped/custom Mesa stack. Fedora development RPMs +# require a different runtime. Keep these headers in Containerfile.devel rather +# than extracting RPMs over the host or replacing gaming drivers. # Containers & virtualization dnf5 install -y distrobox podman-compose diff --git a/build_files/smoke-test.sh b/build_files/smoke-test.sh index a545d97..0e7a0b5 100755 --- a/build_files/smoke-test.sh +++ b/build_files/smoke-test.sh @@ -6,7 +6,7 @@ for tool in git gh go rustc cargo node npm python3 pip3 java javac code \ protoc cmake ninja pkg-config; do command -v "$tool" done -pkg-config --exists libpipewire-0.3 gbm webkit2gtk-4.1 openssl gtk+-3.0 sqlite3 +pkg-config --exists webkit2gtk-4.1 openssl gtk+-3.0 sqlite3 cloudflared --version starship --version javac -version @@ -27,9 +27,4 @@ if [[ ${1:-} != --build ]]; then flutter config --no-analytics flutter create --platforms=linux --no-pub /tmp/flutter-smoke test -f /tmp/flutter-smoke/linux/CMakeLists.txt - # Compile/link representative headers against the actual image libraries. - printf '#include \n#include \nint main(void) { pw_init(0, 0); return gbm_create_device(-1) != 0; }\n' > /tmp/headers-smoke.c - # pkg-config deliberately emits separate compiler arguments. - # shellcheck disable=SC2046 - cc /tmp/headers-smoke.c $(pkg-config --cflags --libs libpipewire-0.3 gbm) -o /tmp/headers-smoke fi From 1c5487b3e0404596e65d5f20cf364bcfbaf1e7de Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:50:03 -0700 Subject: [PATCH 6/7] Provide source RPM keys to disk builder and check Fedora Chromium executable --- .github/workflows/build-disk.yml | 18 ++++++++------- .github/workflows/check.yml | 2 +- Containerfile.builder | 8 +++++++ Justfile | 19 +--------------- README.md | 5 +++- build_files/smoke-test.sh | 4 ++-- scripts/build-disk.sh | 39 ++++++++++++++++++++++++++++++++ 7 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 Containerfile.builder create mode 100755 scripts/build-disk.sh diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index 6304170..8547441 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -15,6 +15,8 @@ on: paths: - 'disk_config/**' - '.github/workflows/build-disk.yml' + - 'Containerfile.builder' + - 'scripts/build-disk.sh' env: IMAGE_NAME: ${{ github.event.repository.name }} # output of build.yml, keep in sync @@ -65,14 +67,14 @@ jobs: - name: Build disk images id: build - uses: osbuild/bootc-image-builder-action@028e4d28330efe546229eb9cb52aee78174d2347 - with: - builder-image: ${{ env.BIB_IMAGE }} - config-file: ${{ matrix.disk-type == 'anaconda-iso' && './disk_config/iso-kde.toml' || './disk_config/disk.toml' }} - image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} - chown: ${{ env.USER_UID }}:${{ env.USER_GID }} - types: ${{ matrix.disk-type }} - additional-args: --use-librepo=True --rootfs=btrfs + env: + IMAGE_TYPE: ${{ matrix.disk-type }} + DISK_CONFIG: ${{ matrix.disk-type == 'anaconda-iso' && 'disk_config/iso-kde.toml' || 'disk_config/disk.toml' }} + run: | + sudo bash scripts/build-disk.sh \ + "${IMAGE_REGISTRY}/${IMAGE_NAME}:${DEFAULT_TAG}" "$IMAGE_TYPE" \ + "$DISK_CONFIG" "$PWD/output" "$BIB_IMAGE" + echo "output-directory=$PWD/output" >> "$GITHUB_OUTPUT" - name: Upload disk images and Checksum to Job Artifacts if: inputs.upload-to-s3 != true && github.event_name != 'pull_request' diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e3efcda..5438df7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - run: | bash -n build_files/build.sh - shellcheck build_files/*.sh + shellcheck build_files/*.sh scripts/*.sh python3 -m unittest discover -s tests -v python3 - <<'PY' import pathlib, tomllib diff --git a/Containerfile.builder b/Containerfile.builder new file mode 100644 index 0000000..3591493 --- /dev/null +++ b/Containerfile.builder @@ -0,0 +1,8 @@ +ARG SOURCE_IMAGE +ARG BIB_IMAGE=quay.io/centos-bootc/bootc-image-builder:latest +FROM ${SOURCE_IMAGE} AS source-keys +# BIB's dependency solver resolves file:// key URLs in the builder filesystem. +# Dereference source-image symlinks; retain the builder's other distro keys. +RUN mkdir /tmp/tuxlinux-rpm-gpg && cp -L /etc/pki/rpm-gpg/* /tmp/tuxlinux-rpm-gpg/ +FROM ${BIB_IMAGE} +COPY --from=source-keys /tmp/tuxlinux-rpm-gpg/ /etc/pki/rpm-gpg/ diff --git a/Justfile b/Justfile index 05168ae..99d16dc 100644 --- a/Justfile +++ b/Justfile @@ -162,25 +162,8 @@ _build-bib $target_image $tag $type $config: (_rootful_load_image target_image t #!/usr/bin/env bash set -euo pipefail - args="--type ${type} " - args+="--use-librepo=True " - args+="--rootfs=btrfs" - BUILDTMP=$(mktemp -p "${PWD}" -d -t _build-bib.XXXXXXXXXX) - - sudo podman run \ - --rm \ - -it \ - --privileged \ - --pull=newer \ - --net=host \ - --security-opt label=type:unconfined_t \ - -v $(pwd)/${config}:/config.toml:ro \ - -v $BUILDTMP:/output \ - -v /var/lib/containers/storage:/var/lib/containers/storage \ - "${bib_image}" \ - ${args} \ - "${target_image}:${tag}" + sudo bash scripts/build-disk.sh "${target_image}:${tag}" "$type" "$config" "$BUILDTMP" "$bib_image" mkdir -p output sudo mv -f $BUILDTMP/* output/ diff --git a/README.md b/README.md index 746c32a..945c97d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ just build podman build -t tuxlinux . python3 -m unittest discover -s tests -v -shellcheck build_files/*.sh +shellcheck build_files/*.sh scripts/*.sh ``` CI checks required tools/development metadata during the build, runs bootc lint, @@ -118,6 +118,9 @@ is supported. The disk workflow consumes the published `latest` image; a PR disk build checks installer configuration, not an unpublished PR container. Installer post-install configuration retains the TuxLinux origin with signature enforcement. QCOW2 and installer artifacts have separate names to prevent matrix overwrites. +Both local and CI builds use `scripts/build-disk.sh`, which copies the source +image's public RPM keys into a temporary builder image for dependency solving. +Repository signature checks remain enabled. Packages live in `build_files/build.sh`; launchers, trust setup, and smoke checks live alongside it. Flutter and Starship versions/checksums must be updated diff --git a/build_files/smoke-test.sh b/build_files/smoke-test.sh index 0e7a0b5..8b6da71 100755 --- a/build_files/smoke-test.sh +++ b/build_files/smoke-test.sh @@ -2,9 +2,9 @@ set -euo pipefail for tool in git gh go rustc cargo node npm python3 pip3 java javac code \ - wezterm chromium cloudflared podman distrobox starship flutter dart \ + wezterm chromium-browser cloudflared podman distrobox starship flutter dart \ protoc cmake ninja pkg-config; do - command -v "$tool" + command -v "$tool" || { echo "Missing required executable: $tool" >&2; exit 1; } done pkg-config --exists webkit2gtk-4.1 openssl gtk+-3.0 sqlite3 cloudflared --version diff --git a/scripts/build-disk.sh b/scripts/build-disk.sh new file mode 100755 index 0000000..dcf62ab --- /dev/null +++ b/scripts/build-disk.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Rootful Podman is required by bootc-image-builder. +set -euo pipefail +if [[ $# != 5 || $EUID != 0 ]]; then + echo 'Usage: sudo build-disk.sh IMAGE TYPE CONFIG OUTPUT BUILDER_IMAGE' >&2 + exit 2 +fi +image=$1 +type=$2 +config=$(realpath "$3") +output=$(realpath -m "$4") +builder_image=$5 +repo=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +case "$type" in qcow2|raw|anaconda-iso) ;; *) echo "Unsupported type: $type" >&2; exit 2 ;; esac +test -f "$config" +mkdir -p "$output" +if [[ -n $(find "$output" -mindepth 1 -print -quit) ]]; then + echo "Output directory must be empty: $output" >&2 + exit 2 +fi +podman image exists "$image" || podman pull "$image" +podman pull "$builder_image" +# Each build gets a unique builder tag, avoiding collisions with parallel work. +builder_tag="localhost/tuxlinux-image-builder:$(date +%s)-$$" +trap 'podman image rm "$builder_tag" >/dev/null || true' EXIT +podman build --pull=never --build-arg "SOURCE_IMAGE=$image" \ + --build-arg "BIB_IMAGE=$builder_image" -f "$repo/Containerfile.builder" \ + -t "$builder_tag" "$repo" +podman run --rm --privileged --security-opt label=type:unconfined_t \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ + -v "$config:/config.toml:ro" -v "$output:/output" \ + "$builder_tag" --type "$type" --rootfs=btrfs --use-librepo=True "$image" +( + cd "$output" + # SHA256SUMS is explicitly excluded from the input list. + # shellcheck disable=SC2094 + find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 -r sha256sum > SHA256SUMS +) +chown -R "${SUDO_UID:-0}:${SUDO_GID:-0}" "$output" From c0343c60936687e42fcb70f757f38a0ea303abbe Mon Sep 17 00:00:00 2001 From: TuxClaw <261464027+tuxclaw@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:13:59 -0700 Subject: [PATCH 7/7] Use a valid Dart package name in the runtime smoke test --- build_files/smoke-test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_files/smoke-test.sh b/build_files/smoke-test.sh index 8b6da71..5e06e57 100755 --- a/build_files/smoke-test.sh +++ b/build_files/smoke-test.sh @@ -25,6 +25,6 @@ if [[ ${1:-} != --build ]]; then flutter --version dart --version flutter config --no-analytics - flutter create --platforms=linux --no-pub /tmp/flutter-smoke - test -f /tmp/flutter-smoke/linux/CMakeLists.txt + flutter create --platforms=linux --no-pub /tmp/flutter_smoke + test -f /tmp/flutter_smoke/linux/CMakeLists.txt fi