From 45fd0aa727d27dcf5d19e34359584d88ea0fdefe Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Wed, 19 Nov 2025 16:09:52 -0500 Subject: [PATCH 1/2] require ext vsn and include in ext names for images Also update runtime construction to enable extensions for avocadoctl. --- src/commands/build.rs | 46 +++++++++++-- src/commands/ext/build.rs | 58 +++++++++++++++-- src/commands/ext/image.rs | 74 ++++++++++++++++++--- src/commands/ext/package.rs | 40 ++++++++++++ src/commands/runtime/build.rs | 83 ++++++++++++++++-------- src/commands/runtime/deps.rs | 4 +- src/commands/runtime/install.rs | 2 +- src/utils/container.rs | 110 ++++++++++++++++++++++++++++++++ 8 files changed, 371 insertions(+), 46 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index ba1e9d0..02f6e35 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -764,6 +764,43 @@ echo "External extension {extension_name} images are ready in output directory" // Initialize SDK container helper let container_helper = crate::utils::container::SdkContainer::new(); + // Query RPM version for the extension from the RPM database + // Use the same RPM configuration that was used during installation + let version_query_script = format!( + r#" +set -e +# Query RPM version for extension from RPM database using the same config as installation +RPM_CONFIGDIR=$AVOCADO_SDK_PREFIX/ext-rpm-config \ +RPM_ETCCONFIGDIR=$DNF_SDK_TARGET_PREFIX \ +rpm --root="$AVOCADO_EXT_SYSROOTS/{extension_name}" --dbpath=/var/lib/extension.d/rpm -q {extension_name} --queryformat '%{{VERSION}}' +"# + ); + + let version_query_config = crate::utils::container::RunConfig { + container_image: container_image.to_string(), + target: target.to_string(), + command: version_query_script, + verbose: self.verbose, + source_environment: true, + interactive: false, + repo_url: repo_url.clone(), + repo_release: repo_release.clone(), + container_args: merged_container_args.clone(), + dnf_args: self.dnf_args.clone(), + ..Default::default() + }; + + let ext_version = container_helper + .run_in_container_with_output(version_query_config) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to query RPM version for extension '{}'. The RPM database should contain this package. \ + This may indicate the extension was not properly installed via packages, or the RPM database is corrupted.", + extension_name + ) + })?; + // Create the image creation script let image_script = format!( r#" @@ -771,14 +808,15 @@ set -e # Common variables EXT_NAME="{extension_name}" +EXT_VERSION="{ext_version}" OUTPUT_DIR="$AVOCADO_PREFIX/output/extensions" -OUTPUT_FILE="$OUTPUT_DIR/$EXT_NAME.raw" +OUTPUT_FILE="$OUTPUT_DIR/$EXT_NAME-$EXT_VERSION.raw" # Create output directory mkdir -p $OUTPUT_DIR -# Remove existing file if it exists -rm -f "$OUTPUT_FILE" +# Remove existing file if it exists (including any old versions) +rm -f "$OUTPUT_DIR/$EXT_NAME"*.raw # Check if extension sysroot exists if [ ! -d "$AVOCADO_EXT_SYSROOTS/$EXT_NAME" ]; then @@ -793,7 +831,7 @@ mksquashfs \ -noappend \ -no-xattrs -echo "Successfully created image for versioned extension '$EXT_NAME' at $OUTPUT_FILE" +echo "Successfully created image for versioned extension '$EXT_NAME-$EXT_VERSION' at $OUTPUT_FILE" "# ); diff --git a/src/commands/ext/build.rs b/src/commands/ext/build.rs index 7ebd753..2e7e32c 100644 --- a/src/commands/ext/build.rs +++ b/src/commands/ext/build.rs @@ -206,6 +206,14 @@ impl ExtBuildCommand { .and_then(|v| v.as_str()) .unwrap_or("0.1.0"); + // Validate semver format + Self::validate_semver(ext_version).with_context(|| { + format!( + "Extension '{}' has invalid version '{}'. Version must be in semantic versioning format (e.g., '1.0.0', '2.1.3')", + self.extension, ext_version + ) + })?; + // Get overlay configuration let overlay_config = ext_config.get("overlay").map(|v| { if let Some(dir_str) = v.as_str() { @@ -459,7 +467,7 @@ impl ExtBuildCommand { #[allow(clippy::too_many_arguments)] fn create_sysext_build_script( &self, - _ext_version: &str, + ext_version: &str, ext_scopes: &[String], overlay_config: Option<&OverlayConfig>, modprobe_modules: &[String], @@ -564,7 +572,7 @@ echo "[INFO] Added custom on_merge command to release file: {command}""# set -e {}{} release_dir="$AVOCADO_EXT_SYSROOTS/{}/usr/lib/extension-release.d" -release_file="$release_dir/extension-release.{}" +release_file="$release_dir/extension-release.{}-{}" modules_dir="$AVOCADO_EXT_SYSROOTS/{}/usr/lib/modules" mkdir -p "$release_dir" @@ -595,6 +603,7 @@ fi users_section, self.extension, self.extension, + ext_version, self.extension, if reload_service_manager { "1" } else { "0" }, ext_scopes.join(" "), @@ -610,7 +619,7 @@ fi #[allow(clippy::too_many_arguments)] fn create_confext_build_script( &self, - _ext_version: &str, + ext_version: &str, ext_scopes: &[String], overlay_config: Option<&OverlayConfig>, enable_services: &[String], @@ -733,7 +742,7 @@ echo "[INFO] Added custom on_merge command to release file: {command}""# set -e {}{} release_dir="$AVOCADO_EXT_SYSROOTS/{}/etc/extension-release.d" -release_file="$release_dir/extension-release.{}" +release_file="$release_dir/extension-release.{}-{}" mkdir -p "$release_dir" echo "ID=_any" > "$release_file" @@ -762,6 +771,7 @@ fi users_section, self.extension, self.extension, + ext_version, if reload_service_manager { "1" } else { "0" }, ext_scopes.join(" "), self.extension, @@ -1345,6 +1355,38 @@ echo "Set proper permissions on authentication files""#, Ok(()) } + + /// Validate semantic versioning format (X.Y.Z where X, Y, Z are non-negative integers) + fn validate_semver(version: &str) -> Result<()> { + let parts: Vec<&str> = version.split('.').collect(); + + if parts.len() < 3 { + return Err(anyhow::anyhow!( + "Version must follow semantic versioning format with at least MAJOR.MINOR.PATCH components (e.g., '1.0.0', '2.1.3')" + )); + } + + // Validate the first 3 components (MAJOR.MINOR.PATCH) + for (i, part) in parts.iter().take(3).enumerate() { + // Handle pre-release and build metadata (e.g., "1.0.0-alpha" or "1.0.0+build") + let component = part.split(&['-', '+'][..]).next().unwrap_or(part); + + component.parse::().with_context(|| { + let component_name = match i { + 0 => "MAJOR", + 1 => "MINOR", + 2 => "PATCH", + _ => "component", + }; + format!( + "{} version component '{}' must be a non-negative integer in semantic versioning format", + component_name, component + ) + })?; + } + + Ok(()) + } } #[cfg(test)] @@ -1379,7 +1421,7 @@ mod tests { assert!(script.contains( "release_dir=\"$AVOCADO_EXT_SYSROOTS/test-ext/usr/lib/extension-release.d\"" )); - assert!(script.contains("release_file=\"$release_dir/extension-release.test-ext\"")); + assert!(script.contains("release_file=\"$release_dir/extension-release.test-ext-1.0\"")); assert!(script.contains("modules_dir=\"$AVOCADO_EXT_SYSROOTS/test-ext/usr/lib/modules\"")); assert!(script.contains("echo \"ID=_any\" > \"$release_file\"")); assert!(script.contains("echo \"EXTENSION_RELOAD_MANAGER=0\" >> \"$release_file\"")); @@ -1430,7 +1472,7 @@ mod tests { assert!(script .contains("release_dir=\"$AVOCADO_EXT_SYSROOTS/test-ext/etc/extension-release.d\"")); - assert!(script.contains("release_file=\"$release_dir/extension-release.test-ext\"")); + assert!(script.contains("release_file=\"$release_dir/extension-release.test-ext-1.0\"")); assert!(script.contains("echo \"ID=_any\" > \"$release_file\"")); assert!(script.contains("echo \"EXTENSION_RELOAD_MANAGER=0\" >> \"$release_file\"")); assert!(script.contains("echo \"CONFEXT_SCOPE=system\" >> \"$release_file\"")); @@ -2293,6 +2335,8 @@ mod tests { assert!(script.contains( "release_dir=\"$AVOCADO_EXT_SYSROOTS/avocado-dev/usr/lib/extension-release.d\"" )); + // Note: Script generation uses ext_version parameter which is "0.1.0" in create_sysext_build_script call + assert!(script.contains("release_file=\"$release_dir/extension-release.avocado-dev-")); assert!(script.contains("echo \"ID=_any\" > \"$release_file\"")); assert!(script.contains("echo \"SYSEXT_SCOPE=system\" >> \"$release_file\"")); } @@ -2335,6 +2379,8 @@ mod tests { assert!(script.contains("Creating user 'root'")); assert!(script .contains("release_dir=\"$AVOCADO_EXT_SYSROOTS/avocado-dev/etc/extension-release.d\"")); + // Note: Script generation uses ext_version parameter + assert!(script.contains("release_file=\"$release_dir/extension-release.avocado-dev-")); assert!(script.contains("echo \"ID=_any\" > \"$release_file\"")); assert!(script.contains("echo \"CONFEXT_SCOPE=system\" >> \"$release_file\"")); } diff --git a/src/commands/ext/image.rs b/src/commands/ext/image.rs index a56b24c..d41b797 100644 --- a/src/commands/ext/image.rs +++ b/src/commands/ext/image.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crate::utils::config::{Config, ExtensionLocation}; use crate::utils::container::{RunConfig, SdkContainer}; @@ -78,6 +78,25 @@ impl ExtImageCommand { anyhow::anyhow!("Extension '{}' not found in configuration.", self.extension) })?; + // Get extension version + let ext_version = ext_config + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + anyhow::anyhow!( + "Extension '{}' is missing required 'version' field", + self.extension + ) + })?; + + // Validate semver format + Self::validate_semver(ext_version).with_context(|| { + format!( + "Extension '{}' has invalid version '{}'. Version must be in semantic versioning format (e.g., '1.0.0', '2.1.3')", + self.extension, ext_version + ) + })?; + // Get extension types from the types array let ext_types = ext_config .get("types") @@ -134,6 +153,7 @@ impl ExtImageCommand { &container_helper, container_image, &target_arch, + ext_version, &ext_types.join(","), // Pass types for potential future use repo_url.as_ref(), repo_release.as_ref(), @@ -144,16 +164,18 @@ impl ExtImageCommand { if result { print_success( &format!( - "Successfully created image for extension '{}' (types: {}).", + "Successfully created image for extension '{}-{}' (types: {}).", self.extension, + ext_version, ext_types.join(", ") ), OutputLevel::Normal, ); } else { return Err(anyhow::anyhow!( - "Failed to create extension image for '{}'", - self.extension + "Failed to create extension image for '{}-{}'", + self.extension, + ext_version )); } @@ -166,13 +188,14 @@ impl ExtImageCommand { container_helper: &SdkContainer, container_image: &str, target_arch: &str, + ext_version: &str, extension_type: &str, repo_url: Option<&String>, repo_release: Option<&String>, merged_container_args: &Option>, ) -> Result { // Create the build script - let build_script = self.create_build_script(extension_type); + let build_script = self.create_build_script(ext_version, extension_type); // Execute the build script in the SDK container if self.verbose { @@ -197,15 +220,16 @@ impl ExtImageCommand { Ok(result) } - fn create_build_script(&self, _extension_type: &str) -> String { + fn create_build_script(&self, ext_version: &str, _extension_type: &str) -> String { format!( r#" set -e # Common variables EXT_NAME="{}" +EXT_VERSION="{}" OUTPUT_DIR="$AVOCADO_PREFIX/output/extensions" -OUTPUT_FILE="$OUTPUT_DIR/$EXT_NAME.raw" +OUTPUT_FILE="$OUTPUT_DIR/$EXT_NAME-$EXT_VERSION.raw" # Create output directory mkdir -p $OUTPUT_DIR @@ -225,8 +249,42 @@ mksquashfs \ "$OUTPUT_FILE" \ -noappend \ -no-xattrs + +echo "Created extension image: $OUTPUT_FILE" "#, - self.extension + self.extension, ext_version ) } + + /// Validate semantic versioning format (X.Y.Z where X, Y, Z are non-negative integers) + fn validate_semver(version: &str) -> Result<()> { + let parts: Vec<&str> = version.split('.').collect(); + + if parts.len() < 3 { + return Err(anyhow::anyhow!( + "Version must follow semantic versioning format with at least MAJOR.MINOR.PATCH components (e.g., '1.0.0', '2.1.3')" + )); + } + + // Validate the first 3 components (MAJOR.MINOR.PATCH) + for (i, part) in parts.iter().take(3).enumerate() { + // Handle pre-release and build metadata (e.g., "1.0.0-alpha" or "1.0.0+build") + let component = part.split(&['-', '+'][..]).next().unwrap_or(part); + + component.parse::().with_context(|| { + let component_name = match i { + 0 => "MAJOR", + 1 => "MINOR", + 2 => "PATCH", + _ => "component", + }; + format!( + "{} version component '{}' must be a non-negative integer in semantic versioning format", + component_name, component + ) + })?; + } + + Ok(()) + } } diff --git a/src/commands/ext/package.rs b/src/commands/ext/package.rs index c04fdd9..7882aae 100644 --- a/src/commands/ext/package.rs +++ b/src/commands/ext/package.rs @@ -158,6 +158,14 @@ impl ExtPackageCommand { })? .to_string(); + // Validate semver format + Self::validate_semver(&version).with_context(|| { + format!( + "Extension '{}' has invalid version '{}'. Version must be in semantic versioning format (e.g., '1.0.0', '2.1.3')", + self.extension, version + ) + })?; + // Generate defaults let name = self.extension.clone(); let release = ext_config @@ -245,6 +253,38 @@ impl ExtPackageCommand { format!("avocado_{}", target.replace('-', "_")) } + /// Validate semantic versioning format (X.Y.Z where X, Y, Z are non-negative integers) + fn validate_semver(version: &str) -> Result<()> { + let parts: Vec<&str> = version.split('.').collect(); + + if parts.len() < 3 { + return Err(anyhow::anyhow!( + "Version must follow semantic versioning format with at least MAJOR.MINOR.PATCH components (e.g., '1.0.0', '2.1.3')" + )); + } + + // Validate the first 3 components (MAJOR.MINOR.PATCH) + for (i, part) in parts.iter().take(3).enumerate() { + // Handle pre-release and build metadata (e.g., "1.0.0-alpha" or "1.0.0+build") + let component = part.split(&['-', '+'][..]).next().unwrap_or(part); + + component.parse::().with_context(|| { + let component_name = match i { + 0 => "MAJOR", + 1 => "MINOR", + 2 => "PATCH", + _ => "component", + }; + format!( + "{} version component '{}' must be a non-negative integer in semantic versioning format", + component_name, component + ) + })?; + } + + Ok(()) + } + /// Create the RPM package inside the container at $AVOCADO_PREFIX/output/extensions async fn create_rpm_package_in_container( &self, diff --git a/src/commands/runtime/build.rs b/src/commands/runtime/build.rs index 87b5e42..2a9f1b4 100644 --- a/src/commands/runtime/build.rs +++ b/src/commands/runtime/build.rs @@ -175,20 +175,26 @@ impl RuntimeBuildCommand { // Process local extensions defined in [ext.*] sections if let Some(ext_config) = parsed.get("ext").and_then(|v| v.as_table()) { - for (ext_name, _ext_data) in ext_config { + for (ext_name, ext_data) in ext_config { // Only process extensions that are required by this runtime if all_required_extensions.contains(ext_name) { + // Get version from extension config + let ext_version = ext_data + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("0.1.0"); + symlink_commands.push(format!( r#" -OUTPUT_EXT=$AVOCADO_PREFIX/output/extensions/{ext_name}.raw -RUNTIMES_EXT=$VAR_DIR/lib/avocado/extensions/{ext_name}.raw +OUTPUT_EXT=$AVOCADO_PREFIX/output/extensions/{ext_name}-{ext_version}.raw +RUNTIMES_EXT=$VAR_DIR/lib/avocado/extensions/{ext_name}-{ext_version}.raw if [ -f "$OUTPUT_EXT" ]; then if ! cmp -s "$OUTPUT_EXT" "$RUNTIMES_EXT" 2>/dev/null; then ln -f $OUTPUT_EXT $RUNTIMES_EXT fi else - echo "Missing image for extension {ext_name}." + echo "Missing image for extension {ext_name}-{ext_version}." fi"# )); processed_extensions.insert(ext_name.clone()); @@ -199,15 +205,16 @@ fi"# // Process external extensions (those required but not defined locally) for ext_name in &all_required_extensions { if !processed_extensions.contains(ext_name) { - // This is an external extension - add symlink command for it + // This is an external extension - use wildcard to find versioned file symlink_commands.push(format!( r#" -OUTPUT_EXT=$AVOCADO_PREFIX/output/extensions/{ext_name}.raw -RUNTIMES_EXT=$VAR_DIR/lib/avocado/extensions/{ext_name}.raw - -if [ -f "$OUTPUT_EXT" ]; then +# Find external extension {ext_name} with any version +OUTPUT_EXT=$(ls $AVOCADO_PREFIX/output/extensions/{ext_name}-*.raw 2>/dev/null | head -n 1) +if [ -n "$OUTPUT_EXT" ]; then + EXT_FILENAME=$(basename "$OUTPUT_EXT") + RUNTIMES_EXT=$VAR_DIR/lib/avocado/extensions/$EXT_FILENAME if ! cmp -s "$OUTPUT_EXT" "$RUNTIMES_EXT" 2>/dev/null; then - ln -f $OUTPUT_EXT $RUNTIMES_EXT + ln -f "$OUTPUT_EXT" "$RUNTIMES_EXT" fi else echo "Missing image for external extension {ext_name}." @@ -228,21 +235,47 @@ fi"# RUNTIME_NAME="{}" TARGET_ARCH="{}" +# Read OS VERSION_ID from rootfs +if [ -f "$AVOCADO_PREFIX/rootfs/etc/os-release" ]; then + # Source the os-release file and extract VERSION_ID + . "$AVOCADO_PREFIX/rootfs/etc/os-release" + if [ -z "$VERSION_ID" ]; then + echo "Warning: VERSION_ID not found in os-release, using 'unknown'" + VERSION_ID="unknown" + fi + echo "Using OS VERSION_ID: $VERSION_ID" +else + echo "Warning: /etc/os-release not found in rootfs, using VERSION_ID='unknown'" + VERSION_ID="unknown" +fi + VAR_DIR=$AVOCADO_PREFIX/runtimes/$RUNTIME_NAME/var-staging mkdir -p "$VAR_DIR/lib/avocado/extensions" +mkdir -p "$VAR_DIR/lib/avocado/runtime/$VERSION_ID" OUTPUT_DIR="$AVOCADO_PREFIX/runtimes/$RUNTIME_NAME" mkdir -p $OUTPUT_DIR {} +# Create symlinks in runtime/ pointing to enabled extensions +echo "Creating runtime symlinks for VERSION_ID: $VERSION_ID" +for ext in "$VAR_DIR/lib/avocado/extensions/"*.raw; do + if [ -f "$ext" ]; then + ext_filename=$(basename "$ext") + ln -sf "../../extensions/$ext_filename" "$VAR_DIR/lib/avocado/runtime/$VERSION_ID/$ext_filename" + echo "Created symlink: runtime/$VERSION_ID/$ext_filename -> extensions/$ext_filename" + fi +done + # Potential future SDK target hook. # echo "Run: avocado-pre-image-var-$TARGET_ARCH $RUNTIME_NAME" # avocado-pre-image-var-$TARGET_ARCH $RUNTIME_NAME -# Create btrfs image with extensions and confexts subvolumes +# Create btrfs image with extensions and runtime subvolumes mkfs.btrfs -r "$VAR_DIR" \ --subvol rw:lib/avocado/extensions \ + --subvol rw:lib/avocado/runtime \ -f "$OUTPUT_DIR/avocado-image-var-$TARGET_ARCH.btrfs" echo -e "\033[94m[INFO]\033[0m Running SDK lifecycle hook 'avocado-build' for '$TARGET_ARCH'." @@ -497,7 +530,7 @@ target = "x86_64" test-dep = { ext = "test-ext" } [ext.test-ext] -version = "1.0" +version = "1.0.0" types = ["sysext"] "#; let config_path = create_test_config_file(&temp_dir, config_content); @@ -513,10 +546,10 @@ types = ["sysext"] let script = cmd.create_build_script(&parsed, "x86_64").unwrap(); - assert!(script.contains("test-ext.raw")); + assert!(script.contains("test-ext-1.0.0.raw")); // Extension should be copied to avocado extensions directory but not symlinked to systemd directories - assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext.raw")); - assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext.raw")); + assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext-1.0.0.raw")); + assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext-1.0.0.raw")); } #[test] @@ -533,7 +566,7 @@ target = "x86_64" test-dep = { ext = "test-ext", types = ["sysext"] } [ext.test-ext] -version = "1.0" +version = "1.0.0" types = ["sysext", "confext"] "#; let config_path = create_test_config_file(&temp_dir, config_content); @@ -550,11 +583,11 @@ types = ["sysext", "confext"] let script = cmd.create_build_script(&parsed, "x86_64").unwrap(); // Extension should be copied to avocado extensions directory but not symlinked to systemd directories - assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext.raw")); - assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext.raw")); + assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext-1.0.0.raw")); + assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext-1.0.0.raw")); // Should NOT include symlinks to systemd directories (runtime will handle this) - assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext.raw $SYSEXT")); - assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext.raw $CONFEXT")); + assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext-1.0.0.raw $SYSEXT")); + assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext-1.0.0.raw $CONFEXT")); } #[test] @@ -571,7 +604,7 @@ target = "x86_64" test-dep = { ext = "test-ext" } [ext.test-ext] -version = "1.0" +version = "1.0.0" types = ["confext"] "#; let config_path = create_test_config_file(&temp_dir, config_content); @@ -588,10 +621,10 @@ types = ["confext"] let script = cmd.create_build_script(&parsed, "x86_64").unwrap(); // Extension should be copied to avocado extensions directory but not symlinked to systemd directories - assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext.raw")); - assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext.raw")); + assert!(script.contains("$AVOCADO_PREFIX/output/extensions/test-ext-1.0.0.raw")); + assert!(script.contains("$VAR_DIR/lib/avocado/extensions/test-ext-1.0.0.raw")); // Should NOT include symlinks to systemd directories (runtime will handle this) - assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext.raw $SYSEXT")); - assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext.raw $CONFEXT")); + assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext-1.0.0.raw $SYSEXT")); + assert!(!script.contains("ln -sf /var/lib/avocado/extensions/test-ext-1.0.0.raw $CONFEXT")); } } diff --git a/src/commands/runtime/deps.rs b/src/commands/runtime/deps.rs index c9e035f..d2c5342 100644 --- a/src/commands/runtime/deps.rs +++ b/src/commands/runtime/deps.rs @@ -170,7 +170,7 @@ gcc = { version = "11.0" } app-ext = { ext = "my-extension" } [ext.my-extension] -version = "2.0" +version = "2.0.0" types = ["sysext"] "# } @@ -198,7 +198,7 @@ types = ["sysext"] // Extensions should come first assert_eq!(deps[0].0, "ext"); assert_eq!(deps[0].1, "my-extension"); - assert_eq!(deps[0].2, "2.0"); + assert_eq!(deps[0].2, "2.0.0"); // Then packages assert_eq!(deps[1].0, "pkg"); diff --git a/src/commands/runtime/install.rs b/src/commands/runtime/install.rs index ed584e1..feaf49b 100644 --- a/src/commands/runtime/install.rs +++ b/src/commands/runtime/install.rs @@ -556,7 +556,7 @@ curl = { version = "7.0" } app-ext = { ext = "my-extension" } [ext.my-extension] -version = "2.0" +version = "2.0.0" types = ["sysext"] "#; let config_path = create_test_config_file(&temp_dir, config_content); diff --git a/src/utils/container.rs b/src/utils/container.rs index b770045..665cba1 100644 --- a/src/utils/container.rs +++ b/src/utils/container.rs @@ -263,6 +263,116 @@ impl SdkContainer { Ok(container_cmd) } + /// Run a command in the container and capture its output + pub async fn run_in_container_with_output(&self, config: RunConfig) -> Result> { + // Get or create docker volume for persistent state + let volume_manager = VolumeManager::new(self.container_tool.clone(), self.verbose); + let volume_state = volume_manager.get_or_create_volume(&self.cwd).await?; + + // Build environment variables + let mut env_vars = config.env_vars.unwrap_or_default(); + + // Set host platform environment variable + let host_platform = if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else { + "unknown" + }; + env_vars.insert( + "AVOCADO_HOST_PLATFORM".to_string(), + host_platform.to_string(), + ); + + if let Some(url) = &config.repo_url { + env_vars.insert("AVOCADO_SDK_REPO_URL".to_string(), url.clone()); + } + if let Some(release) = &config.repo_release { + env_vars.insert("AVOCADO_SDK_REPO_RELEASE".to_string(), release.clone()); + } + if let Some(dnf_args) = &config.dnf_args { + env_vars.insert("AVOCADO_DNF_ARGS".to_string(), dnf_args.join(" ")); + } + if config.verbose || self.verbose { + env_vars.insert("AVOCADO_VERBOSE".to_string(), "1".to_string()); + } + + // Build the complete command + let mut full_command = String::new(); + + // Conditionally include the entrypoint script + if config.use_entrypoint { + full_command.push_str(&self.create_entrypoint_script( + config.source_environment, + config.extension_sysroot.as_deref(), + config.runtime_sysroot.as_deref(), + &config.target, + config.no_bootstrap, + config.disable_weak_dependencies, + )); + full_command.push('\n'); + } + + full_command.push_str(&config.command); + + let bash_cmd = vec!["bash".to_string(), "-c".to_string(), full_command]; + + // Build container command with volume state + let container_cmd = self.build_container_command( + &config.container_image, + &bash_cmd, + &config.target, + &env_vars, + config.container_name.as_deref(), + false, // Never detach when capturing output + config.rm, + false, // Never interactive when capturing output + config.container_args.as_deref(), + &volume_state, + )?; + + if config.verbose || self.verbose { + print_info( + &format!( + "Mounting source directory: {} -> /opt/src", + self.cwd.display() + ), + OutputLevel::Normal, + ); + print_info( + &format!("Container command: {}", container_cmd.join(" ")), + OutputLevel::Normal, + ); + } + + // Execute command and capture output + let mut cmd = AsyncCommand::new(&container_cmd[0]); + cmd.args(&container_cmd[1..]); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + let output = cmd + .output() + .await + .with_context(|| "Failed to execute container command")?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(Some(stdout)) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + if config.verbose || self.verbose { + print_error( + &format!("Container execution failed: {stderr}"), + OutputLevel::Normal, + ); + } + Ok(None) + } + } + /// Execute the container command async fn execute_container_command( &self, From 20a549d0fb8ef245cd1726563eb3418e1eaf43d0 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Wed, 19 Nov 2025 16:35:53 -0500 Subject: [PATCH 2/2] update runtime enable from runtime to os-release --- src/commands/runtime/build.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/runtime/build.rs b/src/commands/runtime/build.rs index 2a9f1b4..b4d21df 100644 --- a/src/commands/runtime/build.rs +++ b/src/commands/runtime/build.rs @@ -251,20 +251,20 @@ fi VAR_DIR=$AVOCADO_PREFIX/runtimes/$RUNTIME_NAME/var-staging mkdir -p "$VAR_DIR/lib/avocado/extensions" -mkdir -p "$VAR_DIR/lib/avocado/runtime/$VERSION_ID" +mkdir -p "$VAR_DIR/lib/avocado/os-releases/$VERSION_ID" OUTPUT_DIR="$AVOCADO_PREFIX/runtimes/$RUNTIME_NAME" mkdir -p $OUTPUT_DIR {} -# Create symlinks in runtime/ pointing to enabled extensions -echo "Creating runtime symlinks for VERSION_ID: $VERSION_ID" +# Create symlinks in os-releases/ pointing to enabled extensions +echo "Creating OS release symlinks for VERSION_ID: $VERSION_ID" for ext in "$VAR_DIR/lib/avocado/extensions/"*.raw; do if [ -f "$ext" ]; then ext_filename=$(basename "$ext") - ln -sf "../../extensions/$ext_filename" "$VAR_DIR/lib/avocado/runtime/$VERSION_ID/$ext_filename" - echo "Created symlink: runtime/$VERSION_ID/$ext_filename -> extensions/$ext_filename" + ln -sf "../../extensions/$ext_filename" "$VAR_DIR/lib/avocado/os-releases/$VERSION_ID/$ext_filename" + echo "Created symlink: os-releases/$VERSION_ID/$ext_filename -> extensions/$ext_filename" fi done @@ -272,10 +272,10 @@ done # echo "Run: avocado-pre-image-var-$TARGET_ARCH $RUNTIME_NAME" # avocado-pre-image-var-$TARGET_ARCH $RUNTIME_NAME -# Create btrfs image with extensions and runtime subvolumes +# Create btrfs image with extensions and os-releases subvolumes mkfs.btrfs -r "$VAR_DIR" \ --subvol rw:lib/avocado/extensions \ - --subvol rw:lib/avocado/runtime \ + --subvol rw:lib/avocado/os-releases \ -f "$OUTPUT_DIR/avocado-image-var-$TARGET_ARCH.btrfs" echo -e "\033[94m[INFO]\033[0m Running SDK lifecycle hook 'avocado-build' for '$TARGET_ARCH'."