diff --git a/data/quirks.yml b/data/quirks.yml index 43ceb5f..a57d0e6 100644 --- a/data/quirks.yml +++ b/data/quirks.yml @@ -1,15 +1,43 @@ # Quirks configuration for package-specific workarounds # This file defines package-specific quirks that are applied during pull and deb operations +# +# Entries can be scoped with `series`: an empty list applies to every +# series, otherwise only the listed ones. Packaging workarounds should +# carry the series they were verified against so they can be dropped once +# the upstream packaging catches up. quirks: + # The resolute kernels declare `llvm-21-dev` unqualified while their + # other llvm pieces are `:native`; the dpkg cross rules then resolve it + # against the host architecture, whose dependency closure conflicts with + # the `:native` python3. Resolve it against the build architecture + # until the control is fixed upstream. + linux: + deb: + series: [resolute] + dependencies: + replace: + llvm-21-dev: llvm-21-dev:native + linux-riscv: + deb: + series: [resolute] + dependencies: + replace: + llvm-21-dev: llvm-21-dev:native + # Add more packages and their quirks as needed # example-package: # pull: # method: archive # deb: - # extra_dependencies: - # - another-dependency + # series: [noble] + # dependencies: + # replace: + # old-dep: new-dep (>= 2) [linux-any] + # inject: + # - missing-dep + # drop: + # - broken-dep # parameters: # key: value - diff --git a/src/deb/local.rs b/src/deb/local.rs index 53929a0..8390231 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -265,6 +265,7 @@ pub async fn build( package, version, arch, + series, package_dir_str, build_root, cross, @@ -564,6 +565,7 @@ fn install_build_dependencies( package: &str, version: &str, arch: &str, + series: &str, package_dir: &str, build_root: &str, cross: bool, @@ -610,10 +612,13 @@ fn install_build_dependencies( union: false, build_dep: true, }; - let clauses: Vec> = Deps::parse(&deps_value, &parse_opts)? + let mut clauses: Vec> = Deps::parse(&deps_value, &parse_opts)? .clauses() .map(<[PkgRelation]>::to_vec) .collect(); + // Package-specific workarounds (see data/quirks.yml), before anything + // derives candidate queries or install specs from the clauses. + crate::quirks::apply_dependency_quirks(package, series, &mut clauses, &parse_opts)?; let conflict_clauses: Vec> = if conflicts_value.trim().is_empty() { Vec::new() } else { diff --git a/src/quirks.rs b/src/quirks.rs index 9d0aa16..dd7b1e1 100644 --- a/src/quirks.rs +++ b/src/quirks.rs @@ -4,15 +4,46 @@ //! and apply them during pull and deb operations. use crate::data::embed_data; +use crate::debian::deps::{Deps, ParseOpts, PkgRelation}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +/// Build-dependency resolution rules for a package +/// +/// Applied after the declared Build-* fields are parsed and reduced, +/// before the resolver derives anything from them. Dependency strings +/// use the full dependency grammar: `name[:arch] [(op version)] +/// [arches] `. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct DependencyQuirks { + /// Declared dependency name -> dependency string to resolve in its + /// place. The replacement is parsed fresh and replaces the declared + /// dependency wholesale (qualifier, version, restrictions). + #[serde(default)] + pub replace: HashMap, + + /// Dependencies to resolve as if the control declared them. + #[serde(default)] + pub inject: Vec, + + /// Declared dependency names to ignore. + #[serde(default)] + pub drop: Vec, +} + /// Quirks configuration for a specific operation (pull or deb) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OperationQuirks { - /// Extra dependencies to install before the operation + /// Series the entry applies to. An empty list applies to every + /// series; packaging workarounds should carry the series they were + /// verified against, so they can be dropped once the upstream + /// packaging catches up. #[serde(default)] - pub extra_dependencies: Vec, + pub series: Vec, + + /// Build-dependency resolution rules. + #[serde(default)] + pub dependencies: Option, /// Additional parameters for the operation #[serde(default)] @@ -63,24 +94,29 @@ pub fn get_package_quirks<'a>( config.quirks.get(package) } -/// Get deb-time extra dependencies for a package -/// -/// This function returns the list of extra dependencies that should be installed -/// before building a package, as defined in the quirks configuration. +/// Whether a quirks entry applies to `series`: an empty series filter +/// matches every series, otherwise the series must be listed. +fn entry_applies_to_series(quirks: &OperationQuirks, series: &str) -> bool { + quirks.series.is_empty() || quirks.series.iter().any(|s| s == series) +} + +/// Get the build-dependency resolution rules of a package for a series /// /// # Arguments /// * `package` - The package name +/// * `series` - The distribution series (e.g. "resolute") /// /// # Returns -/// * `Vec` - List of extra dependencies, or empty vector if none -pub fn get_deb_extra_dependencies(package: &str) -> Vec { - if let Some(quirks) = get_package_quirks(&QUIRKS_DATA, package) - && let Some(deb_quirks) = &quirks.deb - { - return deb_quirks.extra_dependencies.clone(); +/// * `Option` - The rules, or None when the package has +/// no deb entry or the entry does not apply to the series +pub fn get_deb_dependency_quirks(package: &str, series: &str) -> Option { + let quirks = get_package_quirks(&QUIRKS_DATA, package)?; + let deb = quirks.deb.as_ref()?; + if entry_applies_to_series(deb, series) { + deb.dependencies.clone() + } else { + None } - - Vec::new() } /// Get package directories from quirks configuration @@ -111,15 +147,165 @@ pub fn get_package_directories(package: &str) -> Vec { Vec::new() } +/// Apply the dependency quirks of `package` in `series` to parsed +/// build-dependency clauses +/// +/// Rules apply in order — drop, replace, inject. `replace` matches by +/// declared name wherever the dependency appears; rule names that match +/// nothing are warned about, so stale quirks surface once the upstream +/// packaging is fixed. +pub fn apply_dependency_quirks( + package: &str, + series: &str, + clauses: &mut Vec>, + opts: &ParseOpts, +) -> Result<(), String> { + let Some(deps) = get_deb_dependency_quirks(package, series) else { + return Ok(()); + }; + apply_rules(clauses, &deps, opts) +} + +/// Apply one set of dependency rules to parsed clauses. +fn apply_rules( + clauses: &mut Vec>, + deps: &DependencyQuirks, + opts: &ParseOpts, +) -> Result<(), String> { + for name in &deps.drop { + let hits = clauses + .iter() + .flatten() + .filter(|rel| &rel.package == name) + .count(); + if hits == 0 { + log::warn!("dependency quirk: 'drop {name}' matched nothing"); + } + } + if !deps.drop.is_empty() { + for clause in clauses.iter_mut() { + clause.retain(|rel| !deps.drop.iter().any(|name| name == &rel.package)); + } + clauses.retain(|clause| !clause.is_empty()); + } + + for (declared, replacement) in &deps.replace { + let mut hits = 0; + for clause in clauses.iter_mut() { + for rel in clause.iter_mut() { + if rel.package == *declared { + *rel = crate::debian::deps::parse_simple(replacement, true) + .map_err(|e| format!("invalid replacement '{replacement}': {e}"))?; + hits += 1; + } + } + } + if hits == 0 { + log::warn!("dependency quirk: 'replace {declared}' matched nothing"); + } + } + + for injected in &deps.inject { + let parsed = Deps::parse(injected, opts)?; + clauses.extend(parsed.clauses().map(<[PkgRelation]>::to_vec)); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; + fn parse(s: &str) -> PkgRelation { + crate::debian::deps::parse_simple(s, true).unwrap() + } + + fn opts() -> ParseOpts { + ParseOpts { + host_arch: "riscv64".into(), + build_arch: "amd64".into(), + build_profiles: vec!["cross".into()], + reduce_restrictions: true, + union: false, + build_dep: true, + } + } + #[test] fn test_unknown_package_has_no_quirks() { - // A package absent from quirks.yml (currently every package) has no - // extra dependencies nor custom directories, and must not panic - assert!(get_deb_extra_dependencies("not-in-quirks").is_empty()); + // A package absent from quirks.yml has no dependency rules nor + // custom directories, and must not panic + assert!(get_deb_dependency_quirks("not-in-quirks", "resolute").is_none()); assert!(get_package_directories("not-in-quirks").is_empty()); } + + /// The linux dependency quirks are scoped to the series they were + /// verified against. + #[test] + fn linux_dependency_quirks_are_series_scoped() { + for package in ["linux", "linux-riscv"] { + let deps = + get_deb_dependency_quirks(package, "resolute").expect("the resolute entry applies"); + assert_eq!( + deps.replace.get("llvm-21-dev").map(String::as_str), + Some("llvm-21-dev:native ") + ); + assert!(get_deb_dependency_quirks(package, "noble").is_none()); + } + } + + /// `replace` rewrites exactly the dependencies whose declared name + /// matches, wholesale: the replacement carries its own qualifier and + /// restrictions. + #[test] + fn replace_rewrites_matching_names_only() { + let mut clauses = vec![vec![ + parse("llvm-21-dev "), + parse("clang-21:native"), + ]]; + let deps = DependencyQuirks { + replace: HashMap::from([( + "llvm-21-dev".to_string(), + "llvm-21-dev:native ".to_string(), + )]), + ..Default::default() + }; + apply_rules(&mut clauses, &deps, &opts()).unwrap(); + let rewritten = &clauses[0][0]; + assert_eq!(rewritten.arch_qualifier.as_deref(), Some("native")); + assert_eq!(rewritten.restrictions.len(), 1); + assert_eq!(clauses[0][1].arch_qualifier.as_deref(), Some("native")); + } + + /// `drop` removes named dependencies (empty clauses disappear) and + /// `inject` appends dependencies resolved like declared ones. + #[test] + fn drop_and_inject() { + let mut clauses = vec![vec![parse("broken-dep"), parse("keep-me")]]; + let deps = DependencyQuirks { + inject: vec!["injected-dep:any".to_string()], + drop: vec!["broken-dep".to_string()], + ..Default::default() + }; + apply_rules(&mut clauses, &deps, &opts()).unwrap(); + let names: Vec<&str> = clauses + .iter() + .flatten() + .map(|rel| rel.package.as_str()) + .collect(); + assert_eq!(names, ["keep-me", "injected-dep"]); + } + + /// A replacement that does not parse is a quirk configuration error, + /// not a silent no-op. + #[test] + fn invalid_replacement_is_an_error() { + let mut clauses = vec![vec![parse("llvm-21-dev")]]; + let deps = DependencyQuirks { + replace: HashMap::from([("llvm-21-dev".to_string(), "not@@valid".to_string())]), + ..Default::default() + }; + assert!(apply_rules(&mut clauses, &deps, &opts()).is_err()); + } }