Files
pkh/src/quirks.rs
T
vhaudiquet ce19c5648b quirks: inject the kernel's host-tool libraries as :native for cross
The resolute and stonking kernels declare libelf-dev, libdw-dev and
libssl-dev unqualified, unlike the Debian control which carries the
same names qualified :native for the host-side tools (resolve_btfids,
gendwarfksyms, sign-file).  The dpkg cross rules resolve an unqualified
Multi-Arch: same name against the host architecture only, so a cross
build installs no build-architecture variants and the kernel's
host-side tools cannot link (-ldw, -lelf, -lssl all fail).

Inject the :native variants for the affected series — the declared
unqualified dependencies stay, so the target-side tools keep their
host-architecture libraries.  Series-scoped so the entry can be
dropped once the control is fixed upstream.
2026-09-25 20:53:10 +02:00

378 lines
13 KiB
Rust

//! Quirks module for handling package-specific workarounds
//!
//! This module provides functionality to read quirks from a YAML file
//! 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] <restrictions>`.
#[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<String, String>,
/// Dependencies to resolve as if the control declared them.
#[serde(default)]
pub inject: Vec<String>,
/// Declared dependency names to ignore.
#[serde(default)]
pub drop: Vec<String>,
}
/// Quirks configuration for a specific operation (pull or deb)
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OperationQuirks {
/// 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 series: Vec<String>,
/// Build-dependency resolution rules.
#[serde(default)]
pub dependencies: Option<DependencyQuirks>,
/// Additional parameters for the operation
#[serde(default)]
pub parameters: HashMap<String, serde_yaml::Value>,
/// Custom package directories to try when looking for the package source
/// This is useful for packages that don't follow the standard naming conventions
/// like linux packages that use directories like "linux-main" or other custom names
#[serde(default)]
pub package_directory: Vec<String>,
}
/// Quirks for a specific package
///
/// `pull` and `deb` hold one entry per scope: an operation can carry
/// several entries with different `series` lists; every matching entry
/// applies, in file order.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PackageQuirks {
/// Quirks to apply during pull operation
#[serde(default)]
pub pull: Vec<OperationQuirks>,
/// Quirks to apply during deb operation
#[serde(default)]
pub deb: Vec<OperationQuirks>,
}
/// Top-level quirks configuration
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct QuirksConfig {
/// Map of package names to their quirks
pub quirks: HashMap<String, PackageQuirks>,
}
embed_data! {
static ref QUIRKS_DATA: QuirksConfig = "../data/quirks.yml"
}
/// Get quirks for a specific package
///
/// # Arguments
/// * `config` - The quirks configuration
/// * `package` - The package name
///
/// # Returns
/// * `Option<PackageQuirks>` - The quirks for the package, or None if not found
pub fn get_package_quirks<'a>(
config: &'a QuirksConfig,
package: &str,
) -> Option<&'a PackageQuirks> {
config.quirks.get(package)
}
/// 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
///
/// Every deb entry whose series list matches contributes its rules; the
/// returned rules apply in file order.
///
/// # Arguments
/// * `package` - The package name
/// * `series` - The distribution series (e.g. "resolute")
///
/// # Returns
/// * `Vec<DependencyQuirks>` - The matching rules, empty when the package
/// has no deb entry or none applies to the series
pub fn get_deb_dependency_quirks(package: &str, series: &str) -> Vec<DependencyQuirks> {
let Some(quirks) = get_package_quirks(&QUIRKS_DATA, package) else {
return Vec::new();
};
quirks
.deb
.iter()
.filter(|deb| entry_applies_to_series(deb, series))
.filter_map(|deb| deb.dependencies.clone())
.collect()
}
/// Get package directories from quirks configuration
///
/// This function returns the list of custom package directories to try
/// when looking for the package source directory: every matching deb
/// entry contributes its directories, falling back to the pull entries
/// when no deb entry carries any.
///
/// # Arguments
/// * `package` - The package name
/// * `series` - The distribution series (e.g. "resolute")
///
/// # Returns
/// * `Vec<String>` - List of package directories to try, or empty vector if none
pub fn get_package_directories(package: &str, series: &str) -> Vec<String> {
let Some(quirks) = get_package_quirks(&QUIRKS_DATA, package) else {
return Vec::new();
};
let mut directories = Vec::new();
for deb in quirks
.deb
.iter()
.filter(|q| entry_applies_to_series(q, series))
{
directories.extend(deb.package_directory.iter().cloned());
}
if directories.is_empty() {
for pull in quirks
.pull
.iter()
.filter(|q| entry_applies_to_series(q, series))
{
directories.extend(pull.package_directory.iter().cloned());
}
}
directories
}
/// 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<Vec<PkgRelation>>,
opts: &ParseOpts,
) -> Result<(), String> {
for deps in get_deb_dependency_quirks(package, series) {
apply_rules(clauses, &deps, opts)?;
}
Ok(())
}
/// Apply one set of dependency rules to parsed clauses.
fn apply_rules(
clauses: &mut Vec<Vec<PkgRelation>>,
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 has no dependency rules nor
// custom directories, and must not panic
assert!(get_deb_dependency_quirks("not-in-quirks", "resolute").is_empty());
assert!(get_package_directories("not-in-quirks", "resolute").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 rules = get_deb_dependency_quirks(package, "resolute");
assert_eq!(rules.len(), 2, "the resolute entries apply");
assert_eq!(
rules[0].replace.get("llvm-21-dev").map(String::as_str),
Some("llvm-21-dev:native <!stage1>")
);
// Only the stonking entry exists there: the llvm-21-dev
// replacement above is a resolute-only packaging state.
let rules = get_deb_dependency_quirks(package, "stonking");
assert_eq!(rules.len(), 1);
assert!(rules[0].replace.is_empty());
assert!(get_deb_dependency_quirks(package, "noble").is_empty());
}
}
/// The kernels of the series whose control dropped the Debian-style
/// `:native` qualifiers on the host-tool libraries inject the
/// build-architecture variants: the dpkg cross rules resolve the
/// unqualified Multi-Arch: same names against the host architecture
/// only, leaving nothing for the kernel's host-side tools to link.
#[test]
fn linux_injects_native_host_tool_libraries_for_cross() {
for series in ["resolute", "stonking"] {
let mut clauses = vec![vec![parse("libelf-dev <!stage1>")]];
crate::quirks::apply_dependency_quirks("linux", series, &mut clauses, &opts()).unwrap();
let injected: Vec<&PkgRelation> = clauses[1..]
.iter()
.flatten()
.filter(|rel| {
["libelf-dev", "libdw-dev", "libssl-dev"].contains(&rel.package.as_str())
})
.collect();
assert_eq!(
injected.len(),
3,
"one :native clause per host-tool library"
);
assert!(
injected
.iter()
.all(|rel| rel.arch_qualifier.as_deref() == Some("native"))
);
// The declared dependency itself is untouched: the host
// (target) variant still installs for the checker.
assert_eq!(clauses[0][0].arch_qualifier, None);
}
}
/// Other kernel series keep their declared dependencies untouched.
#[test]
fn noble_linux_dependencies_are_not_rewritten() {
let mut clauses = vec![vec![parse("libelf-dev")]];
crate::quirks::apply_dependency_quirks("linux", "noble", &mut clauses, &opts()).unwrap();
assert_eq!(clauses.len(), 1);
assert_eq!(clauses[0][0].arch_qualifier, 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 <!stage1>"),
parse("clang-21:native"),
]];
let deps = DependencyQuirks {
replace: HashMap::from([(
"llvm-21-dev".to_string(),
"llvm-21-dev:native <!stage1>".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());
}
}