The DEP-8 run-time contract is one page, so pkh implements it natively instead of wrapping autopkgtest (see plans/pkh-test.md): the testbed is a pkh context — chroot mode reuses the deb ephemeral chroots and their cached mmdebstrap tarballs, local mode runs on the current context — and the binaries under test are the freshness-guarded pkh deb output next to the tree, rebuilt through the deb flow when stale or missing. Per test: restrictions gate on the testbed (isolation-machine, needs-reboot and container isolation skip with a pointer at --mode vm), the stanza Depends are expanded (@, @builddeps@, @recommends@) and installed, then the script or Test-Command runs in the staged tree with the AUTOPKGTEST_* (and legacy ADT_*) environment, a per-test timeout and the stderr/exit-77 verdict rules; flaky tests retry once. stdout, stderr and $AUTOPKGTEST_ARTIFACTS are collected per test under /var/tmp/pkh/tests/ (pruned separately). --shell/--shell-fail drop into the testbed for debugging. pin_pocket and install_injected_packages become crate-internal helpers shared with the deb flow, which they mirror.
249 lines
8.4 KiB
Rust
249 lines
8.4 KiB
Rust
//! DEP-8 `Depends:` substitution: `@`, `@builddeps@` and `@recommends@`
|
|
//! expanded into concrete package specs for `apt-get install`.
|
|
//!
|
|
//! The built binaries themselves are installed separately (from the
|
|
//! staged `.deb` files, so tests exercise exactly what pkh built);
|
|
//! expansion substitutes bare binary *names*, which apt then resolves
|
|
//! against the installed binaries first and the archive second.
|
|
|
|
use crate::debian::control::ControlInfo;
|
|
use crate::debian::deps::{Deps, ParseOpts};
|
|
|
|
/// Expand a test stanza's `Depends:` value into apt package specs.
|
|
///
|
|
/// `raw` is the value as written (`None` = absent, the DEP-8 default
|
|
/// `@`). The substitutions, per DEP-8:
|
|
///
|
|
/// - `@` in an or-clause replicates the clause once per binary package
|
|
/// built from the source, with the name substituted (other
|
|
/// alternatives and version constraints are kept);
|
|
/// - `@builddeps@` becomes the source's Build-Depends,
|
|
/// Build-Depends-Indep and Build-Depends-Arch plus `build-essential`;
|
|
/// - `@recommends@` becomes the Recommends of every binary package,
|
|
/// with `${...}` substvar references dropped (autopkgtest's
|
|
/// "variables stripped") but versions kept.
|
|
///
|
|
/// Returns one apt spec per comma-separated clause, in order; an empty
|
|
/// result means nothing to install.
|
|
pub fn expand_depends(
|
|
raw: Option<&str>,
|
|
control: &ControlInfo,
|
|
binaries: &[String],
|
|
host_arch: &str,
|
|
) -> Result<Vec<String>, String> {
|
|
let raw = raw.unwrap_or("@");
|
|
let mut specs = Vec::new();
|
|
for token in split_top_level_commas(raw) {
|
|
match token.as_str() {
|
|
"@builddeps@" => {
|
|
for field in ["Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"] {
|
|
if let Some(value) = non_empty(control.source.get(field)) {
|
|
for clause in split_top_level_commas(value) {
|
|
if !clause.is_empty() {
|
|
specs.push(clause);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
specs.push("build-essential".to_string());
|
|
}
|
|
"@recommends@" => {
|
|
for paragraph in &control.binaries {
|
|
if let Some(value) = non_empty(paragraph.get("Recommends")) {
|
|
specs.extend(name_only_clauses(value, host_arch)?);
|
|
}
|
|
}
|
|
}
|
|
token if token.split_whitespace().any(|word| word == "@") => {
|
|
// Textual substitution: DEP-8 @ is a word-level
|
|
// placeholder the deps grammar does not know. The clause
|
|
// replicates once per binary with the name substituted,
|
|
// constraints and alternatives kept verbatim.
|
|
for binary in binaries {
|
|
specs.push(
|
|
token
|
|
.split_whitespace()
|
|
.map(|word| {
|
|
if word == "@" {
|
|
binary.clone()
|
|
} else {
|
|
word.to_string()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" "),
|
|
);
|
|
}
|
|
}
|
|
token => specs.push(token.to_string()),
|
|
}
|
|
}
|
|
Ok(specs)
|
|
}
|
|
|
|
/// Split on commas that sit outside any parentheses: or-clauses carry
|
|
/// their own commas inside `(...)` constraints, top-level commas separate
|
|
/// dependencies.
|
|
fn split_top_level_commas(input: &str) -> Vec<String> {
|
|
let mut parts = Vec::new();
|
|
let mut depth = 0usize;
|
|
let mut current = String::new();
|
|
for character in input.chars() {
|
|
match character {
|
|
'(' => {
|
|
depth += 1;
|
|
current.push(character);
|
|
}
|
|
')' => {
|
|
depth = depth.saturating_sub(1);
|
|
current.push(character);
|
|
}
|
|
',' if depth == 0 => {
|
|
parts.push(current.trim().to_string());
|
|
current.clear();
|
|
}
|
|
_ => current.push(character),
|
|
}
|
|
}
|
|
parts.push(current.trim().to_string());
|
|
parts
|
|
}
|
|
|
|
/// Render each or-clause of a dependency list, dropping alternatives
|
|
/// that are `${...}` substvar references (autopkgtest's "variables
|
|
/// stripped") and keeping everything else verbatim.
|
|
fn name_only_clauses(input: &str, host_arch: &str) -> Result<Vec<String>, String> {
|
|
let opts = ParseOpts::build_deps(host_arch.to_string(), Vec::new());
|
|
let deps = Deps::parse(input, &opts)?;
|
|
let mut clauses = Vec::new();
|
|
for clause in deps.clauses() {
|
|
let kept: Vec<String> = clause
|
|
.iter()
|
|
.filter(|relation| !relation.package.contains('$'))
|
|
.map(|relation| relation.output())
|
|
.collect();
|
|
if !kept.is_empty() {
|
|
clauses.push(kept.join(" | "));
|
|
}
|
|
}
|
|
Ok(clauses)
|
|
}
|
|
|
|
/// A field value with its surrounding whitespace trimmed; `None` when the
|
|
/// field is absent or whitespace-only.
|
|
fn non_empty(value: Option<&str>) -> Option<&str> {
|
|
value.map(str::trim).filter(|value| !value.is_empty())
|
|
}
|
|
|
|
/// The binary package names declared by a control file, in stanza order.
|
|
pub fn binary_names(control: &ControlInfo) -> Vec<String> {
|
|
control
|
|
.binaries
|
|
.iter()
|
|
.filter_map(|paragraph| paragraph.get("Package"))
|
|
.map(|name| name.trim().to_string())
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::str::FromStr;
|
|
|
|
fn control() -> ControlInfo {
|
|
ControlInfo::from_str(
|
|
"Source: hello\n\
|
|
Build-Depends: debhelper-compat (= 13),\n meson\n\
|
|
Build-Depends-Indep: dh-python\n\
|
|
\n\
|
|
Package: hello\n\
|
|
Architecture: any\n\
|
|
Recommends: hello-doc, pulseaudio (>= 15)\n\
|
|
\n\
|
|
Package: hello-doc\n\
|
|
Architecture: all\n\
|
|
Recommends: hello\n",
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn binaries() -> Vec<String> {
|
|
vec!["hello".to_string(), "hello-doc".to_string()]
|
|
}
|
|
|
|
/// The default (absent) Depends is `@`: one spec per binary.
|
|
#[test]
|
|
fn default_is_at() {
|
|
let specs = expand_depends(None, &control(), &binaries(), "amd64").unwrap();
|
|
assert_eq!(specs, ["hello", "hello-doc"]);
|
|
}
|
|
|
|
/// An or-clause containing @ replicates once per binary, keeping the
|
|
/// other alternatives and version constraints.
|
|
#[test]
|
|
fn or_clauses_replicate_per_binary() {
|
|
let specs = expand_depends(
|
|
Some("@ (>= 1.0) | fallback, jq"),
|
|
&control(),
|
|
&binaries(),
|
|
"amd64",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
specs,
|
|
[
|
|
"hello (>= 1.0) | fallback",
|
|
"hello-doc (>= 1.0) | fallback",
|
|
"jq"
|
|
]
|
|
);
|
|
}
|
|
|
|
/// @builddeps@ pulls the three Build-* fields plus build-essential.
|
|
#[test]
|
|
fn builddeps_expansion() {
|
|
let specs =
|
|
expand_depends(Some("@builddeps@, @"), &control(), &binaries(), "amd64").unwrap();
|
|
assert_eq!(
|
|
specs,
|
|
[
|
|
"debhelper-compat (= 13)",
|
|
"meson",
|
|
"dh-python",
|
|
"build-essential",
|
|
"hello",
|
|
"hello-doc"
|
|
]
|
|
);
|
|
}
|
|
|
|
/// @recommends@ collects every binary's Recommends, stripped to names.
|
|
#[test]
|
|
fn recommends_expansion() {
|
|
let specs = expand_depends(Some("@recommends@"), &control(), &binaries(), "amd64").unwrap();
|
|
assert_eq!(specs, ["hello-doc", "pulseaudio (>= 15)", "hello"]);
|
|
}
|
|
|
|
/// Plain dependencies pass through untouched; a token mentioning @ in
|
|
/// a version constraint is still a substitution.
|
|
#[test]
|
|
fn plain_tokens_pass_through() {
|
|
let specs = expand_depends(
|
|
Some("ca-certificates, python3:any"),
|
|
&control(),
|
|
&binaries(),
|
|
"amd64",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(specs, ["ca-certificates", "python3:any"]);
|
|
}
|
|
|
|
/// Commas inside version constraints do not split clauses.
|
|
#[test]
|
|
fn parenthesised_commas_stay_together() {
|
|
let specs =
|
|
expand_depends(Some("foo (>= 1) , bar"), &control(), &binaries(), "amd64").unwrap();
|
|
assert_eq!(specs, ["foo (>= 1)", "bar"]);
|
|
}
|
|
}
|