deb,cli,test: add --mirror to build and resolve from a pinned mirror

Pinned runners (a caching proxy, a chosen mirror) must resolve every
package from one base URL. 'pkh deb --mirror <url>' passes the mirror
to mmdebstrap as its mirror positional, so the chroot tarball's apt
sources carry it from the bootstrap on and every apt inside the build
follows - no per-build source rewriting. The mirror must serve every
suite and architecture the build resolves (the security pocket and,
for cross builds, the target archive included); that contract is
documented, not enforced.

The tarball cache is keyed per mirror (a short sha prefix before the
'-buildd.tar.xz' suffix prune matches): a --mirror run over a stale
distro-default tarball would silently resolve from the distro, and
'--incremental' records must never fork across a mirror change.

The runtime source passes recognize the pinned URL as an archive
source (it carries no official host for the host-based checks): the
pocket pass and the universe pass treat it as the archive, and the
cross setup gives it every component and suite, no arch scoping (the
mirror serves every architecture by contract) and no ports entry.
'pkh test' passes no mirror: its tarballs stay distro-default.
This commit is contained in:
2026-09-30 00:04:50 +02:00
parent 8cf9489072
commit 6385cf6dc1
9 changed files with 201 additions and 32 deletions
+11
View File
@@ -191,6 +191,17 @@ A build killed hard (OOM) can leave its chroot behind; the next run of
that target sweeps it (unmounting and escalating through sudo when that target sweeps it (unmounting and escalating through sudo when
needed), and `pkh prune` collects the rest. needed), and `pkh prune` collects the rest.
On a pinned runner — a machine that must resolve every package from one
mirror, typically a caching proxy — `pkh deb --mirror <url>` builds the
chroot and resolves every package from that base URL: the mirror is
passed to mmdebstrap at bootstrap, so the chroot tarball's apt sources
carry it and every apt inside the build follows. The mirror must serve
every suite and architecture the build resolves (the security pocket
and, for `--cross` builds, the target architecture's archive included);
PPAs and other third-party sources are left alone. The tarball cache is
keyed per mirror, so pinned and unpinned builds keep separate tarballs
(and `--incremental` records never fork across a mirror change).
### Running the DEP-8 tests ### Running the DEP-8 tests
`pkh test` runs the package's as-installed tests (`debian/tests/control`, `pkh test` runs the package's as-installed tests (`debian/tests/control`,
+7 -3
View File
@@ -86,9 +86,13 @@ act on the package in the current directory.
- `pkh deb` builds the binary packages in an isolated context with the - `pkh deb` builds the binary packages in an isolated context with the
build dependencies installed. `--ppa` (repeatable) adds dependency build dependencies installed. `--ppa` (repeatable) adds dependency
sources, `--inject <package|.deb>` preinstalls a package, and `-j` sources, `--inject <package|.deb>` preinstalls a package, and `-j`
caps parallel jobs. `--cross` cross-compiles instead of using caps parallel jobs. `--mirror <url>` builds the chroot and resolves
qemu-binfmt, but most packages cannot cross-compile, so prefer qemu. every package from that base URL (a caching proxy on a pinned runner;
Leave `--mode` unset unless you need a specific build context. it must serve every suite and architecture the build resolves).
`--cross`
cross-compiles instead of using qemu-binfmt, but most packages cannot
cross-compile, so prefer qemu. Leave `--mode` unset unless you need a
specific build context.
- `pkh lint [path]` runs lintian plus the pkh-native checks. `--json` - `pkh lint [path]` runs lintian plus the pkh-native checks. `--json`
emits a machine-readable report and `--list-tags` prints the native emits a machine-readable report and `--list-tags` prints the native
+21 -6
View File
@@ -99,6 +99,10 @@ fn cross_suites(
/// are scoped to it and carry every component and cross-build suite, and /// are scoped to it and carry every component and cross-build suite, and
/// the mirror serving the target architecture (ports, for the non-local /// the mirror serving the target architecture (ports, for the non-local
/// ones) is added when no existing source serves the arch from it yet. /// ones) is added when no existing source serves the arch from it yet.
/// Under a mirror pin (`mirror`), the tarball's own source already points
/// at the pinned URL: it gets the components and suites, no arch scoping
/// (the mirror serves every architecture by contract), and no ports
/// entry — a source at the pinned URL counts as serving the target.
/// Debian hosts are left alone: one mirror serves every architecture, so /// Debian hosts are left alone: one mirror serves every architecture, so
/// the host's own sources already cover the target — the os-release gate /// the host's own sources already cover the target — the os-release gate
/// below is what makes that a data conclusion instead of hardcoding. /// below is what makes that a data conclusion instead of hardcoding.
@@ -106,6 +110,7 @@ pub fn ensure_repositories(
arch: &str, arch: &str,
series: &str, series: &str,
pocket: Option<&str>, pocket: Option<&str>,
mirror: Option<&str>,
ctx: Arc<Context>, ctx: Arc<Context>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let local_arch = crate::get_current_arch(); let local_arch = crate::get_current_arch();
@@ -146,14 +151,19 @@ pub fn ensure_repositories(
// Ensure all components are enabled for the primary architecture // Ensure all components are enabled for the primary architecture
for source in &mut sources { for source in &mut sources {
// Official sources served by the local mirror (the primary archive // Official sources served by the local mirror (the primary archive
// and its security sibling); ports serves the other architectures // and its security sibling), or the mirror-pinned source itself
// (the chroot tarball was bootstrapped from it — see
// `DebBuildOptions::mirror`); ports serves the other architectures
// and is configured below instead // and is configured below instead
if !crate::distro_info::is_mirror_source(local_mirror, &source.uri) { let pinned = mirror.is_some_and(|m| source.uri == m);
if !pinned && !crate::distro_info::is_mirror_source(local_mirror, &source.uri) {
continue; continue;
} }
// Scope to local_arch if not already scoped // Scope to local_arch if not already scoped — never under a pin:
if source.architectures.is_empty() { // the pinned mirror serves the target architecture too (its
// contract), scoping it would cut the target apt off
if !pinned && source.architectures.is_empty() {
source.architectures.push(local_arch.clone()); source.architectures.push(local_arch.clone());
} }
@@ -174,9 +184,14 @@ pub fn ensure_repositories(
// Check whether an existing source already serves the target // Check whether an existing source already serves the target
// architecture from its mirror (e.g. the ports mirror for a // architecture from its mirror (e.g. the ports mirror for a
// non-local arch); when cross-building for the local architecture, // non-local arch), or from the pinned mirror — which serves every
// the primary sources above already do // architecture by contract, so its (unscoped) source counts; when
// cross-building for the local architecture, the primary sources
// above already do
let has_target = sources.iter().any(|s| { let has_target = sources.iter().any(|s| {
if mirror.is_some_and(|m| s.uri == m) {
return s.architectures.is_empty() || s.architectures.contains(&arch.to_string());
}
crate::distro_info::is_mirror_source(target_mirror, &s.uri) crate::distro_info::is_mirror_source(target_mirror, &s.uri)
&& s.architectures.contains(&arch.to_string()) && s.architectures.contains(&arch.to_string())
}); });
+85 -15
View File
@@ -4,6 +4,7 @@ use crate::deb::{Phase, enter_phase};
use crate::interrupt::CleanupHookGuard; use crate::interrupt::CleanupHookGuard;
use crate::report::BuildView; use crate::report::BuildView;
use directories::ProjectDirs; use directories::ProjectDirs;
use sha2::{Digest, Sha256};
use std::error::Error; use std::error::Error;
use std::fs; use std::fs;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
@@ -251,19 +252,42 @@ fn is_mountpoint(path: &Path) -> bool {
/// The cached chroot tarball path for a series/arch, as used by the /// The cached chroot tarball path for a series/arch, as used by the
/// bootstrap: the record's fork gate compares the tarball's digest /// bootstrap: the record's fork gate compares the tarball's digest
/// against the sha recorded in a base's manifest. /// against the sha recorded in a base's manifest. A mirror pin is hashed
/// into the name (before the `-buildd.tar.xz` suffix prune matches), so
/// a pinned tarball never collides with the distro-default one — a
/// `--mirror` run over a stale distro tarball would silently resolve
/// from the distro, exactly the lie the pin must not tell.
pub(crate) fn chroot_tarball_path( pub(crate) fn chroot_tarball_path(
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
) -> Result<PathBuf, Box<dyn Error>> { ) -> Result<PathBuf, Box<dyn Error>> {
let proj_dirs = let proj_dirs =
ProjectDirs::from("com", "pkh", "pkh").ok_or("Could not determine project directories")?; ProjectDirs::from("com", "pkh", "pkh").ok_or("Could not determine project directories")?;
let tarball_filename = if let Some(a) = arch { Ok(proj_dirs
format!("{}-{}-buildd.tar.xz", series, a) .cache_dir()
} else { .join(chroot_tarball_filename(series, arch, mirror)))
format!("{}-buildd.tar.xz", series) }
/// The cached chroot tarball's file name for a series/arch/mirror
pub(crate) fn chroot_tarball_filename(
series: &str,
arch: Option<&str>,
mirror: Option<&str>,
) -> String {
let target = match arch {
Some(a) => format!("{series}-{a}"),
None => series.to_string(),
}; };
Ok(proj_dirs.cache_dir().join(tarball_filename)) match mirror {
None => format!("{target}-buildd.tar.xz"),
Some(m) => {
let mut hasher = Sha256::new();
hasher.update(m.as_bytes());
let hash = format!("{:x}", hasher.finalize());
format!("{target}-{}-buildd.tar.xz", &hash[..12])
}
}
} }
/// Root of the shared apt download caches (`/var/tmp/pkh/apt-cache`), /// Root of the shared apt download caches (`/var/tmp/pkh/apt-cache`),
@@ -490,10 +514,11 @@ impl EphemeralContextGuard {
pub async fn new_with_context( pub async fn new_with_context(
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
view: &dyn BuildView, view: &dyn BuildView,
) -> Result<Self, Box<dyn Error>> { ) -> Result<Self, Box<dyn Error>> {
Self::new_internal(None, series, arch, base_ctx, view, false, None).await Self::new_internal(None, series, arch, mirror, base_ctx, view, false, None).await
} }
/// Create the ephemeral build context inside `session`'s chroot. /// Create the ephemeral build context inside `session`'s chroot.
@@ -511,10 +536,12 @@ impl EphemeralContextGuard {
/// * `ccache_dir` - The shared ccache directory to mount at the /// * `ccache_dir` - The shared ccache directory to mount at the
/// chroot's `/var/cache/pkh-ccache` (`pkh deb` computes it from the /// chroot's `/var/cache/pkh-ccache` (`pkh deb` computes it from the
/// build identity; `None` leaves the chroot unshared) /// build identity; `None` leaves the chroot unshared)
#[allow(clippy::too_many_arguments)]
pub async fn new_for_session( pub async fn new_for_session(
session: Session, session: Session,
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
view: &dyn BuildView, view: &dyn BuildView,
keep_upper: bool, keep_upper: bool,
@@ -524,6 +551,7 @@ impl EphemeralContextGuard {
Some(session), Some(session),
series, series,
arch, arch,
mirror,
base_ctx, base_ctx,
view, view,
keep_upper, keep_upper,
@@ -532,10 +560,12 @@ impl EphemeralContextGuard {
.await .await
} }
#[allow(clippy::too_many_arguments)]
async fn new_internal( async fn new_internal(
session: Option<Session>, session: Option<Session>,
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
view: &dyn BuildView, view: &dyn BuildView,
keep_upper: bool, keep_upper: bool,
@@ -603,6 +633,7 @@ impl EphemeralContextGuard {
let apt_cache_lock = match Self::download_and_extract_chroot( let apt_cache_lock = match Self::download_and_extract_chroot(
series, series,
arch, arch,
mirror,
&chroot_path, &chroot_path,
base_ctx.clone(), base_ctx.clone(),
view, view,
@@ -633,7 +664,7 @@ impl EphemeralContextGuard {
// cache to decide whether the record's staged tree is still // cache to decide whether the record's staged tree is still
// reusable. // reusable.
if let Some(s) = &session if let Some(s) = &session
&& let Ok(tarball) = chroot_tarball_path(series, arch) && let Ok(tarball) = chroot_tarball_path(series, arch, mirror)
&& let Ok(sha) = session::sha256_file(&tarball) && let Ok(sha) = session::sha256_file(&tarball)
{ {
s.set_tarball_sha256(&sha); s.set_tarball_sha256(&sha);
@@ -683,6 +714,7 @@ impl EphemeralContextGuard {
async fn download_and_extract_chroot( async fn download_and_extract_chroot(
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
chroot_path: &PathBuf, chroot_path: &PathBuf,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
view: &dyn BuildView, view: &dyn BuildView,
@@ -700,12 +732,7 @@ impl EphemeralContextGuard {
let cache_dir = proj_dirs.cache_dir(); let cache_dir = proj_dirs.cache_dir();
fs::create_dir_all(cache_dir)?; fs::create_dir_all(cache_dir)?;
// Create tarball filename based on series and architecture let tarball_filename = chroot_tarball_filename(series, arch, mirror);
let tarball_filename = if let Some(a) = arch {
format!("{}-{}-buildd.tar.xz", series, a)
} else {
format!("{}-buildd.tar.xz", series)
};
let tarball_path = cache_dir.join(&tarball_filename); let tarball_path = cache_dir.join(&tarball_filename);
// Check for existing lockfile, and wait for a timeout if it exists // Check for existing lockfile, and wait for a timeout if it exists
@@ -752,7 +779,7 @@ impl EphemeralContextGuard {
arch arch
); );
enter_phase(view, Phase::PreparingChroot); enter_phase(view, Phase::PreparingChroot);
Self::download_chroot_tarball(series, arch, &tarball_path, ctx, view).await?; Self::download_chroot_tarball(series, arch, mirror, &tarball_path, ctx, view).await?;
} else { } else {
log::debug!( log::debug!(
"Using cached chroot tarball for {} (arch: {:?})", "Using cached chroot tarball for {} (arch: {:?})",
@@ -917,6 +944,7 @@ impl EphemeralContextGuard {
async fn download_chroot_tarball( async fn download_chroot_tarball(
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
mirror: Option<&str>,
tarball_path: &Path, tarball_path: &Path,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
view: &dyn BuildView, view: &dyn BuildView,
@@ -953,6 +981,14 @@ impl EphemeralContextGuard {
cmd.arg(series) cmd.arg(series)
.arg(tarball_path.to_string_lossy().to_string()); .arg(tarball_path.to_string_lossy().to_string());
// The mirror positional: without it mmdebstrap writes the distro
// defaults into the tarball's apt sources verbatim; with it, the
// tarball's sources.list carries the mirror, so every apt inside
// the build follows.
if let Some(m) = mirror {
cmd.arg(m);
}
if let Some(s) = view.sink() { if let Some(s) = view.sink() {
cmd.capture(s); cmd.capture(s);
} }
@@ -1287,6 +1323,40 @@ impl Drop for EphemeralContextGuard {
mod chroot_cleanup_tests { mod chroot_cleanup_tests {
use super::*; use super::*;
/// The tarball cache name: per series/arch as before, and a distinct
/// deterministic name per pinned mirror — hashed BEFORE the
/// `-buildd.tar.xz` suffix, which is the pattern prune matches (a
/// pinned tarball pruning like any other).
#[test]
fn tarball_filename_keys_the_mirror_in() {
assert_eq!(
chroot_tarball_filename("noble", None, None),
"noble-buildd.tar.xz"
);
assert_eq!(
chroot_tarball_filename("noble", Some("riscv64"), None),
"noble-riscv64-buildd.tar.xz"
);
let pinned =
chroot_tarball_filename("noble", Some("amd64"), Some("http://localhost:3142/ubuntu"));
assert!(pinned.starts_with("noble-amd64-"));
assert!(pinned.ends_with("-buildd.tar.xz"));
// Deterministic, and distinct per mirror (and from the unpinned).
assert_eq!(
pinned,
chroot_tarball_filename("noble", Some("amd64"), Some("http://localhost:3142/ubuntu"))
);
assert_ne!(
pinned,
chroot_tarball_filename("noble", Some("amd64"), Some("http://localhost:3143/ubuntu"))
);
assert_ne!(
pinned,
chroot_tarball_filename("noble", Some("amd64"), None)
);
}
/// /proc/mounts path fields use octal escapes for whitespace and /// /proc/mounts path fields use octal escapes for whitespace and
/// backslashes; anything else must be kept verbatim. /// backslashes; anything else must be kept verbatim.
#[test] #[test]
+9 -4
View File
@@ -94,6 +94,7 @@ pub async fn build(
arch: &str, arch: &str,
series: &str, series: &str,
pocket: Option<&str>, pocket: Option<&str>,
mirror: Option<&str>,
build_root: &str, build_root: &str,
package_dir: &Path, package_dir: &Path,
cross: bool, cross: bool,
@@ -146,7 +147,7 @@ pub async fn build(
if cross { if cross {
log::debug!("Setting up environment for local cross build..."); log::debug!("Setting up environment for local cross build...");
cross::setup_environment(&mut env, arch, ctx.clone())?; cross::setup_environment(&mut env, arch, ctx.clone())?;
cross::ensure_repositories(arch, series, pocket, ctx.clone())?; cross::ensure_repositories(arch, series, pocket, mirror, ctx.clone())?;
} }
let mut sources = apt::sources::load(Some(ctx.clone()))?; let mut sources = apt::sources::load(Some(ctx.clone()))?;
@@ -202,11 +203,13 @@ pub async fn build(
// `uri.contains("ubuntu")` gate also caught third-party repositories // `uri.contains("ubuntu")` gate also caught third-party repositories
// whose URL merely mentions Ubuntu; the mirror-data check leaves them // whose URL merely mentions Ubuntu; the mirror-data check leaves them
// alone. 'universe' is only added when Ubuntu's component list still // alone. 'universe' is only added when Ubuntu's component list still
// carries it. // carries it. A mirror-pinned source (the tarball was bootstrapped
// from the mirror) gets the same treatment: it IS the archive here.
let ubuntu_components = crate::distro_info::get_dist_components("ubuntu")?; let ubuntu_components = crate::distro_info::get_dist_components("ubuntu")?;
if ubuntu_components.iter().any(|c| c == "universe") { if ubuntu_components.iter().any(|c| c == "universe") {
for source in &mut sources { for source in &mut sources {
if crate::distro_info::is_official_source("ubuntu", &source.uri) if (crate::distro_info::is_official_source("ubuntu", &source.uri)
|| mirror.is_some_and(|m| source.uri == m))
&& !source.components.contains(&"universe".to_string()) && !source.components.contains(&"universe".to_string())
{ {
source.components.push("universe".to_string()); source.components.push("universe".to_string());
@@ -221,7 +224,9 @@ pub async fn build(
let pocket_suite = format!("{series}-{pocket_name}"); let pocket_suite = format!("{series}-{pocket_name}");
log::info!("Enabling pocket '{}' for build dependencies", pocket_suite); log::info!("Enabling pocket '{}' for build dependencies", pocket_suite);
for source in &mut sources { for source in &mut sources {
if crate::deb::is_archive_source(&source.uri) && !source.suite.contains(&pocket_suite) { if crate::deb::is_archive_source_pinned(&source.uri, mirror)
&& !source.suite.contains(&pocket_suite)
{
source.suite.push(pocket_suite.clone()); source.suite.push(pocket_suite.clone());
modified = true; modified = true;
} }
+55 -1
View File
@@ -101,6 +101,15 @@ pub struct DebBuildOptions<'a> {
pub series: Option<String>, pub series: Option<String>,
/// Distribution pocket to resolve build-dependencies from. /// Distribution pocket to resolve build-dependencies from.
pub pocket: Option<String>, pub pocket: Option<String>,
/// Base URL the build environment resolves from (`pkh deb --mirror`):
/// passed to mmdebstrap as the mirror positional, so the chroot
/// tarball's apt sources carry it and every apt inside the build
/// follows. A pinned runner (a caching proxy, a chosen mirror) uses
/// it to resolve every package from one URL; the mirror must serve
/// every suite and architecture the build resolves — the security
/// pocket and, for a cross build, the target architecture's archive
/// included. PPAs and other third-party sources are left alone.
pub mirror: Option<String>,
/// Source tree to build; defaults to the process working directory. /// Source tree to build; defaults to the process working directory.
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,
/// Cross-compile for the target architecture instead of using /// Cross-compile for the target architecture instead of using
@@ -211,6 +220,7 @@ impl Default for DebBuildOptions<'_> {
arch: None, arch: None,
series: None, series: None,
pocket: None, pocket: None,
mirror: None,
cwd: None, cwd: None,
cross: false, cross: false,
mode: None, mode: None,
@@ -255,6 +265,7 @@ async fn build_binary_package_impl(
ref arch, ref arch,
ref series, ref series,
ref pocket, ref pocket,
ref mirror,
ref cwd, ref cwd,
cross, cross,
ref mode, ref mode,
@@ -268,6 +279,17 @@ async fn build_binary_package_impl(
} = opts; } = opts;
let cwd = cwd.as_deref().unwrap_or_else(|| Path::new(".")); let cwd = cwd.as_deref().unwrap_or_else(|| Path::new("."));
// A mirror without a scheme cannot be an apt source URI: fail before
// any chroot work, where the failure would surface as an opaque
// apt-get update error.
if let Some(mirror) = mirror
&& !mirror.contains("://")
{
return Err(
format!("Invalid mirror '{mirror}': expected a base URL (scheme://host/path)").into(),
);
}
// Parse changelog to get package name, version and series // Parse changelog to get package name, version and series
let changelog_path = cwd.join("debian/changelog"); let changelog_path = cwd.join("debian/changelog");
let (package, version, package_series) = let (package, version, package_series) =
@@ -347,6 +369,7 @@ async fn build_binary_package_impl(
&version, &version,
series, series,
chroot_arch, chroot_arch,
mirror.as_deref(),
) { ) {
Ok((record, keep)) => { Ok((record, keep)) => {
keep_upper = keep; keep_upper = keep;
@@ -395,12 +418,14 @@ async fn build_binary_package_impl(
// The shared ccache of this target — skipped with --no-ccache. // The shared ccache of this target — skipped with --no-ccache.
let ccache_dir = (session_enabled && ccache).then(|| session::ccache_dir(&identity)); let ccache_dir = (session_enabled && ccache).then(|| session::ccache_dir(&identity));
let mirror_arg = mirror.as_deref();
let mut guard = if let Some(s) = session.clone() { let mut guard = if let Some(s) = session.clone() {
Some( Some(
ephemeral::EphemeralContextGuard::new_for_session( ephemeral::EphemeralContextGuard::new_for_session(
s, s,
series, series,
chroot_arch, chroot_arch,
mirror_arg,
base_ctx.clone(), base_ctx.clone(),
view, view,
keep_upper, keep_upper,
@@ -413,6 +438,7 @@ async fn build_binary_package_impl(
ephemeral::EphemeralContextGuard::new_with_context( ephemeral::EphemeralContextGuard::new_with_context(
series, series,
chroot_arch, chroot_arch,
mirror_arg,
base_ctx.clone(), base_ctx.clone(),
view, view,
) )
@@ -533,6 +559,7 @@ async fn build_binary_package_impl(
arch, arch,
series, series,
pocket.as_deref(), pocket.as_deref(),
mirror.as_deref(),
&build_root, &build_root,
&package_dir, &package_dir,
cross, cross,
@@ -604,10 +631,11 @@ fn resolve_record(
version: &str, version: &str,
series: &str, series: &str,
chroot_arch: Option<&str>, chroot_arch: Option<&str>,
mirror: Option<&str>,
) -> Result<(session::Session, bool), Box<dyn Error>> { ) -> Result<(session::Session, bool), Box<dyn Error>> {
resolve_record_in( resolve_record_in(
&session::sessions_root(), &session::sessions_root(),
&session::cache_tarball_sha256(series, chroot_arch), &session::cache_tarball_sha256(series, chroot_arch, mirror),
incremental, incremental,
host_tree, host_tree,
identity, identity,
@@ -900,6 +928,14 @@ pub(crate) fn is_archive_source(uri: &str) -> bool {
.any(|dist| crate::distro_info::is_official_source(dist, uri)) .any(|dist| crate::distro_info::is_official_source(dist, uri))
} }
/// Whether `uri` counts as an archive source under a mirror pin: a
/// distribution archive source, or already the pinned mirror URL itself
/// — sources rewritten to it no longer carry an official host, but they
/// are exactly the sources a previous pass or build pinned.
pub(crate) fn is_archive_source_pinned(uri: &str, mirror: Option<&str>) -> bool {
is_archive_source(uri) || mirror.is_some_and(|m| uri == m)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -930,6 +966,24 @@ mod tests {
} }
} }
/// Under a mirror pin, the pinned URL itself counts as an archive
/// source (a previous pass or build rewrote official sources to it);
/// without a pin the check is the plain archive check.
#[test]
fn archive_sources_under_a_mirror_pin() {
let mirror = "http://localhost:3142/ubuntu";
assert!(is_archive_source_pinned(mirror, Some(mirror)));
assert!(is_archive_source_pinned(
"http://archive.ubuntu.com/ubuntu",
Some(mirror)
));
assert!(!is_archive_source_pinned(
"https://ppa.launchpadcontent.net/user/ppa/ubuntu",
Some(mirror)
));
assert!(!is_archive_source_pinned(mirror, None));
}
/// An unshare context mapped over `chroot_root`, parented on a local /// An unshare context mapped over `chroot_root`, parented on a local
/// context like the ephemeral build contexts are: exists/list_files/ /// context like the ephemeral build contexts are: exists/list_files/
/// is_dir answer through the path mapping, no namespace privileges /// is_dir answer through the path mapping, no namespace privileges
+9 -3
View File
@@ -1171,9 +1171,15 @@ pub(crate) fn lock_is_held(path: &Path) -> io::Result<bool> {
/// The digest of the chroot tarball a build of `series`/`arch` would /// The digest of the chroot tarball a build of `series`/`arch` would
/// extract, empty when the cache does not hold one yet (the bootstrap /// extract, empty when the cache does not hold one yet (the bootstrap
/// downloads it during the run). This is the fork gate's reference /// downloads it during the run). This is the fork gate's reference
/// against the sha recorded in a base's manifest. /// against the sha recorded in a base's manifest. `mirror` keys the
pub(crate) fn cache_tarball_sha256(series: &str, arch: Option<&str>) -> String { /// cache (see [`crate::deb::ephemeral::chroot_tarball_path`]), so records
crate::deb::ephemeral::chroot_tarball_path(series, arch) /// never fork across a mirror change.
pub(crate) fn cache_tarball_sha256(
series: &str,
arch: Option<&str>,
mirror: Option<&str>,
) -> String {
crate::deb::ephemeral::chroot_tarball_path(series, arch, mirror)
.ok() .ok()
.and_then(|path| sha256_file(&path).ok()) .and_then(|path| sha256_file(&path).ok())
.unwrap_or_default() .unwrap_or_default()
+3
View File
@@ -355,6 +355,8 @@ fn main() {
.arg(arg!(-a --arch <arch> "Target architecture").required(false)) .arg(arg!(-a --arch <arch> "Target architecture").required(false))
.arg(arg!(-p --pocket <pocket> "Build against dependencies from a specific distribution pocket (updates, security, proposed)").required(false) .arg(arg!(-p --pocket <pocket> "Build against dependencies from a specific distribution pocket (updates, security, proposed)").required(false)
.long_help("Build against dependencies from a specific distribution pocket (e.g. updates, security, proposed).\nThe '<series>-<pocket>' suite will be enabled on archive sources when resolving build-dependencies.")) .long_help("Build against dependencies from a specific distribution pocket (e.g. updates, security, proposed).\nThe '<series>-<pocket>' suite will be enabled on archive sources when resolving build-dependencies."))
.arg(arg!(--mirror <url> "Resolve every package of the build from a specific mirror (base URL)").required(false)
.long_help("Build the chroot and resolve every package from the given base URL — a caching proxy or a chosen mirror on a pinned runner — instead of the distro default: the mirror is passed to mmdebstrap at bootstrap, so the chroot tarball's apt sources carry it and every apt inside the build follows.\nThe mirror must serve every suite and architecture the build resolves: the security pocket and, for cross builds, the target architecture's archive included. PPAs and other third-party sources are left alone. The tarball cache is keyed per mirror, so pinned and unpinned builds keep separate tarballs."))
.arg(arg!(--ppa <ppa> "Build the package adding a specific PPA for dependencies (can be specified multiple times)") .arg(arg!(--ppa <ppa> "Build the package adding a specific PPA for dependencies (can be specified multiple times)")
.long_help("Build the package adding a specific PPA for dependencies. Can be specified multiple times.").required(false).action(clap::ArgAction::Append)) .long_help("Build the package adding a specific PPA for dependencies. Can be specified multiple times.").required(false).action(clap::ArgAction::Append))
.arg(arg!(--inject <package> "Inject a package into the build environment (can be specified multiple times)") .arg(arg!(--inject <package> "Inject a package into the build environment (can be specified multiple times)")
@@ -1002,6 +1004,7 @@ fn main() {
arch, arch,
series, series,
pocket, pocket,
mirror: sub_matches.get_one::<String>("mirror").cloned(),
cwd: Some(cwd.clone()), cwd: Some(cwd.clone()),
cross, cross,
mode, mode,
+1
View File
@@ -41,6 +41,7 @@ impl Bed {
let guard = EphemeralContextGuard::new_with_context( let guard = EphemeralContextGuard::new_with_context(
series, series,
chroot_arch.as_deref(), chroot_arch.as_deref(),
None,
base_ctx, base_ctx,
view, view,
) )