Compare commits
6
Commits
5c026a7050
...
ff1f8c7ccd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1f8c7ccd | ||
|
|
31fe6dc524 | ||
|
|
5592d5a6e4 | ||
|
|
bb719e7c80 | ||
|
|
af9845c480 | ||
|
|
02cb5306f3 |
+31
-3
@@ -1,15 +1,43 @@
|
|||||||
# Quirks configuration for package-specific workarounds
|
# Quirks configuration for package-specific workarounds
|
||||||
# This file defines package-specific quirks that are applied during pull and deb operations
|
# 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:
|
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 <!stage1>
|
||||||
|
linux-riscv:
|
||||||
|
deb:
|
||||||
|
series: [resolute]
|
||||||
|
dependencies:
|
||||||
|
replace:
|
||||||
|
llvm-21-dev: llvm-21-dev:native <!stage1>
|
||||||
|
|
||||||
# Add more packages and their quirks as needed
|
# Add more packages and their quirks as needed
|
||||||
# example-package:
|
# example-package:
|
||||||
# pull:
|
# pull:
|
||||||
# method: archive
|
# method: archive
|
||||||
# deb:
|
# deb:
|
||||||
# extra_dependencies:
|
# series: [noble]
|
||||||
# - another-dependency
|
# dependencies:
|
||||||
|
# replace:
|
||||||
|
# old-dep: new-dep (>= 2) [linux-any]
|
||||||
|
# inject:
|
||||||
|
# - missing-dep
|
||||||
|
# drop:
|
||||||
|
# - broken-dep
|
||||||
# parameters:
|
# parameters:
|
||||||
# key: value
|
# key: value
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -89,24 +89,27 @@ pub async fn download_cache_keyrings(
|
|||||||
keyring_dir.display()
|
keyring_dir.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
// Upgrade cache directories created by versions that made them
|
||||||
|
// private: mmdebstrap's unshare-mode hooks cannot read them.
|
||||||
} else {
|
} else {
|
||||||
// Remote contexts (e.g. ssh) have no stat/metadata access through
|
// Remote contexts (e.g. ssh) have no stat/metadata access through
|
||||||
// the context API, so the ownership guard cannot be performed;
|
// the context API, so the ownership guard cannot be performed;
|
||||||
// keep the previous best-effort behavior of tightening the
|
// keep the previous best-effort behavior of tightening the
|
||||||
// directory permissions instead (0700 instead of the former
|
// directory permissions instead (no group/others write).
|
||||||
// world-writable a+rwx).
|
|
||||||
ctx.command("chmod").arg("700").arg(&keyring_dir).status()?;
|
|
||||||
}
|
}
|
||||||
|
ctx.command("chmod").arg("755").arg(&keyring_dir).status()?;
|
||||||
} else {
|
} else {
|
||||||
// Create the directory private to the invoking user (0700). This is
|
// Create the directory readable but not writable by group/others.
|
||||||
// sufficient for mmdebstrap in unshare mode: it runs with the same
|
// mmdebstrap's unshare-mode hooks run under an identity that cannot
|
||||||
// real uid (the user namespace only maps that uid to root, file
|
// read the invoking user's private directories, so 0700 breaks the
|
||||||
// access still happens as the real uid), so no world-accessible
|
// keyring copy into the chroot; the planting guard stays on the
|
||||||
// permissions are needed.
|
// ownership and no-write checks of validate_keyring_dir (the
|
||||||
|
// skip-if-exists logic below trusts pre-existing keyrings, so the
|
||||||
|
// directory must never be writable by anyone else).
|
||||||
ctx.command("mkdir")
|
ctx.command("mkdir")
|
||||||
.arg("-p")
|
.arg("-p")
|
||||||
.arg("-m")
|
.arg("-m")
|
||||||
.arg("700")
|
.arg("755")
|
||||||
.arg(&keyring_dir)
|
.arg(&keyring_dir)
|
||||||
.status()?;
|
.status()?;
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,11 @@ pub async fn download_cache_keyrings(
|
|||||||
binary_path.display()
|
binary_path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Readable like the directory: mmdebstrap's hooks copy these into
|
||||||
|
// the chroot. Applies to legacy files too, which a restrictive
|
||||||
|
// umask may have left private, and a permissive one group-writable.
|
||||||
|
let _ = ctx.command("chmod").arg("644").arg(&binary_path).status();
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -348,6 +356,10 @@ mod tests {
|
|||||||
assert!(validate_keyring_dir(1000, 0o750, 1000).is_ok());
|
assert!(validate_keyring_dir(1000, 0o750, 1000).is_ok());
|
||||||
assert!(validate_keyring_dir(1000, 0o1744, 1000).is_ok());
|
assert!(validate_keyring_dir(1000, 0o1744, 1000).is_ok());
|
||||||
assert!(validate_keyring_dir(0, 0o700, 0).is_ok());
|
assert!(validate_keyring_dir(0, 0o700, 0).is_ok());
|
||||||
|
// The world-readable modes the cache now uses: readable so that
|
||||||
|
// mmdebstrap's unshare-mode hooks can copy the keyrings, while the
|
||||||
|
// ownership and no-write checks keep the planting guard.
|
||||||
|
assert!(validate_keyring_dir(1000, 0o755, 1000).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+38
-1
@@ -243,12 +243,22 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Make a command inside context
|
/// Make a command inside context
|
||||||
|
///
|
||||||
|
/// Build tooling must not inherit the session's locale: dpkg-family
|
||||||
|
/// tools and perl-based packaging scripts change their output (and
|
||||||
|
/// dpkg-buildpackage treats some of it as data) with the environment,
|
||||||
|
/// and a translated or mixed locale leaks host state into builds. The
|
||||||
|
/// C locale is the default; a caller can still override it by setting
|
||||||
|
/// LANG/LC_ALL through [`ContextCommand::envs`] afterwards.
|
||||||
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand<'_> {
|
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand<'_> {
|
||||||
ContextCommand {
|
ContextCommand {
|
||||||
context: self,
|
context: self,
|
||||||
program: program.as_ref().to_string_lossy().to_string(),
|
program: program.as_ref().to_string_lossy().to_string(),
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
env: Vec::new(),
|
env: vec![
|
||||||
|
("LANG".to_string(), "C".to_string()),
|
||||||
|
("LC_ALL".to_string(), "C".to_string()),
|
||||||
|
],
|
||||||
cwd: None,
|
cwd: None,
|
||||||
sink: None,
|
sink: None,
|
||||||
}
|
}
|
||||||
@@ -530,4 +540,31 @@ mod endpoint_tests {
|
|||||||
let err = ContextConfig::from_endpoint("host:99999").unwrap_err();
|
let err = ContextConfig::from_endpoint("host:99999").unwrap_err();
|
||||||
assert_eq!(err, "Invalid port number");
|
assert_eq!(err, "Invalid port number");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Commands run in the C locale whatever the session environment
|
||||||
|
/// carries: host locale variables must not leak into builds. An
|
||||||
|
/// explicit caller override still wins.
|
||||||
|
#[test]
|
||||||
|
fn commands_default_to_the_c_locale() {
|
||||||
|
let ctx = Context::new(ContextConfig::Local).unwrap();
|
||||||
|
|
||||||
|
let locale = ctx
|
||||||
|
.command("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("printf '%s' \"${LC_ALL:-unset}:${LANG:-unset}\"")
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
.stdout;
|
||||||
|
assert_eq!(String::from_utf8_lossy(&locale), "C:C");
|
||||||
|
|
||||||
|
let locale = ctx
|
||||||
|
.command("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("printf '%s' \"$LC_ALL\"")
|
||||||
|
.env("LC_ALL", "C.UTF-8")
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
.stdout;
|
||||||
|
assert_eq!(String::from_utf8_lossy(&locale), "C.UTF-8");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+573
-54
@@ -5,13 +5,15 @@ use crate::deb::{Phase, enter_phase, find_dsc_file};
|
|||||||
use crate::logfmt::QuiltClassifier;
|
use crate::logfmt::QuiltClassifier;
|
||||||
use crate::report::BuildView;
|
use crate::report::BuildView;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::apt;
|
use crate::apt;
|
||||||
use crate::deb::cross;
|
use crate::deb::cross;
|
||||||
|
use crate::debian::control::ControlInfo;
|
||||||
|
use crate::debian::deps::{Deps, Facts, ParseOpts, PkgRelation};
|
||||||
|
|
||||||
/// Attach the capture sink to a command when the live UI is active
|
/// Attach the capture sink to a command when the live UI is active
|
||||||
fn cap<'a>(
|
fn cap<'a>(
|
||||||
@@ -255,59 +257,23 @@ pub async fn build(
|
|||||||
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
|
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install arch-specific build dependencies
|
// Resolve and install the Build-* dependencies with dpkg's cross
|
||||||
log::debug!("Installing arch-specific build dependencies...");
|
// semantics; this replaces the historical `apt-get build-dep` passes,
|
||||||
enter_phase(view, Phase::InstallingBuildDeps);
|
// whose `--host-architecture` resolution cannot express the
|
||||||
let mut cmd = ctx.command("apt-get");
|
// Multi-Arch-aware variant choice dpkg's checker requires.
|
||||||
cmd.current_dir(package_dir_str)
|
install_build_dependencies(
|
||||||
.envs(env.clone())
|
package,
|
||||||
.arg("-y")
|
version,
|
||||||
.arg("build-dep");
|
arch,
|
||||||
if cross {
|
series,
|
||||||
cmd.arg(format!("--host-architecture={arch}"));
|
package_dir_str,
|
||||||
}
|
build_root,
|
||||||
cmd.arg("--arch-only");
|
cross,
|
||||||
let status = cap(&mut cmd, &sink).arg("./").status()?;
|
&env,
|
||||||
|
ctx.clone(),
|
||||||
// If build-dep fails, we try to explain the failure using dose-debcheck
|
view,
|
||||||
if !status.success() {
|
&sink,
|
||||||
view.suspend();
|
)?;
|
||||||
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
|
|
||||||
return Err("Could not install build-dependencies for the build".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install arch-independant build dependencies, only if the source declares
|
|
||||||
// any: without --arch-only this pass resolves the whole Build-Depends field
|
|
||||||
// too, which is redundant after the first pass and breaks cross builds.
|
|
||||||
let has_indep_deps = match ctx.read_file(&package_dir.join("debian/control")) {
|
|
||||||
Ok(control) => control
|
|
||||||
.lines()
|
|
||||||
.any(|l| l.to_ascii_lowercase().starts_with("build-depends-indep:")),
|
|
||||||
Err(e) => {
|
|
||||||
log::debug!("cannot read debian/control for indep build-deps: {}", e);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if has_indep_deps {
|
|
||||||
log::debug!("Installing arch-independant build dependencies...");
|
|
||||||
let mut cmd = ctx.command("apt-get");
|
|
||||||
cmd.current_dir(package_dir_str)
|
|
||||||
.envs(env.clone())
|
|
||||||
.arg("-y")
|
|
||||||
.arg("build-dep");
|
|
||||||
if cross {
|
|
||||||
cmd.arg(format!("--host-architecture={arch}"));
|
|
||||||
}
|
|
||||||
cmd.arg("./");
|
|
||||||
let status = cap(&mut cmd, &sink).status()?;
|
|
||||||
|
|
||||||
// If build-dep fails, we try to explain the failure using dose-debcheck
|
|
||||||
if !status.success() {
|
|
||||||
view.suspend();
|
|
||||||
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
|
|
||||||
return Err("Could not install build-dependencies for the build".into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the build step
|
// Run the build step
|
||||||
log::debug!("Building (debian/rules build) package...");
|
log::debug!("Building (debian/rules build) package...");
|
||||||
@@ -371,6 +337,407 @@ pub async fn build(
|
|||||||
Ok(artifacts)
|
Ok(artifacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One candidate binary package for a dependency name, as reported by
|
||||||
|
/// `apt-cache show`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Candidate {
|
||||||
|
/// Debian architecture of the candidate (`amd64`, `arm64`, `all`, ...).
|
||||||
|
arch: String,
|
||||||
|
/// Multi-Arch attribute; an absent field means `no`.
|
||||||
|
multiarch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure core of the install strategy: the apt install specification that
|
||||||
|
/// satisfies `rel` among the `candidates` available for its package name.
|
||||||
|
///
|
||||||
|
/// The mapping reproduces the dpkg cross semantics that
|
||||||
|
/// [`crate::debian::deps`] evaluates against (`Dpkg::Deps::KnownFacts
|
||||||
|
/// _find_package`), preferring the runnable build-architecture variant
|
||||||
|
/// whenever dpkg's rules accept several:
|
||||||
|
///
|
||||||
|
/// - unqualified: a `Multi-Arch: foreign` build-architecture candidate
|
||||||
|
/// (tools: the variant that runs on the build machine), then the
|
||||||
|
/// host-architecture candidate (the variant dpkg checks unqualified
|
||||||
|
/// dependencies against), then an `Architecture: all` candidate. A
|
||||||
|
/// foreign-arch candidate of a non-foreign package satisfies nothing in
|
||||||
|
/// cross mode — installing it would diverge from the checker;
|
||||||
|
/// - `:native`: the build-architecture candidate (`Architecture: all`
|
||||||
|
/// accepted); a `Multi-Arch: foreign` candidate aborts the lookup,
|
||||||
|
/// like dpkg's checker;
|
||||||
|
/// - `:any`: a `Multi-Arch: allowed` candidate, host architecture first;
|
||||||
|
/// - explicit `:arch`: exactly that candidate;
|
||||||
|
/// - a name with no candidate at all is passed through unqualified, so
|
||||||
|
/// apt resolves virtual packages (`debhelper-compat`, dbus session
|
||||||
|
/// alternatives, ...) to a satisfying provider.
|
||||||
|
///
|
||||||
|
/// `None` when no candidate can satisfy the relation per dpkg semantics:
|
||||||
|
/// the caller falls through to the next alternative of the clause.
|
||||||
|
fn install_spec_for(
|
||||||
|
rel: &PkgRelation,
|
||||||
|
candidates: &[Candidate],
|
||||||
|
build_arch: &str,
|
||||||
|
host_arch: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
let qualified = |arch: &str| format!("{}:{}", rel.package, arch);
|
||||||
|
match rel.arch_qualifier.as_deref() {
|
||||||
|
Some("native") => {
|
||||||
|
if candidates
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.arch == build_arch && c.multiarch == "foreign")
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == build_arch)
|
||||||
|
.map(|_| qualified(build_arch))
|
||||||
|
.or_else(|| {
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == "all")
|
||||||
|
.map(|_| rel.package.clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some("any") => candidates
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.multiarch == "allowed")
|
||||||
|
.find(|c| c.arch == host_arch)
|
||||||
|
.or_else(|| candidates.iter().find(|c| c.multiarch == "allowed"))
|
||||||
|
.map(|c| qualified(&c.arch)),
|
||||||
|
Some(qual) => candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == qual)
|
||||||
|
.map(|_| qualified(qual)),
|
||||||
|
None => {
|
||||||
|
if candidates.is_empty() {
|
||||||
|
return Some(rel.package.clone());
|
||||||
|
}
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == build_arch && c.multiarch == "foreign")
|
||||||
|
.map(|_| qualified(build_arch))
|
||||||
|
.or_else(|| {
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == host_arch)
|
||||||
|
.map(|_| qualified(host_arch))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.arch == "all")
|
||||||
|
.map(|_| rel.package.clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure core of the resolver loop: the next batch of install
|
||||||
|
/// specifications, one alternative per still-unsatisfied clause,
|
||||||
|
/// advancing each clause's cursor past the alternatives that no candidate
|
||||||
|
/// can satisfy per dpkg semantics.
|
||||||
|
fn next_install_pass(
|
||||||
|
clauses: &[Vec<PkgRelation>],
|
||||||
|
unsatisfied: &[usize],
|
||||||
|
cursors: &mut [usize],
|
||||||
|
candidates: &BTreeMap<String, Vec<Candidate>>,
|
||||||
|
build_arch: &str,
|
||||||
|
host_arch: &str,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let mut specs = Vec::new();
|
||||||
|
for &i in unsatisfied {
|
||||||
|
let empty = Vec::new();
|
||||||
|
while cursors[i] < clauses[i].len() {
|
||||||
|
let rel = &clauses[i][cursors[i]];
|
||||||
|
let cands = candidates.get(&rel.package).unwrap_or(&empty);
|
||||||
|
let spec = install_spec_for(rel, cands, build_arch, host_arch);
|
||||||
|
cursors[i] += 1;
|
||||||
|
if let Some(spec) = spec {
|
||||||
|
specs.push(spec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
specs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the candidates available in the build context for `names`: one
|
||||||
|
/// entry per (package, architecture). The plain name surfaces the
|
||||||
|
/// build-architecture and `all` candidates; the host-qualified name is
|
||||||
|
/// what surfaces foreign-arch candidates. Names missing from the indexes
|
||||||
|
/// and virtual names simply produce no stanza.
|
||||||
|
fn query_candidates(
|
||||||
|
ctx: &Arc<Context>,
|
||||||
|
names: &[String],
|
||||||
|
build_arch: &str,
|
||||||
|
host_arch: &str,
|
||||||
|
) -> BTreeMap<String, Vec<Candidate>> {
|
||||||
|
let mut query: Vec<String> = Vec::new();
|
||||||
|
for name in names {
|
||||||
|
query.push(name.clone());
|
||||||
|
if host_arch != build_arch {
|
||||||
|
query.push(format!("{name}:{host_arch}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Ok(output) = ctx.command("apt-cache").arg("show").args(&query).output() else {
|
||||||
|
return BTreeMap::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut map: BTreeMap<String, Vec<Candidate>> = BTreeMap::new();
|
||||||
|
for para in crate::debian::control::parse_paragraphs(&String::from_utf8_lossy(&output.stdout)) {
|
||||||
|
let Some(name) = para.get("Package") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(arch) = para.get("Architecture") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let entry = map.entry(name.to_string()).or_default();
|
||||||
|
if entry.iter().any(|c| c.arch == arch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.push(Candidate {
|
||||||
|
arch: arch.to_string(),
|
||||||
|
multiarch: para.get("Multi-Arch").unwrap_or("no").to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build architecture inside the build context.
|
||||||
|
fn context_build_arch(ctx: &Arc<Context>) -> String {
|
||||||
|
ctx.command("dpkg")
|
||||||
|
.arg("--print-architecture")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(crate::get_current_arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Active build profiles for the dependency evaluation: what the build
|
||||||
|
/// steps run with (`DEB_BUILD_PROFILES`, set to `cross` for cross
|
||||||
|
/// builds), else the vendor defaults.
|
||||||
|
fn build_profiles_for(env: &HashMap<String, String>, ctx: &Arc<Context>) -> Vec<String> {
|
||||||
|
match env.get("DEB_BUILD_PROFILES") {
|
||||||
|
Some(value) => value
|
||||||
|
.split(',')
|
||||||
|
.map(|p| p.trim().to_string())
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.collect(),
|
||||||
|
None => {
|
||||||
|
let vendor = ctx
|
||||||
|
.read_file(Path::new("/etc/dpkg/origins/default"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|content| crate::build::env::vendor_from_origins_content(&content))
|
||||||
|
.unwrap_or_else(crate::build::env::current_vendor);
|
||||||
|
crate::build::env::resolve_build_profiles(&[], &vendor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installed-package facts inside the build context. An unreadable status
|
||||||
|
/// database (fresh chroot) yields empty facts: everything declared then
|
||||||
|
/// installs.
|
||||||
|
fn load_context_facts(ctx: &Arc<Context>, build_arch: &str, host_arch: &str) -> Facts {
|
||||||
|
match ctx.read_file(Path::new("/var/lib/dpkg/status")) {
|
||||||
|
Ok(content) => Facts::from_status(&content, host_arch, build_arch),
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("cannot read the context dpkg status: {e}");
|
||||||
|
Facts::new(host_arch, build_arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve and install the Build-* dependencies with dpkg's cross
|
||||||
|
/// semantics: the clauses of `debian/control` are evaluated natively (the
|
||||||
|
/// `crate::debian::deps` `dpkg-checkbuilddeps` equivalent) against the
|
||||||
|
/// context's installed-package state, and the unsatisfied ones install
|
||||||
|
/// through explicitly architecture-qualified names.
|
||||||
|
///
|
||||||
|
/// Build conflicts abort before anything installs. The loop installs one
|
||||||
|
/// alternative per still-unsatisfied clause per apt transaction and
|
||||||
|
/// re-evaluates after each; a clause whose alternatives no candidate can
|
||||||
|
/// satisfy per dpkg semantics aborts the build. On failure the dose3
|
||||||
|
/// diagnosis runs, like the historical `build-dep` passes.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn install_build_dependencies(
|
||||||
|
package: &str,
|
||||||
|
version: &str,
|
||||||
|
arch: &str,
|
||||||
|
series: &str,
|
||||||
|
package_dir: &str,
|
||||||
|
build_root: &str,
|
||||||
|
cross: bool,
|
||||||
|
env: &HashMap<String, String>,
|
||||||
|
ctx: Arc<Context>,
|
||||||
|
view: &dyn BuildView,
|
||||||
|
sink: &Option<Arc<dyn LineSink>>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
enter_phase(view, Phase::InstallingBuildDeps);
|
||||||
|
|
||||||
|
let host_arch = arch.to_string();
|
||||||
|
let build_arch = context_build_arch(&ctx);
|
||||||
|
let profiles = build_profiles_for(env, &ctx);
|
||||||
|
|
||||||
|
let control_content = ctx
|
||||||
|
.read_file(&Path::new(package_dir).join("debian/control"))
|
||||||
|
.map_err(|e| format!("cannot read debian/control: {e}"))?;
|
||||||
|
let control = ControlInfo::parse_content(&control_content)
|
||||||
|
.map_err(|e| format!("invalid debian/control in {package_dir}: {e}"))?;
|
||||||
|
let source = &control.source;
|
||||||
|
let deps_value = ["Build-Depends", "Build-Depends-Arch", "Build-Depends-Indep"]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| source.get(f))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let conflicts_value = [
|
||||||
|
"Build-Conflicts",
|
||||||
|
"Build-Conflicts-Arch",
|
||||||
|
"Build-Conflicts-Indep",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| source.get(f))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
if deps_value.trim().is_empty() && conflicts_value.trim().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_opts = ParseOpts {
|
||||||
|
host_arch: host_arch.clone(),
|
||||||
|
build_arch: build_arch.clone(),
|
||||||
|
build_profiles: profiles,
|
||||||
|
reduce_restrictions: true,
|
||||||
|
union: false,
|
||||||
|
build_dep: true,
|
||||||
|
};
|
||||||
|
let mut clauses: Vec<Vec<PkgRelation>> = 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<Vec<PkgRelation>> = if conflicts_value.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
let union_opts = ParseOpts {
|
||||||
|
union: true,
|
||||||
|
..parse_opts.clone()
|
||||||
|
};
|
||||||
|
Deps::parse(&conflicts_value, &union_opts)?
|
||||||
|
.clauses()
|
||||||
|
.map(<[PkgRelation]>::to_vec)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let names: Vec<String> = clauses
|
||||||
|
.iter()
|
||||||
|
.chain(conflict_clauses.iter())
|
||||||
|
.flatten()
|
||||||
|
.map(|rel| rel.package.clone())
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
let candidates = query_candidates(&ctx, &names, &build_arch, &host_arch);
|
||||||
|
|
||||||
|
let mut cursors = vec![0usize; clauses.len()];
|
||||||
|
loop {
|
||||||
|
let facts = load_context_facts(&ctx, &build_arch, &host_arch);
|
||||||
|
|
||||||
|
// Build conflicts use the same lookup as dependencies in dpkg's
|
||||||
|
// checker: a satisfied conflict clause aborts before anything
|
||||||
|
// installs.
|
||||||
|
let mut violated = Vec::new();
|
||||||
|
for alternatives in &conflict_clauses {
|
||||||
|
for rel in alternatives {
|
||||||
|
if facts.evaluate_relation(rel) == Some(true) {
|
||||||
|
violated.push(rel.output());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !violated.is_empty() {
|
||||||
|
view.suspend();
|
||||||
|
if let Err(e) =
|
||||||
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
||||||
|
{
|
||||||
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
||||||
|
}
|
||||||
|
return Err(format!(
|
||||||
|
"build dependencies conflict with installed packages: {}",
|
||||||
|
violated.join(", ")
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsatisfied: Vec<usize> = (0..clauses.len())
|
||||||
|
.filter(|&i| {
|
||||||
|
!clauses[i]
|
||||||
|
.iter()
|
||||||
|
.any(|rel| facts.evaluate_relation(rel) == Some(true))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if unsatisfied.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let specs = next_install_pass(
|
||||||
|
&clauses,
|
||||||
|
&unsatisfied,
|
||||||
|
&mut cursors,
|
||||||
|
&candidates,
|
||||||
|
&build_arch,
|
||||||
|
&host_arch,
|
||||||
|
);
|
||||||
|
if specs.is_empty() {
|
||||||
|
view.suspend();
|
||||||
|
if let Err(e) =
|
||||||
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
||||||
|
{
|
||||||
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
||||||
|
}
|
||||||
|
let remaining: Vec<String> = unsatisfied
|
||||||
|
.iter()
|
||||||
|
.map(|&i| {
|
||||||
|
clauses[i]
|
||||||
|
.iter()
|
||||||
|
.map(PkgRelation::output)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" | ")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Err(format!(
|
||||||
|
"Could not satisfy build dependencies per dpkg cross semantics: {}",
|
||||||
|
remaining.join(", ")
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Installing build dependencies: {:?}", specs);
|
||||||
|
let mut cmd = ctx.command("apt-get");
|
||||||
|
cmd.envs(env.clone()).arg("-y").arg("install");
|
||||||
|
for spec in &specs {
|
||||||
|
cmd.arg(spec);
|
||||||
|
}
|
||||||
|
let status = cap(&mut cmd, sink).status()?;
|
||||||
|
if !status.success() {
|
||||||
|
view.suspend();
|
||||||
|
if let Err(e) =
|
||||||
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
||||||
|
{
|
||||||
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
||||||
|
}
|
||||||
|
return Err(format!(
|
||||||
|
"Could not install build-dependencies for the build: {}",
|
||||||
|
specs.join(" ")
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect the binary artifacts (.deb/.udeb) registered by the build in
|
/// Collect the binary artifacts (.deb/.udeb) registered by the build in
|
||||||
/// `debian/files`, returning their paths inside the build context
|
/// `debian/files`, returning their paths inside the build context
|
||||||
/// (`<build_root>/<filename>`). `debian/files` is the canonical record of
|
/// (`<build_root>/<filename>`). `debian/files` is the canonical record of
|
||||||
@@ -724,6 +1091,158 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::context::ContextConfig;
|
use crate::context::ContextConfig;
|
||||||
|
|
||||||
|
fn cand(arch: &str, ma: &str) -> Candidate {
|
||||||
|
Candidate {
|
||||||
|
arch: arch.to_string(),
|
||||||
|
multiarch: ma.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rel(s: &str) -> PkgRelation {
|
||||||
|
crate::debian::deps::parse_simple(s, true).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unqualified dependencies install the dpkg-satisfying variant,
|
||||||
|
/// preferring the runnable build-architecture one: Multi-Arch:
|
||||||
|
/// foreign candidates from the build architecture, then the
|
||||||
|
/// host-architecture candidate dpkg checks them against, then
|
||||||
|
/// `Architecture: all`. A foreign-arch candidate of a non-foreign
|
||||||
|
/// package satisfies nothing in cross mode.
|
||||||
|
#[test]
|
||||||
|
fn install_spec_unqualified_prefers_runnable_then_host() {
|
||||||
|
let host = "arm64";
|
||||||
|
let build = "amd64";
|
||||||
|
|
||||||
|
let spec = install_spec_for(
|
||||||
|
&rel("t"),
|
||||||
|
&[cand("amd64", "foreign"), cand("arm64", "same")],
|
||||||
|
build,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(spec, "t:amd64");
|
||||||
|
|
||||||
|
let spec = install_spec_for(
|
||||||
|
&rel("t"),
|
||||||
|
&[cand("arm64", "same"), cand("amd64", "no")],
|
||||||
|
build,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(spec, "t:arm64");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t"), &[cand("amd64", "no")], build, host),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let spec = install_spec_for(&rel("t"), &[cand("all", "foreign")], build, host).unwrap();
|
||||||
|
assert_eq!(spec, "t");
|
||||||
|
|
||||||
|
// Native builds (host == build): a plain Multi-Arch: no candidate
|
||||||
|
// is the host candidate.
|
||||||
|
let spec = install_spec_for(&rel("t"), &[cand("amd64", "no")], "amd64", "amd64").unwrap();
|
||||||
|
assert_eq!(spec, "t:amd64");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Qualified dependencies install exactly the variant their qualifier
|
||||||
|
/// selects per dpkg semantics; virtual names (no candidates) pass
|
||||||
|
/// through unqualified for apt to resolve a provider.
|
||||||
|
#[test]
|
||||||
|
fn install_spec_qualifiers() {
|
||||||
|
let host = "arm64";
|
||||||
|
let build = "amd64";
|
||||||
|
|
||||||
|
// :native: the build-architecture candidate; an Architecture: all
|
||||||
|
// candidate passes under the plain name; a Multi-Arch: foreign
|
||||||
|
// candidate aborts the lookup, like dpkg's checker.
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:native"), &[cand("amd64", "no")], build, host).unwrap(),
|
||||||
|
"t:amd64"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:native"), &[cand("all", "no")], build, host).unwrap(),
|
||||||
|
"t"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:native"), &[cand("amd64", "foreign")], build, host),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
// :any: a Multi-Arch: allowed candidate, host first.
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(
|
||||||
|
&rel("t:any"),
|
||||||
|
&[cand("amd64", "allowed"), cand("arm64", "allowed")],
|
||||||
|
build,
|
||||||
|
host
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
"t:arm64"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:any"), &[cand("riscv64", "allowed")], build, host).unwrap(),
|
||||||
|
"t:riscv64"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:any"), &[cand("amd64", "same")], build, host),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
// Explicit qualifier: exactly that candidate.
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:arm64"), &[cand("arm64", "same")], build, host).unwrap(),
|
||||||
|
"t:arm64"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t:arm64"), &[cand("amd64", "same")], build, host),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
// Virtual package: debhelper-compat and friends.
|
||||||
|
assert_eq!(
|
||||||
|
install_spec_for(&rel("t (= 13)"), &[], build, host).unwrap(),
|
||||||
|
"t"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolver loop tries the alternatives of a clause in order,
|
||||||
|
/// falling past the ones no candidate can satisfy, and stops
|
||||||
|
/// contributing a clause once its alternatives are exhausted.
|
||||||
|
#[test]
|
||||||
|
fn next_install_pass_falls_through_alternatives() {
|
||||||
|
// `a:arm64` has only an amd64 candidate: no candidate satisfies
|
||||||
|
// the explicit qualifier, so the clause falls through to `t`.
|
||||||
|
let clauses = vec![
|
||||||
|
vec![rel("a:arm64"), rel("t")],
|
||||||
|
vec![rel("u")],
|
||||||
|
vec![rel("v")],
|
||||||
|
];
|
||||||
|
let mut candidates = BTreeMap::new();
|
||||||
|
candidates.insert("a".to_string(), vec![cand("amd64", "same")]);
|
||||||
|
candidates.insert("t".to_string(), vec![cand("arm64", "same")]);
|
||||||
|
candidates.insert("v".to_string(), vec![cand("amd64", "foreign")]);
|
||||||
|
let mut cursors = vec![0; clauses.len()];
|
||||||
|
|
||||||
|
let host = "arm64";
|
||||||
|
let build = "amd64";
|
||||||
|
let specs = next_install_pass(&clauses, &[0, 1, 2], &mut cursors, &candidates, build, host);
|
||||||
|
assert_eq!(
|
||||||
|
specs,
|
||||||
|
vec![
|
||||||
|
"t:arm64".to_string(),
|
||||||
|
"u".to_string(),
|
||||||
|
"v:amd64".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(cursors, vec![2, 1, 1]);
|
||||||
|
|
||||||
|
// An exhausted clause contributes nothing; the loop aborts with
|
||||||
|
// the remaining clauses when nothing installs any more.
|
||||||
|
let specs = next_install_pass(&clauses, &[1], &mut cursors, &candidates, build, host);
|
||||||
|
assert!(specs.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detector_matches_local_options_options_and_dashed_spellings() {
|
fn detector_matches_local_options_options_and_dashed_spellings() {
|
||||||
// The pkh scaffold spelling: bare token in local-options.
|
// The pkh scaffold spelling: bare token in local-options.
|
||||||
|
|||||||
+93
-1
@@ -603,12 +603,34 @@ mod tests {
|
|||||||
/// NOTE: Ideally, we want to run this in CI, but it takes more than 1h
|
/// NOTE: Ideally, we want to run this in CI, but it takes more than 1h
|
||||||
/// to fully build the linux-riscv package on an amd64 builder, which is too
|
/// to fully build the linux-riscv package on an amd64 builder, which is too
|
||||||
/// much time
|
/// much time
|
||||||
|
/// The series is the current LTS (26.04) rather than an interim one:
|
||||||
|
/// interim series vanish from the mirrors a few months after their EOL
|
||||||
|
/// (questing is already unreachable), an LTS stays pullable for years.
|
||||||
#[ignore]
|
#[ignore]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
async fn test_deb_linux_riscv_ubuntu_cross_end_to_end() {
|
async fn test_deb_linux_riscv_ubuntu_cross_end_to_end() {
|
||||||
test_build_end_to_end("linux-riscv", "questing", None, Some("riscv64"), true).await;
|
test_build_end_to_end("linux-riscv", "resolute", None, Some("riscv64"), true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// KNOWN-BROKEN cross build of the noble-era kernel, kept as an
|
||||||
|
/// ignored fixture to work from. Noble controls declare their build
|
||||||
|
/// tools unqualified (the `:native` idiom landed later), so exact dpkg
|
||||||
|
/// semantics demand host-architecture instances of them
|
||||||
|
/// (python3:riscv64, gcc-13:riscv64, clang-17:riscv64, ...) and the
|
||||||
|
/// resulting two-architecture install set is unsolvable: t64
|
||||||
|
/// libraries (libclang1-17t64) conflict with their own foreign-arch
|
||||||
|
/// variant, and the riscv64 toolchain instances drag depends chains
|
||||||
|
/// (gcc:riscv64) that do not resolve from the chroot sources. A real
|
||||||
|
/// run fails at the apt transaction with 'Unable to correct problems'.
|
||||||
|
/// Resolute-era controls declare :native properly; see the test above.
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
async fn test_deb_linux_riscv_noble_cross_end_to_end() {
|
||||||
|
test_build_end_to_end("linux-riscv", "noble", None, Some("riscv64"), true).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is a specific test case for the latest gcc package on Debian
|
/// This is a specific test case for the latest gcc package on Debian
|
||||||
@@ -739,4 +761,74 @@ mod tests {
|
|||||||
"arch-independant .deb not produced, got: {deb_files:?}"
|
"arch-independant .deb not produced, got: {deb_files:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cross build whose build-dependencies cannot be satisfied under
|
||||||
|
/// dpkg's cross semantics must abort at the install step, naming the
|
||||||
|
/// unsatisfied dependency: the expected-failure counterpart of the
|
||||||
|
/// cross builds above.
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
async fn test_deb_cross_unsatisfiable_build_dep_fails_end_to_end() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let pkg_dir = temp_dir.path().join("pkh-crosstest");
|
||||||
|
std::fs::create_dir_all(pkg_dir.join("debian/source")).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/changelog"),
|
||||||
|
"pkh-crosstest (1.0) noble; urgency=medium\n\n \
|
||||||
|
* Synthetic package declaring an unsatisfiable build-dependency.\n\n \
|
||||||
|
-- pkh tests <pkh@example.com> Tue, 15 Sep 2026 08:00:00 +0000\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/control"),
|
||||||
|
"Source: pkh-crosstest\n\
|
||||||
|
Section: devel\n\
|
||||||
|
Priority: optional\n\
|
||||||
|
Maintainer: pkh tests <pkh@example.com>\n\
|
||||||
|
Standards-Version: 4.7.4\n\
|
||||||
|
Build-Depends: pkh-no-such-package-xyz\n\
|
||||||
|
Architecture: any\n\
|
||||||
|
\n\
|
||||||
|
Package: pkh-crosstest\n\
|
||||||
|
Architecture: any\n\
|
||||||
|
Depends: ${misc:Depends}, ${shlibs:Depends}\n\
|
||||||
|
Description: Cross-build negative regression package\n \
|
||||||
|
Declares a build-dependency absent from the archive.\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
pkg_dir.join("debian/rules"),
|
||||||
|
"#!/usr/bin/make -f\n%:\n\tdh $@\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let rules = pkg_dir.join("debian/rules");
|
||||||
|
let mut perms = std::fs::metadata(&rules).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
std::fs::set_permissions(&rules, perms).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(pkg_dir.join("debian/source/format"), "3.0 (native)\n").unwrap();
|
||||||
|
|
||||||
|
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap());
|
||||||
|
|
||||||
|
let err = crate::deb::build_binary_package(DebBuildOptions {
|
||||||
|
arch: Some("arm64".to_string()),
|
||||||
|
series: Some("noble".to_string()),
|
||||||
|
cwd: Some(pkg_dir),
|
||||||
|
cross: true,
|
||||||
|
ctx: Some(ctx),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("an unsatisfiable build-dependency must fail the cross build");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("pkh-no-such-package-xyz"),
|
||||||
|
"error should name the unsatisfied dependency: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1349,6 +1349,233 @@ Provides: virt2 (>= 1.0), plain
|
|||||||
assert_eq!(facts.evaluate_relation(&o("plain")), Some(false));
|
assert_eq!(facts.evaluate_relation(&o("plain")), Some(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cross-compilation semantics of the package lookup (the `Facts`
|
||||||
|
/// host/build split), mirroring `Dpkg::Deps::KnownFacts::_find_package`:
|
||||||
|
/// an unqualified dependency is satisfied by the HOST architecture
|
||||||
|
/// instance, by any instance of a `Multi-Arch: foreign` package, or by
|
||||||
|
/// an `Architecture: all` instance — never by a foreign-arch instance
|
||||||
|
/// of a `Multi-Arch: no`/`same` package. The build-architecture
|
||||||
|
/// instances only come into play through `:native`.
|
||||||
|
///
|
||||||
|
/// Verdicts marked «live» were probed against `dpkg-checkbuilddeps -a
|
||||||
|
/// <host>` on a real amd64 system carrying the build-arch instances.
|
||||||
|
#[test]
|
||||||
|
fn cross_lookup_matrix() {
|
||||||
|
const O: fn(&str) -> PkgRelation = |s| parse_simple(s, true).unwrap();
|
||||||
|
// host = arm64 (target), build = amd64 (machine): the instances
|
||||||
|
// below simulate what a cross-building amd64 machine has installed.
|
||||||
|
let facts = |ma_build: &str, ma_host: &str| {
|
||||||
|
let mut f = Facts::new("arm64", "amd64");
|
||||||
|
if !ma_build.is_empty() {
|
||||||
|
f.add_installed("t", "1.0", "amd64", ma_build);
|
||||||
|
}
|
||||||
|
if !ma_host.is_empty() {
|
||||||
|
f.add_installed("t", "1.0", "arm64", ma_host);
|
||||||
|
}
|
||||||
|
f
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unqualified: only the host-arch instance satisfies...
|
||||||
|
assert_eq!(
|
||||||
|
facts("no", "").evaluate_relation(&O("t")),
|
||||||
|
Some(false),
|
||||||
|
"M-A:no build-arch instance must not satisfy an unqualified dep"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
facts("same", "").evaluate_relation(&O("t")),
|
||||||
|
Some(false),
|
||||||
|
"M-A:same build-arch instance must not satisfy an unqualified dep"
|
||||||
|
);
|
||||||
|
assert_eq!(facts("", "no").evaluate_relation(&O("t")), Some(true));
|
||||||
|
assert_eq!(facts("", "same").evaluate_relation(&O("t")), Some(true));
|
||||||
|
// ...unless the package is Multi-Arch: foreign («live»: bison,
|
||||||
|
// flex: the natively-installed variant satisfies the cross check).
|
||||||
|
assert_eq!(facts("foreign", "").evaluate_relation(&O("t")), Some(true));
|
||||||
|
assert_eq!(facts("", "foreign").evaluate_relation(&O("t")), Some(true));
|
||||||
|
|
||||||
|
// `Architecture: all` instances satisfy unqualified dependencies
|
||||||
|
// whatever the Multi-Arch attribute.
|
||||||
|
let mut all = Facts::new("arm64", "amd64");
|
||||||
|
all.add_installed("t", "1.0", "all", "foreign");
|
||||||
|
assert_eq!(all.evaluate_relation(&O("t")), Some(true));
|
||||||
|
let mut all2 = Facts::new("arm64", "amd64");
|
||||||
|
all2.add_installed("t", "1.0", "all", "no");
|
||||||
|
assert_eq!(all2.evaluate_relation(&O("t")), Some(true));
|
||||||
|
|
||||||
|
// Versioned relations check the first matching instance only:
|
||||||
|
// insertion order decides which instance a dependency binds to,
|
||||||
|
// and an unsatisfying version does not fall through to later
|
||||||
|
// instances.
|
||||||
|
let mut mixed = Facts::new("arm64", "amd64");
|
||||||
|
mixed.add_installed("t", "0.5", "arm64", "no");
|
||||||
|
mixed.add_installed("t", "3.0", "amd64", "no");
|
||||||
|
assert_eq!(mixed.evaluate_relation(&O("t (>= 1)")), Some(false));
|
||||||
|
assert_eq!(mixed.evaluate_relation(&O("t (<< 1)")), Some(true));
|
||||||
|
|
||||||
|
// `:native`: the build-architecture instance satisfies («live»:
|
||||||
|
// gcc:native on an amd64 machine, whatever the target); an
|
||||||
|
// Architecture: all instance does too — but a Multi-Arch: foreign
|
||||||
|
// instance aborts the whole lookup, even on the build architecture
|
||||||
|
// («live»: flex:native with natively-installed M-A:foreign flex is
|
||||||
|
// unmet).
|
||||||
|
assert_eq!(
|
||||||
|
facts("no", "").evaluate_relation(&O("t:native")),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
facts("same", "").evaluate_relation(&O("t:native")),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
facts("foreign", "").evaluate_relation(&O("t:native")),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
// An Architecture: all instance satisfies :native — unless it is
|
||||||
|
// Multi-Arch: foreign, which aborts the lookup like any foreign
|
||||||
|
// instance.
|
||||||
|
assert_eq!(all2.evaluate_relation(&O("t:native")), Some(true));
|
||||||
|
assert_eq!(all.evaluate_relation(&O("t:native")), Some(false));
|
||||||
|
assert_eq!(
|
||||||
|
facts("", "no").evaluate_relation(&O("t:native")),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// `:any`: only a Multi-Arch: allowed instance satisfies, on any
|
||||||
|
// architecture («live»: libssl-dev:any with M-A:same libssl-dev is
|
||||||
|
// unmet).
|
||||||
|
assert_eq!(
|
||||||
|
facts("allowed", "").evaluate_relation(&O("t:any")),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
facts("", "allowed").evaluate_relation(&O("t:any")),
|
||||||
|
Some(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
facts("same", "").evaluate_relation(&O("t:any")),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Explicit architecture qualifier: only that exact instance.
|
||||||
|
assert_eq!(facts("no", "").evaluate_relation(&O("t:amd64")), Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
facts("", "no").evaluate_relation(&O("t:amd64")),
|
||||||
|
Some(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same cross matrix, validated against the real
|
||||||
|
/// `dpkg-checkbuilddeps`: for each fixture the exit status and the
|
||||||
|
/// reported unmet list must match, with host != build (the machine is
|
||||||
|
/// the native architecture; the host architecture is a foreign one).
|
||||||
|
#[test]
|
||||||
|
fn diff_checkbuilddeps_cross_matrix() {
|
||||||
|
let build_arch = arch::native().unwrap_or_else(|_| "amd64".into());
|
||||||
|
// Any foreign arch the dpkg tables know; the instances only exist
|
||||||
|
// in the synthetic status file.
|
||||||
|
let host_arch = if build_arch == "arm64" {
|
||||||
|
"riscv64".to_string()
|
||||||
|
} else {
|
||||||
|
"arm64".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mk_status = |entries: &[(&str, &str)]| {
|
||||||
|
let mut s = String::new();
|
||||||
|
for (pkg_arch, ma) in entries {
|
||||||
|
let ma = if ma.is_empty() { "no" } else { ma };
|
||||||
|
s.push_str(&format!(
|
||||||
|
"Package: t\nStatus: install ok installed\nVersion: 1.0\nArchitecture: {pkg_arch}\nMulti-Arch: {ma}\n\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
for (name, entries, dep) in [
|
||||||
|
// Unqualified: build-arch instances never satisfy, host-arch
|
||||||
|
// and Multi-Arch: foreign do.
|
||||||
|
("ma-no-build", &[("amd64", "no")] as &[(&str, &str)], "t"),
|
||||||
|
("ma-same-build", &[("amd64", "same")], "t"),
|
||||||
|
("ma-no-host", &[("arm64", "no")], "t"),
|
||||||
|
("ma-same-host", &[("arm64", "same")], "t"),
|
||||||
|
("ma-foreign-build", &[("amd64", "foreign")], "t"),
|
||||||
|
("all-build", &[("all", "foreign")], "t"),
|
||||||
|
// :native and :any qualifiers.
|
||||||
|
("native-build", &[("amd64", "no")], "t:native"),
|
||||||
|
("native-foreign-build", &[("amd64", "foreign")], "t:native"),
|
||||||
|
("any-allowed-build", &[("amd64", "allowed")], "t:any"),
|
||||||
|
("any-same-build", &[("amd64", "same")], "t:any"),
|
||||||
|
("explicit-build", &[("amd64", "no")], "t:amd64"),
|
||||||
|
("explicit-host", &[("arm64", "no")], "t:amd64"),
|
||||||
|
] {
|
||||||
|
// Substitute the foreign architecture for fixtures that name
|
||||||
|
// the host arch explicitly.
|
||||||
|
let dep = dep.replace("arm64", &host_arch);
|
||||||
|
let entries: Vec<(String, &str)> = entries
|
||||||
|
.iter()
|
||||||
|
.map(|(a, m)| (a.replace("amd64", &build_arch), *m))
|
||||||
|
.collect();
|
||||||
|
let entries: Vec<(&str, &str)> =
|
||||||
|
entries.iter().map(|(a, m)| (a.as_str(), *m)).collect();
|
||||||
|
let status = mk_status(&entries);
|
||||||
|
let verdict = |bd: &str, host: &str| {
|
||||||
|
diff_cross_case(bd, &status, &build_arch, host, |ours, real| {
|
||||||
|
assert_eq!(ours, real, "verdict mismatch for {name}")
|
||||||
|
})
|
||||||
|
};
|
||||||
|
verdict(&dep, &host_arch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One differential cross case: run the real `dpkg-checkbuilddeps`
|
||||||
|
/// with `-a <host>` against a synthetic admindir, and the native
|
||||||
|
/// checker with the equivalent options on the same control, then hand
|
||||||
|
/// both verdicts to `compare`.
|
||||||
|
fn diff_cross_case(
|
||||||
|
bd: &str,
|
||||||
|
status: &str,
|
||||||
|
build_arch: &str,
|
||||||
|
host_arch: &str,
|
||||||
|
compare: impl Fn(bool, bool),
|
||||||
|
) {
|
||||||
|
let control_text = format!(
|
||||||
|
"Source: t\nMaintainer: a <a@b.c>\nBuild-Depends: {bd}\n\nPackage: t\nArchitecture: any\nDescription: x\n y\n"
|
||||||
|
);
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("control"), &control_text).unwrap();
|
||||||
|
let admindir = dir.path().join("admin");
|
||||||
|
std::fs::create_dir_all(&admindir).unwrap();
|
||||||
|
std::fs::write(admindir.join("status"), status).unwrap();
|
||||||
|
|
||||||
|
let output = std::process::Command::new("dpkg-checkbuilddeps")
|
||||||
|
.current_dir(dir.path())
|
||||||
|
.env("LC_ALL", "C")
|
||||||
|
.arg("--admindir")
|
||||||
|
.arg(&admindir)
|
||||||
|
.arg("-a")
|
||||||
|
.arg(host_arch)
|
||||||
|
.arg("-I")
|
||||||
|
.arg("control")
|
||||||
|
.output()
|
||||||
|
.expect("run dpkg-checkbuilddeps (is dpkg-dev installed?)");
|
||||||
|
let real_ok = output.status.success();
|
||||||
|
|
||||||
|
let opts = CheckOpts {
|
||||||
|
host_arch: host_arch.to_string(),
|
||||||
|
build_arch: build_arch.to_string(),
|
||||||
|
build_profiles: Vec::new(),
|
||||||
|
ignore_arch: false,
|
||||||
|
ignore_indep: false,
|
||||||
|
ignore_builtin: true,
|
||||||
|
admindir: admindir.to_path_buf(),
|
||||||
|
};
|
||||||
|
let control = ControlInfo::parse_content(&control_text).unwrap();
|
||||||
|
let ours_ok = check_build_depends(&control, &opts)
|
||||||
|
.expect("native check failure")
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
compare(ours_ok, real_ok);
|
||||||
|
}
|
||||||
|
|
||||||
/// The same undecidable verdicts through the direct facts API: an
|
/// The same undecidable verdicts through the direct facts API: an
|
||||||
/// unreadable provided version and an invalid (non-`=`) provide each
|
/// unreadable provided version and an invalid (non-`=`) provide each
|
||||||
/// leave a versioned relation undecided, while a readable provider
|
/// leave a versioned relation undecided, while a readable provider
|
||||||
|
|||||||
+203
-17
@@ -4,15 +4,46 @@
|
|||||||
//! and apply them during pull and deb operations.
|
//! and apply them during pull and deb operations.
|
||||||
|
|
||||||
use crate::data::embed_data;
|
use crate::data::embed_data;
|
||||||
|
use crate::debian::deps::{Deps, ParseOpts, PkgRelation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
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)
|
/// Quirks configuration for a specific operation (pull or deb)
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct OperationQuirks {
|
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)]
|
#[serde(default)]
|
||||||
pub extra_dependencies: Vec<String>,
|
pub series: Vec<String>,
|
||||||
|
|
||||||
|
/// Build-dependency resolution rules.
|
||||||
|
#[serde(default)]
|
||||||
|
pub dependencies: Option<DependencyQuirks>,
|
||||||
|
|
||||||
/// Additional parameters for the operation
|
/// Additional parameters for the operation
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -63,24 +94,29 @@ pub fn get_package_quirks<'a>(
|
|||||||
config.quirks.get(package)
|
config.quirks.get(package)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get deb-time extra dependencies for a package
|
/// Whether a quirks entry applies to `series`: an empty series filter
|
||||||
///
|
/// matches every series, otherwise the series must be listed.
|
||||||
/// This function returns the list of extra dependencies that should be installed
|
fn entry_applies_to_series(quirks: &OperationQuirks, series: &str) -> bool {
|
||||||
/// before building a package, as defined in the quirks configuration.
|
quirks.series.is_empty() || quirks.series.iter().any(|s| s == series)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the build-dependency resolution rules of a package for a series
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `package` - The package name
|
/// * `package` - The package name
|
||||||
|
/// * `series` - The distribution series (e.g. "resolute")
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Vec<String>` - List of extra dependencies, or empty vector if none
|
/// * `Option<DependencyQuirks>` - The rules, or None when the package has
|
||||||
pub fn get_deb_extra_dependencies(package: &str) -> Vec<String> {
|
/// no deb entry or the entry does not apply to the series
|
||||||
if let Some(quirks) = get_package_quirks(&QUIRKS_DATA, package)
|
pub fn get_deb_dependency_quirks(package: &str, series: &str) -> Option<DependencyQuirks> {
|
||||||
&& let Some(deb_quirks) = &quirks.deb
|
let quirks = get_package_quirks(&QUIRKS_DATA, package)?;
|
||||||
{
|
let deb = quirks.deb.as_ref()?;
|
||||||
return deb_quirks.extra_dependencies.clone();
|
if entry_applies_to_series(deb, series) {
|
||||||
|
deb.dependencies.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get package directories from quirks configuration
|
/// Get package directories from quirks configuration
|
||||||
@@ -111,15 +147,165 @@ pub fn get_package_directories(package: &str) -> Vec<String> {
|
|||||||
Vec::new()
|
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<Vec<PkgRelation>>,
|
||||||
|
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<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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_unknown_package_has_no_quirks() {
|
fn test_unknown_package_has_no_quirks() {
|
||||||
// A package absent from quirks.yml (currently every package) has no
|
// A package absent from quirks.yml has no dependency rules nor
|
||||||
// extra dependencies nor custom directories, and must not panic
|
// custom directories, and must not panic
|
||||||
assert!(get_deb_extra_dependencies("not-in-quirks").is_empty());
|
assert!(get_deb_dependency_quirks("not-in-quirks", "resolute").is_none());
|
||||||
assert!(get_package_directories("not-in-quirks").is_empty());
|
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 <!stage1>")
|
||||||
|
);
|
||||||
|
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 <!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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user