Add Forgejo Actions workflows for automated builds and artifact uploads
- Add comprehensive build-artifacts.yml workflow with Forgejo Package Registry upload - Add simple-build.yml workflow for basic artifact management - Update README.md with workflow documentation and setup instructions - Fix debian/rules to correctly create bootupctl symlink to /usr/libexec/bootupd - Improve error handling and validation throughout the codebase - Remove unused functions and imports - Update documentation to clarify bootupd is not a daemon - Fix binary layout to match RPM packaging pattern
This commit is contained in:
parent
aaf662d5b1
commit
95c23891b6
10 changed files with 790 additions and 145 deletions
238
.forgejo/workflows/build-artifacts.yml
Normal file
238
.forgejo/workflows/build-artifacts.yml
Normal file
|
|
@ -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"
|
||||
54
.forgejo/workflows/simple-build.yml
Normal file
54
.forgejo/workflows/simple-build.yml
Normal file
|
|
@ -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
|
||||
148
README.md
148
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,15 +43,59 @@ 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:
|
||||
- `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
|
||||
|
||||
```bash
|
||||
|
|
@ -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
|
||||
|
|
|
|||
212
deb-bootupd.md
212
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 <your-fork-url>
|
||||
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
|
||||
|
||||
|
|
|
|||
4
debian/rules
vendored
4
debian/rules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::<Result<Vec<_>>>()?
|
||||
} 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(
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Option<SystemVersionWithTimestamp>> {
|
||||
// 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<Option<SystemVersionWithTimestamp>> {
|
||||
let path = &root.join(COREOS_ALEPH_PATH);
|
||||
|
|
@ -63,7 +49,10 @@ pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
let aleph: SystemVersion = serde_json::from_reader(bufr)?;
|
||||
|
||||
// Use created time if available, otherwise fall back to modified time
|
||||
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
|
||||
let ts = meta
|
||||
.created()
|
||||
.unwrap_or_else(|_| meta.modified().unwrap())
|
||||
.into();
|
||||
|
||||
Ok(Some(SystemVersionWithTimestamp {
|
||||
version_info: aleph,
|
||||
|
|
@ -72,6 +61,7 @@ pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
}
|
||||
|
||||
/// Get Debian version information
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_debian_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
|
||||
let path = &root.join(DEBIAN_VERSION_PATH);
|
||||
if !path.exists() {
|
||||
|
|
@ -83,7 +73,10 @@ pub(crate) fn get_debian_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
let deb_version: SystemVersion = serde_json::from_reader(bufr)?;
|
||||
|
||||
// Use created time if available, otherwise fall back to modified time
|
||||
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
|
||||
let ts = meta
|
||||
.created()
|
||||
.unwrap_or_else(|_| meta.modified().unwrap())
|
||||
.into();
|
||||
|
||||
Ok(Some(SystemVersionWithTimestamp {
|
||||
version_info: deb_version,
|
||||
|
|
@ -175,13 +168,19 @@ mod test {
|
|||
#[test]
|
||||
fn test_parse_debian_version() -> 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<P: AsRef<Path>>(sysroot: P) -> Result<std::process::Command> {
|
||||
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<Option<String>> {
|
||||
let mut cmd = std::process::Command::new("ostree");
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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,21 +28,30 @@ fn parse_dpkg_s_output(output: &[u8]) -> Result<String> {
|
|||
}
|
||||
|
||||
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<DateTime<Utc>> {
|
||||
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))
|
||||
}
|
||||
|
|
@ -60,7 +71,9 @@ fn dpkg_parse_metadata(packages: &BTreeSet<String>) -> Result<ContentMetadata> {
|
|||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -86,40 +99,67 @@ pub(crate) fn query_files<T>(
|
|||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
if sysroot_path.is_empty() {
|
||||
bail!("sysroot_path cannot be empty");
|
||||
}
|
||||
|
||||
let mut packages = BTreeSet::new();
|
||||
let paths: Vec<_> = paths.into_iter().collect();
|
||||
|
||||
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());
|
||||
if paths.is_empty() {
|
||||
bail!("No paths provided to query");
|
||||
}
|
||||
|
||||
let dpkgout = cmd.output()?;
|
||||
if !dpkgout.status.success() {
|
||||
// Skip files that don't belong to any package
|
||||
log::debug!("Querying dpkg database for {} paths in sysroot: {}", paths.len(), sysroot_path);
|
||||
|
||||
for path in &paths {
|
||||
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());
|
||||
cmd.arg(path_ref);
|
||||
|
||||
let dpkgout = cmd.output()
|
||||
.with_context(|| format!("Failed to execute local dpkg command for path: {:?}", path_ref))?;
|
||||
|
||||
let dpkgout = cmd.output()?;
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +179,25 @@ fn test_parse_dpkg_s_output() {
|
|||
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
|
||||
|
|
@ -163,10 +222,12 @@ fn test_dpkg_parse_metadata() {
|
|||
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)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue