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.
378 lines
13 KiB
Rust
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());
|
|
}
|
|
}
|