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
46 changes: 42 additions & 4 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,21 +764,59 @@ 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#"
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
Expand All @@ -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"
"#
);

Expand Down
58 changes: 52 additions & 6 deletions src/commands/ext/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(" "),
Expand All @@ -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],
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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::<u32>().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)]
Expand Down Expand Up @@ -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\""));
Expand Down Expand Up @@ -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\""));
Expand Down Expand Up @@ -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\""));
}
Expand Down Expand Up @@ -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\""));
}
Expand Down
74 changes: 66 additions & 8 deletions src/commands/ext/image.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};

use crate::utils::config::{Config, ExtensionLocation};
use crate::utils::container::{RunConfig, SdkContainer};
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(),
Expand All @@ -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
));
}

Expand All @@ -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<Vec<String>>,
) -> Result<bool> {
// 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 {
Expand All @@ -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
Expand All @@ -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::<u32>().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(())
}
}
Loading
Loading