diff --git a/.forgejo/workflows/build-artifacts.yml b/.forgejo/workflows/build-artifacts.yml new file mode 100644 index 0000000..4fa02e6 --- /dev/null +++ b/.forgejo/workflows/build-artifacts.yml @@ -0,0 +1,238 @@ +name: Build deb-bootupd Artifacts + +# ⚠️ IMPORTANT: Each repository needs its own ACCESS_TOKEN secret! +# +# To set up this workflow in a new repository: +# 1. Go to repository settings: https://git.raines.xyz/OWNER/REPO/settings +# 2. Find "Secrets" or "Repository secrets" section +# 3. Add new secret: +# - Name: ACCESS_TOKEN +# - Value: Your Personal Access Token with repo and write:packages permissions +# 4. The token needs these scopes: +# - repo (Full control of private repositories) +# - write:packages (Write packages) +# - read:packages (Read packages) +# +# This workflow will fail with "ACCESS_TOKEN is not set" if the secret is missing. + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +env: + UBUNTU_VERSION: "24.04" + RUST_VERSION: "1.75.0" + +jobs: + build-artifacts: + name: Build deb-bootupd Artifacts + runs-on: ubuntu-latest + container: + image: ubuntu:latest + steps: + - name: Setup build environment + shell: bash + run: | + apt update -y + apt install -y git curl pkg-config build-essential gnupg + + # Install system Rust packages first for dpkg-buildpackage compatibility + apt install -y rustc cargo + + # Install Rust using rustup to get the latest version + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + . ~/.cargo/env + + # Set default toolchain for rustup + rustup default stable + + # Verify Rust version + rustc --version + cargo --version + + # Install additional build dependencies + apt install -y libssl-dev libsystemd-dev + + - name: Checkout repository manually + run: | + # Clone the repository manually instead of using actions/checkout + git clone https://git.raines.xyz/robojerk/deb-bootupd.git /tmp/deb-bootupd + cd /tmp/deb-bootupd + + # Show repository info + echo "Repository: $(git remote get-url origin)" + echo "Branch: $(git branch --show-current)" + echo "Commit: $(git rev-parse --short HEAD)" + echo "Date: $(git log -1 --format=%cd)" + + - name: Build Rust project + run: | + cd /tmp/deb-bootupd + + # Show project structure + echo "Project structure:" + ls -la + + # Check Cargo.toml + echo "Cargo.toml contents:" + cat Cargo.toml + + # Build in release mode + echo "Building deb-bootupd in release mode..." + cargo build --release + + # Verify binaries were created + echo "Build artifacts:" + ls -la target/release/ + + # Show binary information + if [ -f target/release/bootupd ]; then + echo "bootupd binary info:" + file target/release/bootupd + ldd target/release/bootupd || echo "Static binary or no dynamic dependencies" + fi + + - name: Run tests + run: | + cd /tmp/deb-bootupd + + echo "Running tests..." + cargo test --release + + echo "Running clippy..." + cargo clippy --release + + echo "Checking formatting..." + cargo fmt --check + + - name: Create build artifacts + run: | + cd /tmp/deb-bootupd + + # Create artifacts directory + mkdir -p build-artifacts + + # Copy compiled binaries + cp target/release/bootupd build-artifacts/ + cp target/release/bootupctl build-artifacts/ 2>/dev/null || echo "bootupctl not found (may be symlink)" + + # Copy source code for reference + cp -r src/ build-artifacts/ + cp Cargo.toml Cargo.lock build-artifacts/ + + # Copy Debian packaging files + cp -r debian/ build-artifacts/ 2>/dev/null || echo "debian/ directory not found" + cp -r systemd/ build-artifacts/ 2>/dev/null || echo "systemd/ directory not found" + + # Create build info file + cat > build-artifacts/BUILD_INFO.txt << EOF + deb-bootupd Build Information + ============================= + Build Date: $(date) + Ubuntu Version: ${UBUNTU_VERSION} + Rust Version: $(rustc --version) + Cargo Version: $(cargo --version) + Git Commit: $(git rev-parse --short HEAD) + Git Branch: $(git branch --show-current) + Build Type: Release + EOF + + # Show artifacts + echo "Build artifacts created:" + ls -la build-artifacts/ + echo "" + echo "Build info:" + cat build-artifacts/BUILD_INFO.txt + + - name: Upload artifacts to Forgejo + env: + USER: robojerk + TOKEN: ${{ secrets.ACCESS_TOKEN }} + BASE_URL: "git.raines.xyz" + run: | + cd /tmp/deb-bootupd + + # Create zip archive of artifacts + artifact_name="deb-bootupd-artifacts-$(git rev-parse --short HEAD).zip" + zip -r "$artifact_name" build-artifacts/ + + echo "Created artifact archive: $artifact_name" + ls -la "$artifact_name" + + # Upload to Forgejo generic package registry + echo "Uploading artifacts to Forgejo Package Registry..." + + # Use the same upload pattern as bootc-deb + path="api/packages/robojerk/generic/deb-bootupd/$(git rev-parse --short HEAD)" + upload_url="https://${BASE_URL}/${path}/${artifact_name}" + + echo "Upload URL: $upload_url" + + # Upload with proper authentication + http_code=$(curl -s -o /dev/null -w "%{http_code}" \ + --user "${USER}:${TOKEN}" \ + --upload-file "$artifact_name" \ + "$upload_url") + + echo "HTTP Response Code: $http_code" + + if [ "$http_code" = "201" ]; then + echo "✅ Artifacts uploaded successfully to Forgejo Package Registry" + elif [ "$http_code" = "409" ]; then + echo "➡️ INFO: Artifacts already exist (HTTP 409 Conflict)" + else + echo "❌ Upload failed with HTTP $http_code" + # Show verbose output for debugging + curl -v -i --user "${USER}:${TOKEN}" \ + --upload-file "$artifact_name" \ + "$upload_url" 2>&1 + exit 1 + fi + + - name: Create release assets + run: | + cd /tmp/deb-bootupd + + mkdir -p release-assets + cp "$artifact_name" release-assets/ 2>/dev/null || echo "No artifact archive found" + + # Create a summary file + cat > release-assets/BUILD_SUMMARY.txt << EOF + deb-bootupd Build Summary + ========================= + Build Date: $(date) + Ubuntu Version: ${UBUNTU_VERSION} + Rust Version: $(rustc --version) + Git Commit: $(git rev-parse --short HEAD) + Git Branch: $(git branch --show-current) + + Built Artifacts: + - Rust binaries (release mode) + - Source code + - Debian packaging files + - Systemd service files + + Artifact Archive: $artifact_name + EOF + + echo "Release assets created:" + ls -la release-assets/ + + - name: Success Summary + run: | + echo "=== Build Summary ===" + echo "✅ deb-bootupd compiled successfully in release mode" + echo "✅ All tests passed" + echo "✅ Code formatting and linting passed" + echo "✅ Build artifacts created and uploaded to Forgejo" + echo "" + echo "📦 Artifacts available at:" + echo " https://git.raines.xyz/robojerk/deb-bootupd/packages" + echo "" + echo "🎯 Next steps:" + echo " - Verify artifacts appear in repository packages page" + echo " - Test binaries on Ubuntu Noble systems" + echo " - Consider building .deb packages for distribution" diff --git a/.forgejo/workflows/simple-build.yml b/.forgejo/workflows/simple-build.yml new file mode 100644 index 0000000..a69d91c --- /dev/null +++ b/.forgejo/workflows/simple-build.yml @@ -0,0 +1,54 @@ +name: Simple Build & Upload + +# Simple workflow for building deb-bootupd and uploading artifacts +# Based on patterns from: https://domaindrivenarchitecture.org/pages/dda-pallet/ + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + container: + image: rust:1.75-slim + steps: + - name: Checkout code + uses: https://data.forgejo.org/actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -y + apt install -y pkg-config libssl-dev libsystemd-dev + + - name: Build project + run: | + cargo build --release + ls -la target/release/ + + - name: Run tests + run: | + cargo test --release + cargo clippy --release + cargo fmt --check + + - name: Create artifacts + run: | + mkdir -p artifacts + cp target/release/bootupd artifacts/ + cp -r src/ artifacts/ + cp Cargo.toml artifacts/ + + # Create build info + echo "Build: $(date) - $(git rev-parse --short HEAD)" > artifacts/build-info.txt + + - name: Upload artifacts + uses: https://data.forgejo.org/actions/upload-artifact@v3 + with: + name: deb-bootupd-build + path: artifacts/ + if-no-files-found: error + retention-days: 30 diff --git a/README.md b/README.md index a7c7202..cc72212 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ This fork specifically adapts the original Red Hat/Fedora-centric bootupd for ** - **Package System**: DPKG-based package metadata discovery - **State Management**: Persistent state tracking in `/boot/bootupd-state.json` +### Binary Architecture + +- **`bootupd`**: The main binary that performs bootloader updates (NOT a daemon) +- **`bootupctl`**: A symlink to the main `bootupd` binary (multicall binary pattern) +- **Relationship**: Both are the same binary, with `bootupctl` being a symlink. The binary detects which name it was called as and behaves accordingly. + ### Design Philosophy - **No Daemon**: Despite the 'd' suffix, it's "bootloader-upDater" not "bootloader-updater-Daemon" @@ -37,14 +43,58 @@ This fork specifically adapts the original Red Hat/Fedora-centric bootupd for ** ## Installation +### Installation Methods + +**1. Debian Package (Recommended)** +- **Pros**: No compilation, automatic dependency resolution, system integration +- **Use when**: You want to install and run immediately, or for production systems +- **Requirements**: Just `apt` and root access + +**2. Pre-built .deb Package** +- **Pros**: No compilation, portable between similar systems +- **Use when**: You have a .deb file but no repository access +- **Requirements**: `dpkg` and root access + +**3. Build from Source** +- **Pros**: Latest development version, customization options +- **Use when**: Developing, testing, or need specific features +- **Requirements**: Rust toolchain, build dependencies, more time + +**4. Build Your Own .deb Package** +- **Pros**: Customizable, distributable, reproducible +- **Use when**: Creating packages for distribution or custom builds +- **Requirements**: Build dependencies, packaging knowledge + ### Prerequisites +**For Package Installation (Methods 1-2)**: - Debian-based system (Debian, Ubuntu, etc.) -- Rust toolchain (for building from source) -- Required system packages: - - `efibootmgr` (for EFI systems) - - `grub-common` (for GRUB support) - - `mount`/`umount` (standard Linux tools) +- `apt` package manager +- Root access for installation + +**For Source Building (Methods 3-4)**: +- Debian-based system (Debian, Ubuntu, etc.) +- Rust toolchain (rustc, cargo) +- Build dependencies (see below) + +**Required Runtime Packages** (installed automatically with .deb): +- `efibootmgr` (for EFI systems) +- `grub-common` (for GRUB support) +- `mount`/`umount` (standard Linux tools) + +### Debian Package (Recommended - No Compilation Required) + +```bash +# Install from Debian repository (when available) +sudo apt update +sudo apt install deb-bootupd + +# Or install from a pre-built .deb package +sudo dpkg -i deb-bootupd_*.deb + +# If dependencies are missing, install them +sudo apt install -f +``` ### Building from Source @@ -60,16 +110,106 @@ cargo build --release sudo cargo install --path . ``` -### Debian Package +### Running as Rust Script (Development) ```bash +# Clone the repository +git clone https://git.raimes.xyz/robojerk/deb-bootupd.git +cd deb-bootupd + +# Run directly without installing (development mode) +cargo run -- status # Run bootupctl status +cargo run -- update # Run bootupctl update +cargo run -- adopt-and-update # Run bootupctl adopt-and-update + +# Run with specific binary name (multicall binary) +cargo run --bin bootupd -- status # Run as bootupd binary +cargo run --bin bootupctl -- status # Run as bootupctl binary + +# Run with debug output +RUST_LOG=debug cargo run -- status + +# Run with custom log level +RUST_LOG=info cargo run -- status + +# Run specific tests +cargo test # Run all tests +cargo test --package deb-bootupd # Run package tests +cargo test --test integration # Run integration tests + +# Development workflow +cargo check # Check compilation without building +cargo clippy # Run linter +cargo fmt --check # Check code formatting +cargo fmt # Auto-format code +``` + +### How the Multicall Binary Works + +deb-bootupd uses a **multicall binary pattern** - a single Rust executable that behaves differently based on how it's called: + +```bash +# When called as 'bootupd' (main binary) +cargo run --bin bootupd -- install --src-root /src --dest-root /dest + +# When called as 'bootupctl' (CLI interface) +cargo run --bin bootupctl -- status +cargo run --bin bootupctl -- update + +# The binary detects argv[0] and switches behavior accordingly +``` + +**Benefits for Development:** +- **Single codebase**: All functionality in one Rust project +- **Easy testing**: Test both modes from one source +- **Consistent behavior**: Same binary, different interfaces +- **Simplified deployment**: One executable to install + +### Building Your Own Debian Package + +```bash +# Install build dependencies +sudo apt install build-essential dh-cargo rustc cargo pkg-config libssl-dev libsystemd-dev + # Build Debian package dpkg-buildpackage -b -# Install package +# Install the resulting package sudo dpkg -i ../deb-bootupd_*.deb ``` +## Quick Start + +### Install and Run (No Compilation) + +```bash +# 1. Install the package (when available in repositories) +sudo apt update && sudo apt install deb-bootupd + +# 2. Check if it's working +bootupctl status + +# 3. You're ready to use deb-bootupd! +``` + +### Automated Builds with Forgejo Actions + +This repository includes Forgejo Actions workflows for automated building and artifact management: + +- **`.forgejo/workflows/build-artifacts.yml`** - Full build pipeline with Forgejo Package Registry upload +- **`.forgejo/workflows/simple-build.yml`** - Simple build with artifact upload + +**Setup Requirements:** +1. Add `ACCESS_TOKEN` secret to repository settings +2. Token needs `repo` and `write:packages` permissions +3. Workflows trigger on push/PR to main/master branches + +**What Gets Built:** +- Rust binaries (release mode) +- Source code artifacts +- Debian packaging files +- Systemd service files + ## Usage ### Basic Commands diff --git a/deb-bootupd.md b/deb-bootupd.md index f241344..006b08f 100644 --- a/deb-bootupd.md +++ b/deb-bootupd.md @@ -28,6 +28,154 @@ - State in `/var` (shared across deployments) - OSTree object store in `/ostree` +## Critical Context: How bootc-image-builder Uses bootupd + +### **bootc-image-builder Uses bootupd as a Rust Crate Packaged into an RPM** + +This is a fundamental insight that shapes our entire implementation strategy: + +#### **1. Build-Time Integration (Not Runtime)** +```bash +# During image build, bootc-image-builder: +1. Installs the rust-bootupd RPM package +2. RPM package contains the compiled Rust binary +3. Binary gets placed in /usr/libexec/bootupd +4. bootupctl symlink created in /usr/bin/ (multicall binary pattern) +5. Systemd services and GRUB configs installed +6. Final bootc image contains the compiled bootupd binary +``` + +#### **2. Package-Based Distribution** +From the Fedora RPM spec file, bootupd is distributed as: +- **`rust-bootupd`** - The RPM package containing the compiled Rust binary +- **Source**: The RPM build process compiles the Rust crate into a binary +- **Result**: A pre-compiled binary package, not source code + +The bootc-image-builder installs the pre-built RPM package, which contains the compiled Rust binary. + +#### **3. Why This Matters for deb-bootupd** +This is exactly what we need to replicate for Debian: + +```bash +# debian-bootc-image-builder workflow: +1. Install deb-bootupd .deb package during image build +2. Package manager automatically places bootupd binary in /usr/libexec/bootupd and creates bootupctl symlink in /usr/bin/ +3. Systemd services and GRUB configs installed +4. Final Debian bootc image contains deb-bootupd +``` + +#### **4. Key Differences from Rust Crate Usage** + +| Aspect | Rust Crate | Fedora Package | Debian Package | +|--------|------------|----------------|----------------| +| **Build Process** | `cargo install` | `dnf install rust-bootupd` | `apt install deb-bootupd` | +| **Binary Location** | `~/.cargo/bin/` | `/usr/libexec/bootupd` | `/usr/libexec/bootupd` | +| **System Integration** | Manual setup | Automatic via RPM | Automatic via .deb | +| **Dependencies** | Rust deps only | System packages | System packages | +| **Image Builder** | Source compilation | Package installation | Package installation | + +#### **5. Why Package-Based Approach is Critical** +1. **Reproducible Builds**: Same binary every time, no compilation variations +2. **System Integration**: Automatic service files, paths, and dependencies +3. **Security**: Signed packages with proper dependency resolution +4. **Performance**: No compilation time during image building +5. **Maintenance**: Updates come through package management, not cargo + +### **Conclusion** +bootc-image-builder uses bootupd as a **Rust crate packaged into an RPM**. This means: + +- **deb-bootupd must be packaged as a Debian .deb package (using dh-cargo)** +- **debian-bootc-image-builder must install the .deb package during image build** +- **The .deb package contains the compiled Rust binary** +- **No Rust compilation happens during image building - binary is pre-compiled** + +This is why we're creating the Debian packaging files (`debian/control`, `debian/rules`) with `dh-cargo` - so that debian-bootc-image-builder can install deb-bootupd as a package, just like Fedora systems do with the `rust-bootupd` RPM package. + +## **Critical Analysis: How bootupd is Actually Packaged** + +Based on the [official bootupd repository](https://github.com/coreos/bootupd), here's exactly how they package it: + +### **1. RPM Packaging Structure (contrib/packaging/bootupd.spec)** + +```rpm +Name: rust-%{crate} # Package name: rust-bootupd +Version: 0.2.9 +BuildRequires: cargo-rpm-macros >= 25 # Uses Fedora's Rust packaging macros +``` + +**Key Files Installed**: +- `%{_bindir}/bootupctl` → `/usr/bin/bootupctl` (symlink) +- `%{_libexecdir}/bootupd` → `/usr/libexec/bootupd` (main binary) +- `%{_prefix}/lib/bootupd/grub2-static/` → GRUB configuration files +- `%{_unitdir}/bootloader-update.service` → systemd service + +### **2. Makefile Installation Pattern** + +```makefile +install: + mkdir -p "${DESTDIR}$(PREFIX)/bin" "${DESTDIR}$(LIBEXECDIR)" + install -D -t "${DESTDIR}$(LIBEXECDIR)" target/${PROFILE}/bootupd + ln -f ${DESTDIR}$(LIBEXECDIR)/bootupd ${DESTDIR}$(PREFIX)/bin/bootupctl +``` + +**Critical Insight**: `bootupctl` is a **symlink** to the main `bootupd` binary, not a separate binary! + +### **3. Multicall Binary Architecture** + +From the Makefile: +```makefile +all: + cargo build ${CARGO_ARGS} + ln -f target/${PROFILE}/bootupd target/${PROFILE}/bootupctl +``` + +**How it works**: +1. **Single binary**: `bootupd` is compiled as one Rust binary +2. **Symlink creation**: `bootupctl` is created as a symlink to `bootupd` +3. **Runtime detection**: The binary detects which name it was called as (`argv[0]`) +4. **Behavior switching**: Different behavior based on the name (daemon vs client) + +### **4. Why This Matters for deb-bootupd** + +**Our current debian/rules is INCORRECT**: +```makefile +# Current (wrong): +ln -sf /usr/bin/bootupd debian/deb-bootupd/usr/bin/bootupctl + +# Should be (correct): +ln -sf /usr/libexec/bootupd debian/deb-bootupd/usr/bin/bootupctl +``` + +**Correct Debian packaging should**: +1. **Install main binary**: `/usr/libexec/bootupd` (like RPM does) +2. **Create symlink**: `/usr/bin/bootupctl` → `/usr/libexec/bootupd` +3. **Follow RPM pattern**: Mirror the exact file layout from the RPM spec + +### **5. File Layout Comparison** + +| Component | RPM Location | Debian Location | Type | +|-----------|--------------|-----------------|------| +| **Main binary** | `/usr/libexec/bootupd` | `/usr/libexec/bootupd` | Executable | +| **CLI interface** | `/usr/bin/bootupctl` | `/usr/bin/bootupctl` | Symlink | +| **GRUB configs** | `/usr/lib/bootupd/grub2-static/` | `/usr/lib/bootupd/grub2-static/` | Static files | +| **Systemd service** | `/usr/lib/systemd/system/bootloader-update.service` | `/etc/systemd/system/bootloader-update.service` | Service file | + +**Key insight**: The binary goes in `/usr/libexec/` (not `/usr/bin/`), and `bootupctl` is a symlink to it. + +### **6. Critical Correction: bootupd is NOT a Daemon** + +From the [official bootupd repository](https://github.com/coreos/bootupd): + +> **Is bootupd a daemon?** +> +> It was never a daemon. The name was intended to be "bootloader-upDater" not "bootloader-updater-Daemon". The choice of a "d" suffix is in retrospect probably too confusing. + +**What this means for deb-bootupd**: +- **No daemon process**: `bootupd` is a command-line tool that runs and exits +- **systemd integration**: Uses `systemd-run` for locking and sandboxing, not as a persistent service +- **Event-driven**: Triggered by systemd services or bootc hooks, not running continuously +- **Multicall binary**: Single executable that behaves differently based on how it's called + ## Phase 1: Project Setup & Structure ### 1.1 Create Debian Bootupd Directory Structure @@ -47,27 +195,37 @@ deb-bootupd/ - Create debian branch - Set up proper attribution and licensing -### 1.3 Git Strategy: Hard Clean Fork -**Approach**: Simple, direct fork for proof-of-concept +### 1.3 Git Strategy: Fork with Upstream Remote +**Approach**: Proper Git fork with upstream remote for sustainable development **Benefits**: - **Clean start**: No complex git history or upstream sync complexity +- **Maintainable**: Can easily pull upstream changes, security patches, and bug fixes - **Focus on core**: Concentrate on making Debian immutable system bootable -- **Proof-of-concept**: Perfect for testing the concept without maintenance overhead -- **Simple workflow**: Just copy, adapt, and test +- **Proof-of-concept**: Perfect for testing the concept while maintaining upstream compatibility +- **Future-proof**: Simplifies long-term maintenance and reduces technical debt **Implementation**: ```bash -# Simple copy approach +# Proper fork approach +git clone https://github.com/coreos/bootupd.git deb-bootupd +cd deb-bootupd +git remote rename origin upstream +git remote add origin +git checkout -b debian-adaptation +``` + +**Alternative (if proper fork is too complex initially)**: +```bash +# Simple copy approach (higher maintenance risk) cp -r .Red_Hat_Version/bootupd deb-bootupd/ cd deb-bootupd -chmod 755 deb-bootupd git init git add . git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-concept" ``` -**Future considerations**: Can always add upstream sync later if the concept proves viable +**Recommendation**: Use the proper Git fork approach. While slightly more complex initially, it significantly reduces the long-term maintenance burden and makes it easier to incorporate upstream security patches and improvements. ## Phase 2: Core Code Adaptation @@ -139,8 +297,9 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc **Kernel Path Adaptation**: ```rust // Current (Red Hat): /usr/lib/modules/*/vmlinuz -// Debian OSTree: /usr/lib/modules/*/vmlinuz (same path, different context) +// Debian OSTree: /ostree/deploy/debian/deploy/$checksum.0/usr/lib/modules/*/vmlinuz // Need to ensure this works with Debian kernel naming conventions +// Critical: bootupd must correctly resolve the nested OSTree deployment path ``` **Kernel Filename Parsing** (Key Challenge): @@ -165,15 +324,17 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc **Current Dependencies**: - `efibootmgr` (EFI boot management) - `mount`/`umount` (filesystem operations) -- `grub-install` (GRUB installation) +- `grub-common` (GRUB configuration tools) **Debian Compatibility**: - ✅ `efibootmgr`: Available in Debian repositories - ✅ `mount`/`umount`: Standard Linux tools -- ✅ `grub-install`: Available in Debian repositories +- ✅ `grub-common`: Available in Debian repositories **Action**: Ensure these packages are available in particle-os base image +**Note**: `bootupd`'s primary role is updating bootloader configuration files and entries, not running a full `grub-install`. In an immutable OSTree system, the core GRUB files are part of the image itself. `bootupd` uses tools like `efibootmgr` and GRUB configuration manipulation to point to the correct deployment. + ### 3.2 Systemd Integration **Current**: Hard dependency on `libsystemd` **Debian**: ✅ Fully supports systemd @@ -210,6 +371,8 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc - **Rollback support**: Leverage OSTree's built-in rollback capabilities - **State persistence**: Ensure bootupd state survives across deployments +**Trigger Mechanism**: `bootupd` is not a standalone daemon that runs on a schedule. It's triggered by systemd services (like `bootupd-post-upgrade.service`) or `bootc` hooks that run after a new OSTree deployment is committed. This event-driven approach ensures bootloader updates happen at the right time in the deployment lifecycle. + ### 4.2 Debian OSTree Integration **Unique Challenges**: - **Hybrid approach**: Debian packages within immutable OSTree system @@ -273,6 +436,8 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc - **Cargo.lock**: Include for reproducible builds - **Vendor directory**: Consider including for offline builds +**Important**: The Rust compilation happens when the `.deb` package is built (using `dh_cargo build --release`), not during the `debian-bootc-image-builder` process. The image builder simply installs the resulting pre-compiled binary via the `.deb` package. + ### 6.2 Container Integration **Integration Points**: - particle-os base image requirements @@ -283,16 +448,18 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc **Critical Integration Points**: **Build Process Integration**: -- **Include deb-bootupd**: Binary must be built and included in the bootc image -- **Build timing**: deb-bootupd compiled during image build, not first boot +- **Include deb-bootupd**: Binary must be installed as a .deb package during image build +- **Build timing**: deb-bootupd package installed during image build, not compiled from source - **Dependencies**: Ensure all required tools (`efibootmgr`, `grub-install`) are in base image **Image Builder Workflow**: -1. **Build phase**: Compile deb-bootupd using debian-bootc-image-builder -2. **Installation**: Install deb-bootupd binary and systemd service +1. **Build phase**: Install deb-bootupd .deb package using debian-bootc-image-builder +2. **Installation**: Package manager automatically places binary in `/usr/libexec/bootupd` and creates symlinks 3. **Configuration**: Set up initial bootloader configuration 4. **First boot**: deb-bootupd runs to adopt existing bootloader or install new one +**Key Insight**: This mirrors exactly how Fedora bootc-image-builder works - it installs the `rust-bootupd` RPM package, which contains the compiled Rust binary. We must replicate this pattern with Debian packaging using `dh-cargo`. + **debian/control Integration**: - **Build dependencies**: Use `dh-cargo` for Rust build system integration - **Runtime dependencies**: Ensure `efibootmgr`, `grub-common` are available @@ -318,8 +485,10 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc ### High Priority (Phase 1-2) - Proof-of-Concept Core 1. **Package system adaptation** (RPM → DPKG) - Essential for Debian compatibility -2. **Core path and dependency fixes** - Make it compile and run -3. **Basic Debian compatibility** - Get it working on Debian system +2. **Debian packaging creation** - Create .deb package for debian-bootc-image-builder integration +3. **Fix binary layout** - Correct file locations to match RPM pattern (`/usr/libexec/bootupd`, symlink for `bootupctl`) +4. **Core path and dependency fixes** - Make it compile and run +5. **Basic Debian compatibility** - Get it working on Debian system ### Medium Priority (Phase 3-4) - Basic Functionality 1. **Enhanced OS detection** - Proper Debian identification @@ -369,10 +538,13 @@ git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-conc ## Next Steps 1. **Immediate**: Set up deb-bootupd directory structure -2. **Week 1**: Adapt package system integration -3. **Week 2**: Fix hardcoded paths and dependencies -4. **Week 3**: Basic testing and validation -5. **Week 4**: Particle-OS integration testing +2. **Week 1**: Adapt package system integration (RPM → DPKG) +3. **Week 2**: Create Debian packaging files (.deb package) +4. **Week 3**: Fix hardcoded paths and dependencies +5. **Week 4**: Basic testing and validation +6. **Week 5**: Particle-OS integration testing with debian-bootc-image-builder + +**Critical**: Focus on creating a working .deb package first, as this is how debian-bootc-image-builder will integrate with deb-bootupd (mirroring the Fedora RPM approach). ## Resources & References diff --git a/debian/rules b/debian/rules index 43c69a2..0351543 100755 --- a/debian/rules +++ b/debian/rules @@ -11,5 +11,5 @@ override_dh_auto_install: # Install systemd service files install -D -m 644 systemd/bootupd.service debian/deb-bootupd/etc/systemd/system/ install -D -m 644 systemd/bootupd.socket debian/deb-bootupd/etc/systemd/system/ - # Create symlinks for multicall binary - ln -sf /usr/bin/bootupd debian/deb-bootupd/usr/bin/bootupctl + # Create symlink for multicall binary (following RPM pattern) + ln -sf /usr/libexec/bootupd debian/deb-bootupd/usr/bin/bootupctl diff --git a/src/bootupd.rs b/src/bootupd.rs index 1eada4c..a8df07b 100755 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -50,10 +50,20 @@ pub(crate) fn install( target_components: Option<&[String]>, auto_components: bool, ) -> Result<()> { + // Validate input parameters + if source_root.is_empty() { + anyhow::bail!("source_root cannot be empty"); + } + if dest_root.is_empty() { + anyhow::bail!("dest_root cannot be empty"); + } + // TODO: Change this to an Option<&str>; though this probably balloons into having // DeviceComponent and FileBasedComponent let device = device.unwrap_or(""); - let source_root = openat::Dir::open(source_root).context("Opening source root")?; + let source_root = openat::Dir::open(source_root) + .with_context(|| format!("Opening source root: {}", source_root))?; + SavedState::ensure_not_present(dest_root) .context("failed to install, invalid re-install attempted")?; @@ -62,15 +72,23 @@ pub(crate) fn install( println!("No components available for this platform."); return Ok(()); } + let target_components = if let Some(target_components) = target_components { // Checked by CLI parser assert!(!auto_components); + if target_components.is_empty() { + anyhow::bail!("No target components specified"); + } + target_components .iter() .map(|name| { + if name.is_empty() { + anyhow::bail!("Component name cannot be empty"); + } all_components .get(name.as_str()) - .ok_or_else(|| anyhow!("Unknown component: {name}")) + .ok_or_else(|| anyhow!("Unknown component: '{}'", name)) }) .collect::>>()? } else { @@ -81,6 +99,8 @@ pub(crate) fn install( anyhow::bail!("No components specified"); } + log::info!("Installing {} components to {}", target_components.len(), dest_root); + let mut state = SavedState::default(); let mut installed_efi_vendor = None; for &component in target_components.iter() { @@ -434,7 +454,10 @@ pub(crate) fn print_status(status: &Status) -> Result<()> { } if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? { - println!("CoreOS aleph version: {}", coreos_aleph.version_info.version); + println!( + "CoreOS aleph version: {}", + coreos_aleph.version_info.version + ); } #[cfg(any( diff --git a/src/cli/bootupctl.rs b/src/cli/bootupctl.rs index 4e831fe..4032c0d 100755 --- a/src/cli/bootupctl.rs +++ b/src/cli/bootupctl.rs @@ -178,14 +178,18 @@ fn ensure_running_in_systemd() -> Result<()> { require_root_permission()?; let running_in_systemd = running_in_systemd(); if !running_in_systemd { + log::info!("Not running in systemd, re-executing via systemd-run"); + // Clear any failure status that may have happened previously let _r = Command::new("systemctl") .arg("reset-failed") .arg("bootupd.service") .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn()? - .wait()?; + .spawn() + .and_then(|mut child| child.wait()) + .map_err(|e| log::warn!("Failed to reset failed status: {}", e)); + let r = Command::new("systemd-run") .args(SYSTEMD_ARGS_BOOTUPD) .args( @@ -196,7 +200,7 @@ fn ensure_running_in_systemd() -> Result<()> { .args(std::env::args()) .exec(); // If we got here, it's always an error - return Err(r.into()); + return Err(anyhow::anyhow!("Failed to re-execute via systemd-run: {}", r)); } Ok(()) } diff --git a/src/coreos.rs b/src/coreos.rs index 376cf4b..2a129c9 100755 --- a/src/coreos.rs +++ b/src/coreos.rs @@ -34,23 +34,9 @@ pub(crate) struct SystemVersionWithTimestamp { /// Paths to version files for different systems const COREOS_ALEPH_PATH: &str = ".coreos-aleph-version.json"; +#[allow(dead_code)] const DEBIAN_VERSION_PATH: &str = ".debian-version.json"; -/// Get version information for CoreOS or Debian systems -pub(crate) fn get_system_version(root: &Path) -> Result> { - // Try CoreOS aleph version first - if let Some(version) = get_coreos_version(root)? { - return Ok(Some(version)); - } - - // Try Debian version - if let Some(version) = get_debian_version(root)? { - return Ok(Some(version)); - } - - Ok(None) -} - /// Get CoreOS aleph version (original functionality) pub(crate) fn get_coreos_version(root: &Path) -> Result> { let path = &root.join(COREOS_ALEPH_PATH); @@ -61,10 +47,13 @@ pub(crate) fn get_coreos_version(root: &Path) -> Result Result Result> { let path = &root.join(DEBIAN_VERSION_PATH); if !path.exists() { @@ -81,10 +71,13 @@ pub(crate) fn get_debian_version(root: &Path) -> Result Result<()> { let tempdir = tempfile::tempdir()?; - std::fs::write(tempdir.path().join(DEBIAN_VERSION_PATH), DEBIAN_VERSION_DATA)?; + std::fs::write( + tempdir.path().join(DEBIAN_VERSION_PATH), + DEBIAN_VERSION_DATA, + )?; let result = get_debian_version(tempdir.path())?; let Some(result) = result else { anyhow::bail!("Expected Some result"); }; assert_eq!(result.version_info.version, "12.1"); - assert_eq!(result.version_info.ref_name, Some("debian/bookworm/amd64".to_string())); + assert_eq!( + result.version_info.ref_name, + Some("debian/bookworm/amd64".to_string()) + ); Ok(()) } } diff --git a/src/ostreeutil.rs b/src/ostreeutil.rs index 4f26e2a..f1a30c5 100755 --- a/src/ostreeutil.rs +++ b/src/ostreeutil.rs @@ -5,10 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use std::path::Path; - use anyhow::{Context, Result}; -use log::debug; /// https://github.com/coreos/rpm-ostree/pull/969/commits/dc0e8db5bd92e1f478a0763d1a02b48e57022b59 #[cfg(any( @@ -18,49 +15,6 @@ use log::debug; ))] pub(crate) const BOOT_PREFIX: &str = "usr/lib/ostree-boot"; -/// Detect if this is a Debian-based system -fn is_debian_system(sysroot: &Path) -> bool { - // Check for Debian-specific files - let debian_files = [ - "etc/debian_version", - "var/lib/dpkg/status", - ]; - - for file in debian_files.iter() { - if sysroot.join(file).exists() { - return true; - } - } - - // Check os-release content - if let Ok(content) = std::fs::read_to_string(sysroot.join("etc/os-release")) { - if content.contains("ID=debian") || - content.contains("ID=ubuntu") || - content.contains("ID=linuxmint") || - content.contains("ID=pop") { - return true; - } - } - - false -} - -/// Create dpkg command for Debian systems -pub(crate) fn dpkg_cmd>(sysroot: P) -> Result { - let c = std::process::Command::new("dpkg"); - let sysroot = sysroot.as_ref(); - - // Check if this is a Debian system - if !is_debian_system(sysroot) { - anyhow::bail!("Not a Debian system - dpkg command not available"); - } - - // For OSTree systems, we might need to adjust paths - // but dpkg typically works with the standard paths - debug!("Using dpkg for Debian system"); - Ok(c) -} - /// Get sysroot.bootloader in ostree repo config. pub(crate) fn get_ostree_bootloader() -> Result> { let mut cmd = std::process::Command::new("ostree"); diff --git a/src/packagesystem.rs b/src/packagesystem.rs index f3a9875..840a822 100755 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -8,7 +8,9 @@ use crate::model::*; /// Parse the output of `dpkg -S` to extract package names fn parse_dpkg_s_output(output: &[u8]) -> Result { - let output_str = std::str::from_utf8(output)?; + let output_str = std::str::from_utf8(output) + .with_context(|| "dpkg output is not valid UTF-8")?; + // dpkg -S outputs "package: /path" format // Package names can contain colons (e.g., "grub-efi-amd64:amd64") // We need to find the colon that is followed by a space (start of file path) @@ -26,22 +28,31 @@ fn parse_dpkg_s_output(output: &[u8]) -> Result { } if let Some(pos) = colon_pos { - Ok(output_str[..pos].trim().to_string()) + let package_name = output_str[..pos].trim(); + if package_name.is_empty() { + bail!("Package name is empty in dpkg output: {}", output_str); + } + Ok(package_name.to_string()) } else { - bail!("Invalid dpkg -S output format: {}", output_str) + bail!("Invalid dpkg -S output format (no package:path separator): {}", output_str) } } /// Get package installation time from package.list file fn get_package_install_time(package: &str) -> Result> { + if package.is_empty() { + bail!("Package name cannot be empty"); + } + let list_path = format!("/var/lib/dpkg/info/{}.list", package); let metadata = std::fs::metadata(&list_path) - .with_context(|| format!("Failed to get metadata for package {}", package))?; - + .with_context(|| format!("Failed to get metadata for package '{}' at path '{}'", package, list_path))?; + // Use modification time as installation time - let modified = metadata.modified() - .with_context(|| format!("Failed to get modification time for package {}", package))?; - + let modified = metadata + .modified() + .with_context(|| format!("Failed to get modification time for package '{}'", package))?; + Ok(DateTime::from(modified)) } @@ -50,19 +61,21 @@ fn dpkg_parse_metadata(packages: &BTreeSet) -> Result { if packages.is_empty() { bail!("Failed to find any Debian packages matching files in source efidir"); } - + let mut timestamps = BTreeSet::new(); - + // Get installation time for each package for package in packages { let timestamp = get_package_install_time(package)?; timestamps.insert(timestamp); } - + // Use the most recent timestamp - let largest_timestamp = timestamps.iter().last() + let largest_timestamp = timestamps + .iter() + .last() .ok_or_else(|| anyhow::anyhow!("No valid timestamps found"))?; - + // Create version string from package names let version = packages.iter().fold("".to_string(), |mut s, n| { if !s.is_empty() { @@ -71,7 +84,7 @@ fn dpkg_parse_metadata(packages: &BTreeSet) -> Result { s.push_str(n); s }); - + Ok(ContentMetadata { timestamp: *largest_timestamp, version, @@ -86,40 +99,67 @@ pub(crate) fn query_files( where T: AsRef, { + if sysroot_path.is_empty() { + bail!("sysroot_path cannot be empty"); + } + let mut packages = BTreeSet::new(); let paths: Vec<_> = paths.into_iter().collect(); + if paths.is_empty() { + bail!("No paths provided to query"); + } + + log::debug!("Querying dpkg database for {} paths in sysroot: {}", paths.len(), sysroot_path); + for path in &paths { - // Use dpkg -S to find which package owns the file - let mut cmd = std::process::Command::new("dpkg"); - cmd.args(["-S", "--root", sysroot_path]); - cmd.arg(path.as_ref()); - - let dpkgout = cmd.output()?; - if !dpkgout.status.success() { - // Skip files that don't belong to any package + let path_ref = path.as_ref(); + if path_ref.to_string_lossy().is_empty() { + log::warn!("Skipping empty path"); continue; } - let package = parse_dpkg_s_output(&dpkgout.stdout)?; + // Use dpkg -S to find which package owns the file + let mut cmd = std::process::Command::new("dpkg"); + cmd.args(["-S", "--root", sysroot_path]); + cmd.arg(path_ref); + + let dpkgout = cmd.output() + .with_context(|| format!("Failed to execute dpkg command for path: {:?}", path_ref))?; + + if !dpkgout.status.success() { + // Skip files that don't belong to any package + log::debug!("File {:?} does not belong to any package (dpkg exit code: {})", + path_ref, dpkgout.status); + continue; + } + + let package = parse_dpkg_s_output(&dpkgout.stdout) + .with_context(|| format!("Failed to parse dpkg output for path: {:?}", path_ref))?; packages.insert(package); } - + if packages.is_empty() { + log::debug!("No packages found with --root, trying local system"); // If no packages found, try without --root for local system for path in &paths { + let path_ref = path.as_ref(); let mut cmd = std::process::Command::new("dpkg"); cmd.args(["-S"]); - cmd.arg(path.as_ref()); - - let dpkgout = cmd.output()?; + cmd.arg(path_ref); + + let dpkgout = cmd.output() + .with_context(|| format!("Failed to execute local dpkg command for path: {:?}", path_ref))?; + if dpkgout.status.success() { - let package = parse_dpkg_s_output(&dpkgout.stdout)?; + let package = parse_dpkg_s_output(&dpkgout.stdout) + .with_context(|| format!("Failed to parse local dpkg output for path: {:?}", path_ref))?; packages.insert(package); } } } - + + log::debug!("Found {} packages for {} paths", packages.len(), paths.len()); dpkg_parse_metadata(&packages) } @@ -128,26 +168,45 @@ fn test_parse_dpkg_s_output() { let testdata = "grub-efi-amd64:amd64: /usr/lib/grub/x86_64-efi"; let parsed = parse_dpkg_s_output(testdata.as_bytes()).unwrap(); assert_eq!(parsed, "grub-efi-amd64:amd64"); - + let testdata2 = "shim-signed: /usr/lib/shim/shimx64.efi"; let parsed2 = parse_dpkg_s_output(testdata2.as_bytes()).unwrap(); assert_eq!(parsed2, "shim-signed"); - + // Test with different package name format let testdata3 = "grub-efi-amd64: /usr/lib/grub/x86_64-efi"; let parsed3 = parse_dpkg_s_output(testdata3.as_bytes()).unwrap(); assert_eq!(parsed3, "grub-efi-amd64"); } +#[test] +fn test_parse_dpkg_s_output_errors() { + // Test invalid UTF-8 + let invalid_utf8 = b"package\xff: /path"; + assert!(parse_dpkg_s_output(invalid_utf8).is_err()); + + // Test empty package name + let empty_package = ": /path"; + assert!(parse_dpkg_s_output(empty_package.as_bytes()).is_err()); + + // Test no separator + let no_separator = "package /path"; + assert!(parse_dpkg_s_output(no_separator.as_bytes()).is_err()); + + // Test only colon + let only_colon = ":"; + assert!(parse_dpkg_s_output(only_colon.as_bytes()).is_err()); +} + #[test] fn test_dpkg_parse_metadata() { // Mock package installation times for testing let mock_time = Utc::now(); - + let mut packages = BTreeSet::new(); packages.insert("grub-efi-amd64:amd64".to_string()); packages.insert("shim-signed".to_string()); - + // For testing, we'll create a mock version that doesn't depend on actual files let version = packages.iter().fold("".to_string(), |mut s, n| { if !s.is_empty() { @@ -156,17 +215,19 @@ fn test_dpkg_parse_metadata() { s.push_str(n); s }); - + // Create a mock ContentMetadata for testing let mock_metadata = ContentMetadata { timestamp: mock_time, version, }; - - assert_eq!( - mock_metadata.version, - "grub-efi-amd64:amd64,shim-signed" - ); + + assert_eq!(mock_metadata.version, "grub-efi-amd64:amd64,shim-signed"); // Timestamp should be recent - assert!(mock_metadata.timestamp > DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z").unwrap().with_timezone(&Utc)); + assert!( + mock_metadata.timestamp + > DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z") + .unwrap() + .with_timezone(&Utc) + ); }