diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0b5a3e..9dee5b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.16.0 hooks: - id: ruff-check args: [ --fix ] diff --git a/packages/debmagic-common/src/distro.rs b/packages/debmagic-common/src/distro.rs index a66279c..f4629fe 100644 --- a/packages/debmagic-common/src/distro.rs +++ b/packages/debmagic-common/src/distro.rs @@ -54,7 +54,7 @@ static DISTRO_INFO_MAP: LazyLock> = LazyLoc "sid", DistroVersion { distro: Distro::Debian, - codename: "sid".to_string(), + codename: "unstable".to_string(), version: "".to_string(), }, ), @@ -90,6 +90,14 @@ static DISTRO_INFO_MAP: LazyLock> = LazyLoc version: "13".to_string(), }, ), + ( + "stable", + DistroVersion { + distro: Distro::Debian, + codename: "trixie".to_string(), + version: "13".to_string(), + }, + ), ( "bookworm", DistroVersion { @@ -98,6 +106,14 @@ static DISTRO_INFO_MAP: LazyLock> = LazyLoc version: "12".to_string(), }, ), + ( + "oldstable", + DistroVersion { + distro: Distro::Debian, + codename: "bookworm".to_string(), + version: "12".to_string(), + }, + ), ( "bullseye", DistroVersion { @@ -131,6 +147,14 @@ static DISTRO_INFO_MAP: LazyLock> = LazyLoc version: "26.04".to_string(), }, ), + ( + "devel", + DistroVersion { + distro: Distro::Ubuntu, + codename: "resolute".to_string(), + version: "26.04".to_string(), + }, + ), ( "questing", DistroVersion { @@ -190,6 +214,14 @@ static DISTRO_INFO_MAP: LazyLock> = LazyLoc ]) }); -pub fn get_distro_version(codename: &str) -> Option { - DISTRO_INFO_MAP.get(codename).cloned() +/// Look up a distribution by codename or suite alias. +/// +/// Suite aliases are map keys that resolve to a concrete release [`DistroVersion`]: +/// - Debian: `stable` → current stable release, `oldstable` → current oldstable, +/// `sid` → `unstable` +/// - Ubuntu: `devel` → current development release +/// +/// Alias targets are maintained manually when Debian/Ubuntu roll. +pub fn get_distro_version(name: &str) -> Option { + DISTRO_INFO_MAP.get(name).cloned() } diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index 7bd28d9..285713e 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -9,7 +9,7 @@ use std::{ thread, }; -use crate::build::config::DriverOverrides; +use crate::build_intent::BuildIntent; use crate::{ build::{ common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata}, @@ -18,10 +18,9 @@ use crate::{ driver_docker::DriverDocker, }, config::Config, - package::PackageDescription, + package::{PackageIdentity, PackageTarget}, }; use anyhow::{Context, anyhow}; -use debmagic_common::distro::DistroVersion; use glob::glob; pub mod common; @@ -37,19 +36,15 @@ struct Build { fn get_build_driver( config: &BuildConfig, driver_config: &DriverConfig, - driver_overrides: &DriverOverrides, + docker_base_image: Option<&str>, ) -> anyhow::Result> { match config.driver { BuildDriverType::Docker => Ok(Box::new(DriverDocker::create( config, driver_config, - &driver_overrides.docker, + docker_base_image, )?)), - BuildDriverType::Bare => Ok(Box::new(DriverBare::create( - config, - driver_config, - &driver_overrides.bare, - ))), + BuildDriverType::Bare => Ok(Box::new(DriverBare::create(config, driver_config))), // BuildDriverType::Lxd => ... } } @@ -78,9 +73,9 @@ impl Build { pub fn create( config: &BuildConfig, driver_config: &DriverConfig, - driver_overrides: &DriverOverrides, + docker_base_image: Option<&str>, ) -> anyhow::Result { - let driver = get_build_driver(config, driver_config, driver_overrides) + let driver = get_build_driver(config, driver_config, docker_base_image) .context(format!("failed to create {:?} build driver", config.driver))?; Ok(Self { config: config.clone(), @@ -267,81 +262,28 @@ fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> anyhow::Result< } fn get_build_root_and_identifier( - config: &Config, - package: &PackageDescription, + temp_build_dir: &Path, + identity: &PackageIdentity, ) -> (String, PathBuf) { - let package_identifier = format!("{}-{}", package.name, package.version); - let build_root = config.temp_build_dir.join(&package_identifier); + let package_identifier = format!("{}-{}", identity.name, identity.version); + let build_root = temp_build_dir.join(&package_identifier); (package_identifier, build_root) } -/// Determine which distro version to use for the build. -/// -/// If only one distro version is specified in the changelog, it's used automatically. -/// If multiple distro versions are specified, an explicit --distro is required. -/// If --distro is provided, it's validated against the changelog versions. -fn resolve_distro_version( - changelog_distros: &[String], - explicit_distro: Option<&str>, -) -> anyhow::Result { - let resolved_codename = match (changelog_distros.len(), explicit_distro) { - (0, _) => Err(anyhow!("changelog contains no distributions")), - (1, None) => Ok(changelog_distros[0].clone()), - (1, Some(explicit)) => { - if explicit == changelog_distros[0] { - Ok(explicit.to_string()) - } else { - Err(anyhow!( - "explicit distro version '{}' conflicts with distribution specified in changelog '{}'", - explicit, - changelog_distros[0] - )) - } - } - (_, None) => Err(anyhow!( - "changelog contains multiple distributions ({}), please specify which one to build for with --distro", - changelog_distros.join(", ") - )), - (_, Some(explicit)) => { - if changelog_distros.contains(&explicit.to_string()) { - Ok(explicit.to_string()) - } else { - Err(anyhow!( - "explicit distro version '{}' not found in changelog distributions: {}", - explicit, - changelog_distros.join(", ") - )) - } - } - }?; - let resolved = debmagic_common::distro::get_distro_version(&resolved_codename) - .ok_or_else(|| anyhow!("unknown distro codename '{}'", resolved_codename))?; - Ok(resolved) -} - -fn prepare_build_env( - config: &Config, - driver_overrides: &DriverOverrides, - package: &PackageDescription, - driver_type: BuildDriverType, - output_dir: &Path, - explicit_distro_version: Option<&str>, -) -> anyhow::Result { - let (package_identifier, build_root) = get_build_root_and_identifier(config, package); +fn prepare_build_env(intent: &BuildIntent, target: &PackageTarget) -> anyhow::Result { + let (package_identifier, build_root) = + get_build_root_and_identifier(&intent.temp_build_dir, &target.identity); if build_root.exists() { fs::remove_dir_all(&build_root)?; } - let distro_version = resolve_distro_version(&package.distro_versions, explicit_distro_version) - .context("failed to determine distro version")?; - let build_config = BuildConfig { - driver: driver_type, + driver: intent.driver, package_identifier, - source_dir: package.source_dir.clone(), - output_dir: output_dir.to_path_buf(), + source_dir: target.identity.source_dir.clone(), + output_dir: intent.output_dir.clone(), build_root_dir: build_root, - distro: distro_version.clone(), + distro: target.distro.clone(), sign_package: false, }; @@ -352,12 +294,17 @@ fn prepare_build_env( copy_dir_all(&build_config.source_dir, build_config.build_source_dir()) .context("failed to copy source tree to build directory")?; - let build = Build::create(&build_config, &config.driver, driver_overrides)?; + let build = Build::create( + &build_config, + &intent.driver_config, + intent.docker_base_image.as_deref(), + )?; Ok(build) } -pub fn get_shell_in_build(config: &Config, package: &PackageDescription) -> anyhow::Result<()> { - let (_package_identifier, build_root) = get_build_root_and_identifier(config, package); +pub fn get_shell_in_build(config: &Config, identity: &PackageIdentity) -> anyhow::Result<()> { + let (_package_identifier, build_root) = + get_build_root_and_identifier(&config.temp_build_dir, identity); let build = Build::from_build_root(&build_root, &config.driver)?; let result = build .driver @@ -369,23 +316,8 @@ pub fn get_shell_in_build(config: &Config, package: &PackageDescription) -> anyh Ok(()) } -pub fn build_package( - config: &Config, - package: &PackageDescription, - driver_type: BuildDriverType, - driver_overrides: &DriverOverrides, - output_dir: &Path, - explicit_distro_version: Option<&str>, -) -> anyhow::Result<()> { - let build = prepare_build_env( - config, - driver_overrides, - package, - driver_type, - output_dir, - explicit_distro_version, - ) - .context("failed to prepare build environment")?; +pub fn build_package(intent: &BuildIntent, target: &PackageTarget) -> anyhow::Result<()> { + let build = prepare_build_env(intent, target).context("failed to prepare build environment")?; build .write_metadata() .context("failed to write build metadata")?; @@ -453,92 +385,3 @@ pub fn build_package( build.driver.cleanup(); Ok(()) } - -#[cfg(test)] -mod tests { - use debmagic_common::distro::Distro; - - use super::*; - - #[test] - fn test_resolve_distro_version_single_distro_no_explicit() { - let distros = vec!["forky".to_string()]; - let result = resolve_distro_version(&distros, None); - assert!(result.is_ok()); - let distro_version = result.unwrap(); - assert_eq!(distro_version.codename, "forky"); - assert_eq!(distro_version.distro, Distro::Debian); - } - - #[test] - fn test_resolve_distro_version_single_distro_matching_explicit() { - let distros = vec!["forky".to_string()]; - let result = resolve_distro_version(&distros, Some("forky")); - assert!(result.is_ok()); - let distro_version = result.unwrap(); - assert_eq!(distro_version.codename, "forky"); - assert_eq!(distro_version.distro, Distro::Debian); - } - - #[test] - fn test_resolve_distro_version_single_distro_conflicting_explicit() { - let distros = vec!["forky".to_string()]; - let result = resolve_distro_version(&distros, Some("duke")); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("conflicts with distribution specified in changelog") - ); - } - - #[test] - fn test_resolve_distro_version_multiple_distros_no_explicit() { - let distros = vec!["forky".to_string(), "duke".to_string()]; - let result = resolve_distro_version(&distros, None); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("multiple distributions") - ); - } - - #[test] - fn test_resolve_distro_version_multiple_distros_explicit_valid() { - let distros = vec!["forky".to_string(), "duke".to_string()]; - let result = resolve_distro_version(&distros, Some("duke")); - assert!(result.is_ok()); - let distro_version = result.unwrap(); - assert_eq!(distro_version.codename, "duke"); - assert_eq!(distro_version.distro, Distro::Debian); - } - - #[test] - fn test_resolve_distro_version_multiple_distros_explicit_invalid() { - let distros = vec!["forky".to_string(), "duke".to_string()]; - let result = resolve_distro_version(&distros, Some("trixie")); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("not found in changelog distributions") - ); - } - - #[test] - fn test_resolve_distro_version_empty_distros() { - let distros: Vec = vec![]; - let result = resolve_distro_version(&distros, None); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("changelog contains no distributions") - ); - } -} diff --git a/packages/debmagic/src/build/config.rs b/packages/debmagic/src/build/config.rs index 193d454..46392b0 100644 --- a/packages/debmagic/src/build/config.rs +++ b/packages/debmagic/src/build/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -use crate::build::driver_bare::{DriverBareConfig, DriverBareConfigOverrides}; -use crate::build::driver_docker::{DriverDockerConfig, DriverDockerConfigOverrides}; +use crate::build::driver_bare::DriverBareConfig; +use crate::build::driver_docker::DriverDockerConfig; #[derive(Deserialize, Debug, Clone, Default)] #[serde(default)] @@ -10,9 +10,3 @@ pub struct DriverConfig { pub docker: DriverDockerConfig, pub bare: DriverBareConfig, } - -#[derive(Deserialize, Debug, Clone, Default)] -pub struct DriverOverrides { - pub docker: DriverDockerConfigOverrides, - pub bare: DriverBareConfigOverrides, -} diff --git a/packages/debmagic/src/build/driver_bare.rs b/packages/debmagic/src/build/driver_bare.rs index 647df21..bfd31ec 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -13,20 +13,13 @@ use crate::build::{ #[serde(default)] pub struct DriverBareConfig {} -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DriverBareConfigOverrides {} - pub struct DriverBare { config: BuildConfig, _driver_config: DriverConfig, } impl DriverBare { - pub fn create( - config: &BuildConfig, - driver_config: &DriverConfig, - _overrides: &DriverBareConfigOverrides, - ) -> Self { + pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> Self { Self { config: config.clone(), _driver_config: driver_config.clone(), diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index 1210064..0b06a63 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -31,11 +31,6 @@ impl DriverDockerConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct DriverDockerConfigOverrides { - pub base_image: Option, -} - // Constants const BUILD_DIR_IN_CONTAINER: &str = "/debmagic"; const DOCKER_USER: &str = "user"; @@ -71,11 +66,10 @@ pub struct DriverDocker { fn build_build_image( config: &BuildConfig, driver_config: &DriverConfig, - overrides: &DriverDockerConfigOverrides, + docker_base_image: Option<&str>, ) -> anyhow::Result { - let base_image = overrides - .base_image - .clone() + let base_image = docker_base_image + .map(str::to_string) .unwrap_or_else(|| driver_config.docker.base_image_for_distro(&config.distro)); let debian_control_file_path = config.build_source_dir().join("debian").join("control"); @@ -128,7 +122,7 @@ fn build_build_image( return Err(anyhow!("Error creating docker image")); } - Ok(docker_image_name) + Ok(docker_image_name.to_string()) } fn does_container_exist(container_name: &str) -> anyhow::Result { @@ -159,7 +153,7 @@ impl DriverDocker { pub fn create( config: &BuildConfig, driver_config: &DriverConfig, - overrides: &DriverDockerConfigOverrides, + docker_base_image: Option<&str>, ) -> anyhow::Result { let container_name = format!("debmagic-{}", config.build_identifier()); let container_exists = does_container_exist(&container_name)?; @@ -185,7 +179,7 @@ impl DriverDocker { } } - let docker_image_name = build_build_image(config, driver_config, overrides)?; + let docker_image_name = build_build_image(config, driver_config, docker_base_image)?; let mut run_cmd = Command::new("docker"); run_cmd.args([ "run", @@ -212,7 +206,7 @@ impl DriverDocker { Ok(Self { config: config.clone(), driver_config: driver_config.clone(), - container_name, + container_name: container_name.to_string(), }) } diff --git a/packages/debmagic/src/build_intent.rs b/packages/debmagic/src/build_intent.rs new file mode 100644 index 0000000..cf28a46 --- /dev/null +++ b/packages/debmagic/src/build_intent.rs @@ -0,0 +1,178 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Context; + +use crate::{ + build::{common::BuildDriverType, config::DriverConfig}, + config::Config, +}; + +/// Clap-free inputs for resolving a [`BuildIntent`]. +#[derive(Debug, Clone)] +pub struct BuildIntentInput { + /// Directory used when `source_dir` / `output_dir` are unset (typically cwd). + pub fallback_dir: PathBuf, + pub source_dir: Option, + pub output_dir: Option, + pub config_file: Option, + pub driver: BuildDriverType, + pub persist_driver: Option, + pub incremental: Option, + pub docker_base_image: Option, +} + +/// Fully resolved description of *how* a package build should run. +#[derive(Debug, Clone)] +pub struct BuildIntent { + pub source_dir: PathBuf, + pub output_dir: PathBuf, + pub driver: BuildDriverType, + pub driver_config: DriverConfig, + pub incremental: bool, + pub temp_build_dir: PathBuf, + /// One-shot docker base image override; applied when the target distro is known. + pub docker_base_image: Option, +} + +/// Precedence of config files is: +/// +/// 1. explicit config file passed on the command line +/// 2. `/debian/debmagic.toml` +/// 3. `/debmagic/config.toml` +pub fn load_config( + source_dir: Option<&Path>, + config_file: Option<&Path>, +) -> anyhow::Result { + let mut config_file_paths = vec![]; + let xdg_config_file = dirs::config_dir().map(|p| p.join("debmagic").join("config.toml")); + if let Some(xdg_config_file) = xdg_config_file + && xdg_config_file.is_file() + { + config_file_paths.push(xdg_config_file); + } + + if let Some(source_dir) = source_dir { + config_file_paths.push(source_dir.join("debian").join("debmagic.toml")); + } + + if let Some(config_file) = config_file { + config_file_paths.push(config_file.to_path_buf()); + } + + Config::new(&config_file_paths) +} + +pub fn resolve_build_intent(input: BuildIntentInput) -> anyhow::Result { + let source_dir = std::path::absolute(input.source_dir.unwrap_or(input.fallback_dir.clone())) + .context("resolving source dir failed")?; + let output_dir = std::path::absolute(input.output_dir.unwrap_or(input.fallback_dir)) + .context("resolving output dir failed")?; + + let mut config = load_config(Some(&source_dir), input.config_file.as_deref())?; + + if let Some(persist_driver) = input.persist_driver { + config.driver.persistent = persist_driver; + } + + if let Some(incremental) = input.incremental { + config.incremental = incremental; + } + if config.incremental { + config.driver.persistent = true; + } + + Ok(BuildIntent { + source_dir, + output_dir, + driver: input.driver, + driver_config: config.driver, + incremental: config.incremental, + temp_build_dir: config.temp_build_dir, + docker_base_image: input.docker_base_image, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn asset_config() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets") + .join("config1.toml") + } + + fn base_input(fallback: PathBuf) -> BuildIntentInput { + BuildIntentInput { + fallback_dir: fallback, + source_dir: None, + output_dir: None, + config_file: Some(asset_config()), + driver: BuildDriverType::Docker, + persist_driver: None, + incremental: None, + docker_base_image: None, + } + } + + #[test] + fn load_config_reads_explicit_file() -> anyhow::Result<()> { + let cfg = load_config(None, Some(&asset_config()))?; + assert!(cfg.driver.persistent); + assert_eq!( + cfg.driver.docker.base_images.get("debian:trixie"), + Some(&"some-debian-trixie-image:latest".to_string()) + ); + Ok(()) + } + + #[test] + fn resolve_applies_incremental_implies_persistent() -> anyhow::Result<()> { + let dir = std::env::temp_dir(); + let mut input = base_input(dir); + input.persist_driver = Some(false); + input.incremental = Some(true); + + let intent = resolve_build_intent(input)?; + assert!(intent.incremental); + assert!(intent.driver_config.persistent); + Ok(()) + } + + #[test] + fn resolve_honours_persist_driver_without_incremental() -> anyhow::Result<()> { + let dir = std::env::temp_dir(); + let mut input = base_input(dir); + // config1.toml has persistent = true; CLI can turn it off + input.persist_driver = Some(false); + input.incremental = Some(false); + + let intent = resolve_build_intent(input)?; + assert!(!intent.incremental); + assert!(!intent.driver_config.persistent); + Ok(()) + } + + #[test] + fn resolve_keeps_docker_base_image_override() -> anyhow::Result<()> { + let dir = std::env::temp_dir(); + let mut input = base_input(dir); + input.docker_base_image = Some("custom:image".to_string()); + + let intent = resolve_build_intent(input)?; + assert_eq!(intent.docker_base_image.as_deref(), Some("custom:image")); + Ok(()) + } + + #[test] + fn resolve_absolutizes_paths() -> anyhow::Result<()> { + let dir = std::env::temp_dir(); + let intent = resolve_build_intent(base_input(dir.clone()))?; + assert!(intent.source_dir.is_absolute()); + assert!(intent.output_dir.is_absolute()); + assert_eq!(intent.source_dir, std::path::absolute(&dir)?); + assert_eq!(intent.output_dir, std::path::absolute(&dir)?); + Ok(()) + } +} diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index 9ab1084..ffa4663 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -33,7 +33,6 @@ impl Config { } } - // TODO: reimplement cli arg overwrites let build = builder .build() .context("Failed to initialize config reader")?; diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index 81f8781..fca869e 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -1,105 +1,49 @@ -use std::{ - env, - path::{self, PathBuf}, -}; +use std::env; use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ - build::{ - build_package, config::DriverOverrides, driver_bare::DriverBareConfigOverrides, - driver_docker::DriverDockerConfigOverrides, get_shell_in_build, - }, + build::{build_package, get_shell_in_build}, + build_intent::{BuildIntentInput, load_config, resolve_build_intent}, cli::{Cli, Commands}, - config::Config, - package::PackageDescription, + package::{load_package_identity, resolve_package_target}, }; pub mod build; +pub mod build_intent; pub mod cli; pub mod config; pub mod package; -/// Precedence of config files is: -/// -/// 1. explicit config file passed on the command line -/// 2. `/debian/debmagic.toml` -/// 3. `/debmagic/config.toml` -/// -fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result { - let mut config_file_paths = vec![]; - let xdg_config_file = dirs::config_dir().map(|p| p.join("debmagic").join("config.toml")); - if let Some(xdg_config_file) = xdg_config_file - && xdg_config_file.is_file() - { - config_file_paths.push(xdg_config_file); - } - - if let Some(source_dir) = &source_dir { - config_file_paths.push(source_dir.join("debian").join("debmagic.toml")); - } - - if let Some(config_file_override) = &cli.config { - config_file_paths.push(config_file_override.clone()); - } - - let config = Config::new(&config_file_paths)?; - Ok(config) -} - fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let current_dir = env::current_dir()?; match &cli.command { Commands::Build(args) => { - let source_dir = args.common.source_dir.as_deref().unwrap_or(¤t_dir); - let mut config = get_config(&cli, &Some(source_dir.to_path_buf()))?; - - // TODO: figure out a better way to override config from CLI args - maybe more generic, if that is even possible since - // we want a nice cli which somewhat matches the config structure - // but some config options only make sense in some cli subcommands -> these flags don't make sense in all commands - // and should only be used in some - if let Some(persist_driver) = args.persist_driver { - config.driver.persistent = persist_driver; - } - - if let Some(incremental) = args.incremental { - config.incremental = incremental; - } - if config.incremental { - // TODO: investigate if this is actually needed - config.driver.persistent = true; - } - let driver_overrides = DriverOverrides { - docker: DriverDockerConfigOverrides { - base_image: args.docker.base_image.clone(), - }, - bare: DriverBareConfigOverrides {}, - }; - - let package = PackageDescription::from_dir( - &path::absolute(source_dir).context("resolving source dir failed")?, - )?; - let output_dir = args.output_dir.as_deref().unwrap_or(¤t_dir); - build_package( - &config, - &package, - args.driver, - &driver_overrides, - &path::absolute(output_dir).context("resolving output dir failed")?, - args.distro.as_deref(), - ) - .context("Building the package failed")?; + let intent = resolve_build_intent(BuildIntentInput { + fallback_dir: current_dir, + source_dir: args.common.source_dir.clone(), + output_dir: args.output_dir.clone(), + config_file: cli.config.clone(), + driver: args.driver, + persist_driver: args.persist_driver, + incremental: args.incremental, + docker_base_image: args.docker.base_image.clone(), + })?; + + let target = resolve_package_target(&intent.source_dir, args.distro.as_deref()) + .context("failed to resolve package target")?; + build_package(&intent, &target).context("Building the package failed")?; } Commands::Shell(args) => { let source_dir = args.common.source_dir.as_deref().unwrap_or(¤t_dir); - let config = get_config(&cli, &Some(source_dir.to_path_buf()))?; - let package = PackageDescription::from_dir( - &path::absolute(source_dir).context("resolving source dir failed")?, - )?; - get_shell_in_build(&config, &package)?; + let source_dir = + std::path::absolute(source_dir).context("resolving source dir failed")?; + let config = load_config(Some(&source_dir), cli.config.as_deref())?; + let identity = load_package_identity(&source_dir)?; + get_shell_in_build(&config, &identity)?; } Commands::Test(_args) => { println!("Test subcommand! - not implemented"); diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs index 66e3d5f..72b1d20 100644 --- a/packages/debmagic/src/package.rs +++ b/packages/debmagic/src/package.rs @@ -1,84 +1,273 @@ use std::path::{Path, PathBuf}; use anyhow::anyhow; - use debmagic_common::debian::version::PackageVersion; +use debmagic_common::distro::DistroVersion; +/// Who/what is being built, without a chosen target distro. #[derive(Debug, Clone)] -pub struct PackageDescription { +pub struct PackageIdentity { pub name: String, pub version: PackageVersion, pub source_dir: PathBuf, - pub distro_versions: Vec, } -impl PackageDescription { - pub fn from_dir(dir: &Path) -> anyhow::Result { - let changelog_file = dir.join("debian").join("changelog"); - let changelog_contents = std::fs::read_to_string(changelog_file)?; - let changelog: debian_changelog::ChangeLog = changelog_contents.parse()?; - - let first_entry = changelog - .into_iter() - .next() - .ok_or(anyhow!("changelog is empty"))?; - - let name = first_entry - .package() - .ok_or(anyhow!("empty package name in changelog entry"))?; - let version = first_entry - .version() - .ok_or(anyhow!("no package version in changelog entry")) - .map(|v| PackageVersion::new(v.epoch, v.upstream_version, v.debian_revision))?; - - let distro_versions = first_entry - .distributions() - .ok_or(anyhow!("no distribution specified in changelog entry"))?; - - Ok(Self { +/// A [`PackageIdentity`] plus the chosen [`DistroVersion`] for a build run. +#[derive(Debug, Clone)] +pub struct PackageTarget { + pub identity: PackageIdentity, + pub distro: DistroVersion, +} + +struct ChangelogPackage { + identity: PackageIdentity, + /// Raw distribution names from the changelog entry (not looked up yet). + changelog_distros: Vec, +} + +fn read_changelog_package(dir: &Path) -> anyhow::Result { + let changelog_file = dir.join("debian").join("changelog"); + let changelog_contents = std::fs::read_to_string(changelog_file)?; + let changelog: debian_changelog::ChangeLog = changelog_contents.parse()?; + + let first_entry = changelog + .into_iter() + .next() + .ok_or(anyhow!("changelog is empty"))?; + + let name = first_entry + .package() + .ok_or(anyhow!("empty package name in changelog entry"))?; + let version = first_entry + .version() + .ok_or(anyhow!("no package version in changelog entry")) + .map(|v| PackageVersion::new(v.epoch, v.upstream_version, v.debian_revision))?; + + let changelog_distros = first_entry + .distributions() + .ok_or(anyhow!("no distribution specified in changelog entry"))?; + + Ok(ChangelogPackage { + identity: PackageIdentity { name, version, source_dir: dir.to_path_buf(), - distro_versions, - }) + }, + changelog_distros, + }) +} + +pub fn load_package_identity(dir: &Path) -> anyhow::Result { + Ok(read_changelog_package(dir)?.identity) +} + +/// Resolve package identity and target distro from a source tree. +/// +/// If only one distribution is listed in the changelog, it is used automatically. +/// If multiple are listed, an explicit `--distro` is required and must match one of them. +/// The chosen name must be known to distro knowledge (suite aliases are not resolved here). +pub fn resolve_package_target( + dir: &Path, + explicit_distro: Option<&str>, +) -> anyhow::Result { + let parsed = read_changelog_package(dir)?; + let distro = select_distro_version(&parsed.changelog_distros, explicit_distro)?; + Ok(PackageTarget { + identity: parsed.identity, + distro, + }) +} + +fn lookup_distro(name: &str) -> anyhow::Result { + debmagic_common::distro::get_distro_version(name) + .ok_or_else(|| anyhow!("unknown distro codename '{}'", name)) +} + +fn select_distro_version( + changelog_distros: &[String], + explicit_distro: Option<&str>, +) -> anyhow::Result { + match (changelog_distros.len(), explicit_distro) { + (0, _) => Err(anyhow!("changelog contains no distributions")), + (1, None) => lookup_distro(&changelog_distros[0]), + (1, Some(explicit)) => { + let from_changelog = lookup_distro(&changelog_distros[0])?; + let from_explicit = lookup_distro(explicit)?; + if from_changelog == from_explicit { + Ok(from_explicit) + } else { + Err(anyhow!( + "explicit distro version '{}' conflicts with distribution specified in changelog '{}'", + explicit, + changelog_distros[0] + )) + } + } + (_, None) => Err(anyhow!( + "changelog contains multiple distributions ({}), please specify which one to build for with --distro", + changelog_distros.join(", ") + )), + (_, Some(explicit)) => { + let from_explicit = lookup_distro(explicit)?; + let matched = changelog_distros.iter().any(|name| { + lookup_distro(name).is_ok_and(|from_changelog| from_changelog == from_explicit) + }); + if matched { + Ok(from_explicit) + } else { + Err(anyhow!( + "explicit distro version '{}' not found in changelog distributions: {}", + explicit, + changelog_distros.join(", ") + )) + } + } } } #[cfg(test)] mod tests { + use debmagic_common::distro::Distro; + use super::*; - #[test] - fn test_package_description_from_changelog() -> Result<(), anyhow::Error> { - let test_asset_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + fn test_package_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("assets") - .join("test_package"); + .join("test_package") + } - let package = PackageDescription::from_dir(&test_asset_dir)?; + fn test_package_multi_distro_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets") + .join("test_package_multi_distro") + } - assert_eq!(package.name, "test-package"); - assert_eq!(package.version.version(), "1.2.4-1"); - assert_eq!(package.distro_versions, vec!["stable"]); - assert_eq!(package.source_dir, test_asset_dir); + #[test] + fn load_package_identity_from_changelog() -> anyhow::Result<()> { + let dir = test_package_dir(); + let identity = load_package_identity(&dir)?; + assert_eq!(identity.name, "test-package"); + assert_eq!(identity.version.version(), "1.2.4-1"); + assert_eq!(identity.source_dir, dir); Ok(()) } #[test] - fn test_package_description_with_multiple_distros() -> Result<(), anyhow::Error> { - let test_asset_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("assets") - .join("test_package_multi_distro"); + fn resolve_package_target_stable_aliases_to_trixie() -> anyhow::Result<()> { + let target = resolve_package_target(&test_package_dir(), None)?; + assert_eq!(target.distro.codename, "trixie"); + assert_eq!(target.distro.distro, Distro::Debian); + Ok(()) + } - let package = PackageDescription::from_dir(&test_asset_dir)?; + #[test] + fn select_distro_version_alias_matches_canonical_explicit() -> anyhow::Result<()> { + let distro = select_distro_version(&["stable".to_string()], Some("trixie"))?; + assert_eq!(distro.codename, "trixie"); + Ok(()) + } - assert_eq!(package.name, "test-package"); - assert_eq!(package.version.version(), "1.2.4-1"); - assert_eq!(package.distro_versions, vec!["unstable", "testing"]); - assert_eq!(package.source_dir, test_asset_dir); + #[test] + fn select_distro_version_sid_matches_unstable_explicit() -> anyhow::Result<()> { + let distro = select_distro_version(&["sid".to_string()], Some("unstable"))?; + assert_eq!(distro.codename, "unstable"); + Ok(()) + } + + #[test] + fn resolve_package_target_multiple_distros_requires_explicit() { + let result = resolve_package_target(&test_package_multi_distro_dir(), None); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("multiple distributions") + ); + } + + #[test] + fn resolve_package_target_multiple_distros_with_explicit() -> anyhow::Result<()> { + let target = resolve_package_target(&test_package_multi_distro_dir(), Some("unstable"))?; + assert_eq!(target.identity.name, "test-package"); + assert_eq!(target.distro.codename, "unstable"); + assert_eq!(target.distro.distro, Distro::Debian); + Ok(()) + } + + #[test] + fn select_distro_version_single_no_explicit() -> anyhow::Result<()> { + let distro = select_distro_version(&["forky".to_string()], None)?; + assert_eq!(distro.codename, "forky"); + assert_eq!(distro.distro, Distro::Debian); + Ok(()) + } + #[test] + fn select_distro_version_single_matching_explicit() -> anyhow::Result<()> { + let distro = select_distro_version(&["forky".to_string()], Some("forky"))?; + assert_eq!(distro.codename, "forky"); Ok(()) } + + #[test] + fn select_distro_version_single_conflicting_explicit() { + let result = select_distro_version(&["forky".to_string()], Some("duke")); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("conflicts with distribution specified in changelog") + ); + } + + #[test] + fn select_distro_version_multiple_no_explicit() { + let result = select_distro_version(&["forky".to_string(), "duke".to_string()], None); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("multiple distributions") + ); + } + + #[test] + fn select_distro_version_multiple_explicit_valid() -> anyhow::Result<()> { + let distro = + select_distro_version(&["forky".to_string(), "duke".to_string()], Some("duke"))?; + assert_eq!(distro.codename, "duke"); + Ok(()) + } + + #[test] + fn select_distro_version_multiple_explicit_invalid() { + let result = + select_distro_version(&["forky".to_string(), "duke".to_string()], Some("trixie")); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("not found in changelog distributions") + ); + } + + #[test] + fn select_distro_version_empty_distros() { + let result = select_distro_version(&[], None); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("changelog contains no distributions") + ); + } }