Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
**/fuzz/target/
**/fuzz/corpus/
**/fuzz/artifacts/
.worktrees
*.rpm
18 changes: 18 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ fmt:
check-fuzz:
cargo check --manifest-path crates/composefs/fuzz/Cargo.toml

# Run unit + non-privileged integration tests (no VM, no root)
test-all: test test-integration

# Run all checks (clippy + fmt + test + fuzz build)
check: clippy check-feature-combos fmt-check test check-fuzz

Expand Down Expand Up @@ -90,6 +93,21 @@ test-integration-vm *ARGS: build _integration-container-build
install-nextest:
@which cargo-nextest > /dev/null 2>&1 || cargo install cargo-nextest --locked

# Build and run a bls example locally.
# Usage: just test-example-local bls arch
# just test-example-local bls arch fsfmt=ext4 verity=none
# 'fsfmt' defaults to ext4, 'verity' defaults to none (no fs-verity enforcement).
# Requires: qemu-kvm, OVMF, skopeo, mtools, fsverity, mkfs.erofs, systemd-repart, podman.
test-example-local example os fsfmt="ext4" verity="none": build
#!/usr/bin/env bash
set -euo pipefail
export FS_FORMAT={{ fsfmt }}
export FS_VERITY_MODE={{ verity }}
export CFSCTL_PATH=$(pwd)/target/debug/cfsctl
cd examples
{{ example }}/build {{ os }}
TEST_IMAGE="{{ example }}/{{ os }}-{{ example }}-efi.qcow2" pytest test -v

# Run everything: checks + full integration tests including VM
ci: check test-integration-vm

Expand Down
81 changes: 56 additions & 25 deletions bootc/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
# This builds and tests bootc against the local composefs-rs checkout
# using bootc's auto-detection of path dependencies via `cargo xtask local-rust-deps`.
# --------------------------------------------------------------------
#
# NOTE: composefs-boot/src/cmdline.rs contains karg parsing logic (ComposefsCmdline,
# KARG_V1, KARG_V2) that must be kept in sync with bootc's crates/initramfs/src/lib.rs
# manually, since bootc does not yet depend on composefs-boot directly. When changing
# karg handling, verify both sides. The composefs-rs cmdline.rs is the source of truth.
#
# NOTE: cfsctl init now defaults to --erofs v1 (V1-only format set). bootc needs
# --erofs dual so that both V1 EROFS (composefs.digest=) and V2 EROFS (composefs=) are
# generated for each image. bootc's own init scripts must pass --erofs dual; this
# Justfile does not inject that flag.

# Configuration variables (override via environment or command line)
# Example: COMPOSEFS_BOOTC_REF=v1.0.0 just bootc/build
Expand Down Expand Up @@ -51,47 +61,65 @@ patch: clone
#!/bin/bash
set -euo pipefail

# Require a clean composefs-rs working tree so we test a real commit
# Require a clean composefs-rs working tree so we test a real commit.
# Only tracked files matter; untracked files are allowed.
# git diff HEAD already excludes untracked files.
if ! git -C "$_COMPOSEFS_SRC" diff --quiet HEAD 2>/dev/null; then
echo "error: composefs-rs has uncommitted changes — commit or stash first" >&2
git -C "$_COMPOSEFS_SRC" status --short >&2
git -C "$_COMPOSEFS_SRC" diff --stat HEAD >&2
exit 1
fi

cfs_path="$_COMPOSEFS_SRC/crates/cfsctl"
cfs_crates="$_COMPOSEFS_SRC/crates"

cd "$COMPOSEFS_BOOTC_PATH"

# Add or update the [patch] section with a path override
patch_value="cfsctl = { path = \"${cfs_path}\" } # Patched by composefs-rs"
if grep -q '^[[:space:]]*\[patch\."https://github.com/composefs/composefs-rs"\]' Cargo.toml; then
# Patch section already exists (uncommented) — replace the cfsctl line
sed -i '/^[[:space:]]*\[patch\."https:\/\/github.com\/composefs\/composefs-rs"\]/,/^$\|^\[/{
s|^cfsctl = .*|'"$patch_value"'|
}' Cargo.toml
else
# No patch section yet — append one
{
# Crates to override: all composefs-rs workspace members that bootc may
# depend on directly. Cargo resolves transitive workspace deps automatically
# once any workspace member is patched by path, but direct deps in bootc's
# own Cargo.toml need an explicit [patch] entry. Extra entries for crates
# bootc doesn't use yet are harmless (Cargo warns but does not error).
#
# Two patch-section URLs are written because the canonical repo moved from
# github.com/composefs/composefs-rs to github.com/containers/composefs-rs;
# bootc's Cargo.toml may use either. Having both is harmless.
local_crates=(composefs composefs-boot composefs-oci composefs-ctl)

_rev=$(git -C "$_COMPOSEFS_SRC" rev-parse HEAD)

# Build the block of patch entries
patch_entries=""
for crate in "${local_crates[@]}"; do
patch_entries+="${crate} = { path = \"${cfs_crates}/${crate}\" }"$'\n'
done

# Remove any existing composefs-rs [patch] sections (both URLs), then
# append fresh ones. The remove-and-reappend approach is simpler than
# trying to surgically update individual lines when the entry count changes.
for url in "https://github.com/composefs/composefs-rs" "https://github.com/containers/composefs-rs"; do
escaped_url="${url//\//\\/}"
sed -i "/^\[patch\.\"${escaped_url}\"\]/,/^$/d" Cargo.toml
done

{
echo ''
echo "# Patched by composefs-rs at ${_rev}"
for url in "https://github.com/composefs/composefs-rs" "https://github.com/containers/composefs-rs"; do
echo "[patch.\"${url}\"]"
printf '%s' "$patch_entries"
echo ''
echo '# Patched by composefs-rs CI to test against local composefs-rs'
echo '[patch."https://github.com/composefs/composefs-rs"]'
echo "$patch_value"
} >> Cargo.toml
fi
done
} >> Cargo.toml

# Patch the workspace lints to allow missing_docs for composefs-rs crates
# bootc has workspace.lints.rust.missing_docs = "deny" but composefs-rs has undocumented items
# Patch the workspace lints to allow missing_docs for composefs-rs crates.
# bootc has workspace.lints.rust.missing_docs = "deny" but some composefs-rs
# items lack documentation.
sed -i 's/missing_docs = "deny"/missing_docs = "allow"/' Cargo.toml

# Cargo.lock will be updated on the next build/check.
# We intentionally don't run `cargo update` here because it rewrites
# the workspace dependency line in Cargo.toml (replacing git+rev with path).

# Update the rev comment in the [patch] section so Cargo.toml actually
# changes when composefs-rs moves to a new commit. Since the file is
# part of the podman build context this busts the layer cache.
_rev=$(git -C "$_COMPOSEFS_SRC" rev-parse HEAD)
sed -i "s/^# Patched by composefs-rs.*/# Patched by composefs-rs at ${_rev}/" Cargo.toml
echo "bootc patched for composefs-rs at ${_rev}"

# Build sealed bootc image using local composefs-rs
Expand Down Expand Up @@ -156,6 +184,9 @@ config:
Environment Variables:
COMPOSEFS_BOOTC_PATH - Override bootc checkout path
COMPOSEFS_BOOTC_REF - Override bootc git ref (branch/tag/PR)
Use this when composefs-rs has API-breaking changes
that require a matching bootc branch, e.g.:
COMPOSEFS_BOOTC_REF=refs/pull/123/head just bootc/build
COMPOSEFS_BOOTC_REPO - Override bootc git repository

Test Parameters:
Expand Down
27 changes: 15 additions & 12 deletions crates/composefs-boot/src/bootloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use composefs::{
tree::{DirectoryRef, FileSystem, ImageError, Inode, LeafContent, RegularFile},
};

use crate::cmdline::{make_cmdline_composefs, split_cmdline};
use crate::cmdline::split_cmdline;

/// Strips the key (if it matches) plus the following whitespace from a single line in a "Type #1
/// Boot Loader Specification Entry" file.
Expand Down Expand Up @@ -139,11 +139,15 @@ impl BootLoaderEntryFile {
self.lines.push(format!("options {arg}"));
}

/// Adjusts the kernel command-line arguments by adding a composefs= parameter (if appropriate)
/// and adding additional arguments, as requested.
pub fn adjust_cmdline(&mut self, composefs: Option<&str>, insecure: bool, extra: &[&str]) {
if let Some(id) = composefs {
self.add_cmdline(&make_cmdline_composefs(id, insecure));
/// Adjusts the kernel command-line arguments by adding a composefs karg (if provided)
/// and adding additional arguments.
///
/// `karg` should be a complete kernel argument string such as
/// `"composefs.digest=abc123"` or `"composefs=abc123"` as produced by
/// [`composefs_boot::cmdline::ComposefsCmdline::to_cmdline_arg`].
pub fn adjust_cmdline(&mut self, karg: Option<&str>, extra: &[&str]) {
if let Some(k) = karg {
self.add_cmdline(k);
}

for item in extra {
Expand Down Expand Up @@ -729,7 +733,7 @@ mod tests {
#[test]
fn test_adjust_cmdline_with_composefs() {
let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
entry.adjust_cmdline(Some("abc123"), false, &["quiet", "splash"]);
entry.adjust_cmdline(Some("composefs=abc123"), &["quiet", "splash"]);

assert_eq!(entry.lines.len(), 3);
assert_eq!(entry.lines[2], "options composefs=abc123 quiet splash");
Expand All @@ -738,17 +742,16 @@ mod tests {
#[test]
fn test_adjust_cmdline_with_composefs_insecure() {
let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
entry.adjust_cmdline(Some("abc123"), true, &[]);
entry.adjust_cmdline(Some("composefs=?abc123"), &[]);

assert_eq!(entry.lines.len(), 3);
// Assuming make_cmdline_composefs adds digest=off for insecure mode
assert!(entry.lines[2].contains("abc123"));
assert_eq!(entry.lines[2], "options composefs=?abc123");
}

#[test]
fn test_adjust_cmdline_no_composefs() {
let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
entry.adjust_cmdline(None, false, &["quiet", "splash"]);
entry.adjust_cmdline(None, &["quiet", "splash"]);

assert_eq!(entry.lines.len(), 3);
assert_eq!(entry.lines[2], "options quiet splash");
Expand All @@ -757,7 +760,7 @@ mod tests {
#[test]
fn test_adjust_cmdline_existing_options() {
let mut entry = BootLoaderEntryFile::new("title Test Entry\noptions root=/dev/sda1\n");
entry.adjust_cmdline(Some("abc123"), false, &["quiet"]);
entry.adjust_cmdline(Some("composefs=abc123"), &["quiet"]);

assert_eq!(entry.lines.len(), 2);
assert!(entry.lines[1].contains("root=/dev/sda1"));
Expand Down
Loading