Skip to content
Merged
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
34 changes: 17 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "avocado-cli"
version = "0.27.0"
version = "0.28.0"
edition = "2021"
description = "Command line interface for Avocado."
authors = ["Avocado"]
Expand Down
36 changes: 34 additions & 2 deletions src/commands/connect/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,9 @@ fn format_version_from_manifest(manifest: &serde_json::Value) -> String {
// Artifact discovery (manifest-driven, with SHA256 hashing for TUF)
// ---------------------------------------------------------------------------

/// Discover artifacts from the manifest's extensions list.
/// Discover artifacts from the manifest's extensions list and os_bundle.
/// Each extension has an `image_id` that maps to a `{image_id}.raw` file on disk.
/// The os_bundle (if present) is also included as an artifact.
/// Computes SHA256 of each artifact for TUF target metadata.
fn discover_artifacts(dir: &Path, manifest: &serde_json::Value) -> Result<Vec<ArtifactInfo>> {
let images_dir = dir.join("lib/avocado/images");
Expand Down Expand Up @@ -423,8 +424,39 @@ fn discover_artifacts(dir: &Path, manifest: &serde_json::Value) -> Result<Vec<Ar
});
}

// Include OS bundle artifact if present in manifest
if let Some(os_bundle) = manifest.get("os_bundle") {
let image_id = os_bundle
.get("image_id")
.and_then(|v| v.as_str())
.context("os_bundle missing 'image_id' field")?;

let path = images_dir.join(format!("{image_id}.raw"));
if !path.exists() {
anyhow::bail!(
"OS bundle artifact not found: {} (image_id: {})",
path.display(),
image_id,
);
}

let size_bytes = std::fs::metadata(&path)
.with_context(|| format!("Failed to stat {}", path.display()))?
.len();

let sha256 =
compute_sha256(&path).with_context(|| format!("Failed to hash {}", path.display()))?;

artifacts.push(ArtifactInfo {
image_id: image_id.to_string(),
path,
size_bytes,
sha256,
});
}

if artifacts.is_empty() {
anyhow::bail!("No extensions found in manifest. Have you run 'avocado build'?");
anyhow::bail!("No artifacts found in manifest. Have you run 'avocado build'?");
}

Ok(artifacts)
Expand Down
94 changes: 83 additions & 11 deletions src/commands/ext/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use crate::utils::config::{ComposedConfig, Config, ExtensionLocation};
use crate::utils::container::{RunConfig, SdkContainer};
use crate::utils::output::{print_info, print_success, OutputLevel};
use crate::utils::stamps::{
compute_ext_input_hash, generate_batch_read_stamps_script, generate_write_stamp_script,
resolve_required_stamps, validate_stamps_batch, Stamp, StampCommand, StampComponent,
StampOutputs,
compute_ext_input_hash, compute_ext_input_hash_with_fs, generate_batch_read_stamps_script,
generate_write_stamp_script, resolve_required_stamps, validate_stamps_batch, Stamp,
StampCommand, StampComponent, StampOutputs,
};
use crate::utils::target::resolve_target_required;

Expand Down Expand Up @@ -111,6 +111,16 @@ impl ExtImageCommand {
.get_sdk_image()
.ok_or_else(|| anyhow::anyhow!("No SDK container image specified in configuration."))?;

// Resolve the effective filesystem for this extension early — needed for stamp hashing.
// If the extension explicitly sets `filesystem`, use that; otherwise inherit from rootfs.
let rootfs_fs = config.get_rootfs_filesystem();
let effective_fs = parsed
.get("extensions")
.and_then(|e| e.get(&self.extension))
.and_then(|ext| ext.get("filesystem"))
.and_then(|v| v.as_str())
.unwrap_or(&rootfs_fs);

// Validate stamps before proceeding (unless --no-stamps)
if !self.no_stamps {
let container_helper =
Expand Down Expand Up @@ -148,7 +158,8 @@ impl ExtImageCommand {
.await?;

// Compute current inputs from composed config for staleness detection.
// Only compare against Extension stamps — SDK/compile-deps stamps use their own hash.
// Use the base hash (without filesystem) to match what install/build stamps wrote.
// The filesystem-aware hash is only used when writing/reading the image stamp itself.
let current_inputs = compute_ext_input_hash(parsed, &self.extension).ok();
let validation = validate_stamps_batch(
&required,
Expand Down Expand Up @@ -326,17 +337,19 @@ impl ExtImageCommand {
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
.unwrap_or_else(|| vec!["sysext", "confext"]);

// Get filesystem type (defaults to "squashfs")
// Use the effective filesystem resolved earlier (from ext config or rootfs default).
// The per-extension `filesystem` key can still override via ext_config, but
// effective_fs already accounts for that from the raw parsed YAML.
let filesystem = ext_config
.get("filesystem")
.and_then(|v| v.as_str())
.unwrap_or("squashfs");
.unwrap_or(effective_fs);

match filesystem {
"squashfs" | "erofs" => {}
"squashfs" | "erofs" | "erofs-lz4" | "erofs-zst" => {}
other => {
return Err(anyhow::anyhow!(
"Extension '{}' has invalid filesystem type '{}'. Must be 'squashfs' or 'erofs'.",
"Extension '{}' has invalid filesystem type '{}'. Must be 'squashfs', 'erofs', 'erofs-lz4', or 'erofs-zst'.",
self.extension,
other
));
Expand Down Expand Up @@ -447,7 +460,8 @@ impl ExtImageCommand {

// Write extension image stamp (unless --no-stamps)
if !self.no_stamps {
let inputs = compute_ext_input_hash(parsed, &self.extension)?;
let inputs =
compute_ext_input_hash_with_fs(parsed, &self.extension, Some(filesystem))?;
let outputs = StampOutputs::default();
let stamp = Stamp::ext_image(&self.extension, &target, inputs, outputs);
let stamp_script = generate_write_stamp_script(&stamp)?;
Expand Down Expand Up @@ -625,7 +639,12 @@ impl ExtImageCommand {
.collect::<Vec<_>>();

let mkfs_command = match filesystem {
"erofs" => {
"erofs" | "erofs-lz4" | "erofs-zst" => {
let compress_flag = match filesystem {
"erofs-lz4" => "\n -z lz4hc \\",
"erofs-zst" => "\n -z zstd \\",
_ => "",
};
let exclude_flags = var_excludes
.iter()
.map(|p| format!(" --exclude-path={p} \\"))
Expand All @@ -642,7 +661,7 @@ mkfs.erofs \
-T "$SOURCE_DATE_EPOCH" \
-U 00000000-0000-0000-0000-000000000000 \
-x -1 \
--all-root \{exclude_section}
--all-root \{compress_flag}{exclude_section}
"$OUTPUT_FILE" \
"$AVOCADO_EXT_SYSROOTS/$EXT_NAME""#
)
Expand Down Expand Up @@ -745,6 +764,59 @@ mod tests {
);
}

#[test]
fn test_create_build_script_erofs_lz4_includes_compression() {
let cmd = make_cmd("my-ext");
let script = cmd.create_build_script("1.0.0", "sysext", 0, "erofs-lz4", &[]);

assert!(
script.contains("mkfs.erofs"),
"erofs-lz4 should invoke mkfs.erofs"
);
assert!(
script.contains("-z lz4hc"),
"erofs-lz4 should include -z lz4hc compression flag"
);
assert!(
!script.contains("-z zstd"),
"erofs-lz4 should not include zstd compression"
);
}

#[test]
fn test_create_build_script_erofs_zst_includes_compression() {
let cmd = make_cmd("my-ext");
let script = cmd.create_build_script("1.0.0", "sysext", 0, "erofs-zst", &[]);

assert!(
script.contains("mkfs.erofs"),
"erofs-zst should invoke mkfs.erofs"
);
assert!(
script.contains("-z zstd"),
"erofs-zst should include -z zstd compression flag"
);
assert!(
!script.contains("-z lz4hc"),
"erofs-zst should not include lz4 compression"
);
}

#[test]
fn test_create_build_script_erofs_uncompressed_no_z_flag() {
let cmd = make_cmd("my-ext");
let script = cmd.create_build_script("1.0.0", "sysext", 0, "erofs", &[]);

assert!(
script.contains("mkfs.erofs"),
"erofs should invoke mkfs.erofs"
);
assert!(
!script.contains("-z "),
"plain erofs should not include any -z compression flag"
);
}

#[test]
fn test_create_build_script_squashfs_contains_reproducible_flags() {
let cmd = make_cmd("my-ext");
Expand Down
Loading
Loading