deb: integrate ccache for content-keyed compile reuse

Fresh chroots also mean fresh compiles: nothing survived to make the
recompile-heavy middle of a run cheap. Every deb build now wires a
shared ccache — /var/tmp/pkh/ccache/<slug>/, bind-mounted at the
chroot's /var/cache/pkh-ccache — through a PATH shim rather than CC
environment variables: gcc/g++/cc/c++ symlinks in /usr/lib/pkh-ccache-
shim resolve to the ccache the essentials install, so rules that call
the plain compiler names are cached while explicit exotic compilers
stay untouched. CCACHE_COMPILERCHECK=content is critical — the default
is mtime-based and would never hit across fresh chroots, where every
toolchain's mtime is the extraction's — with CCACHE_BASEDIR=/tmp/pkh-
build and CCACHE_NOHASHDIR=true keying the staged tree's relative
paths, and CCACHE_MAXSIZE=10G (PKH_CCACHE_MAXSIZE overrides).

No mtime clamping anywhere: the tarball extraction preserves mtimes,
so the toolchain and libc headers stay older than carried objects and
make keeps them, while build-dependency reinstalls are newer and make
rebuilds their dependents — ccache turns those into hits, and a real
archive library change is a miss that rebuilds them, exactly the
signal clamping would silence. `pkh deb --no-ccache` skips the shim,
the env and the mount entirely: an escape hatch for rules that
mishandle compiler wrappers (kbuild-style flavour configs).
This commit is contained in:
2026-09-29 14:39:54 +02:00
parent 0e1e37b2e6
commit 6a8c301705
6 changed files with 287 additions and 8 deletions
+11 -1
View File
@@ -140,6 +140,16 @@ downloaded packages, and an `apt-get autoclean` at the end of each run
drops the debs the archive no longer serves). `pkh prune` removes the drops the debs the archive no longer serves). `pkh prune` removes the
caches untouched for a week. caches untouched for a week.
Fresh chroots do not mean fresh compiles either: every run's chroot
gets a shared ccache (at `/var/tmp/pkh/ccache/<target>/`, mounted at
`/var/cache/pkh-ccache`) wired in through a compiler PATH shim —
`gcc`, `g++`, `cc` and `c++` resolve to ccache — with content-keyed
cache checks (`CCACHE_COMPILERCHECK=content`), so cache hits survive
the fresh toolchain of a new chroot. The cache is capped at 10G per
target (override with `PKH_CCACHE_MAXSIZE`); `pkh deb --no-ccache`
bypasses the shim and cache entirely, an escape hatch for rules that
mishandle compiler wrappers (kbuild-style flavour configs).
`--incremental` forks a previous record — a copy, the base is never `--incremental` forks a previous record — a copy, the base is never
modified — so a failed run can never destroy the last-good state it was modified — so a failed run can never destroy the last-good state it was
built from. The staged tree is carried only when the base's recorded built from. The staged tree is carried only when the base's recorded
@@ -163,7 +173,7 @@ pkh deb list # the records of this tree: id, outcome,
# version, fork lineage, staged-tree state, age # version, fork lineage, staged-tree state, age
pkh deb log [-f] [<id>] # print a record's build log; -f follows a pkh deb log [-f] [<id>] # print a record's build log; -f follows a
# live build # live build
pkh prune # garbage-collect records (7-day retention) pkh deb --no-ccache # build without the shared compile cache
``` ```
Each target keeps at most five staged trees: when a sixth is retained, Each target keeps at most five staged trees: when a sixth is retained,
+125 -7
View File
@@ -289,6 +289,82 @@ pub(crate) fn write_apt_keep_conf(chroot_path: &Path) -> std::io::Result<()> {
) )
} }
/// The in-chroot mount point of the shared ccache directory.
pub(crate) const CCACHE_MOUNT: &str = "/var/cache/pkh-ccache";
/// The in-chroot directory holding the ccache compiler shims
/// (gcc/g++/cc/c++ → `/usr/bin/ccache`), prepended to the build's PATH
/// by [`crate::deb::local`].
pub(crate) const CCACHE_SHIM_DIR: &str = "/usr/lib/pkh-ccache-shim";
/// Bind-mount the shared ccache directory of a build identity over the
/// chroot's [`CCACHE_MOUNT`] and install the compiler shims, so the
/// builds of that target reuse each other's compiled objects (content
/// keyed, see the ccache env in [`crate::deb::local`]). ccache is
/// designed for concurrent access to one cache — no cross-run lock is
/// needed.
///
/// Failures are warnings, never errors: an unshared cache dir makes
/// ccache per-run (still correct, just slower), and a missing shim
/// leaves the build on the real compilers.
fn share_ccache(chroot_path: &Path, cache: &Path, ctx: Arc<context::Context>) {
if let Err(e) = fs::create_dir_all(cache).and_then(|()| install_ccache_shim(chroot_path)) {
log::warn!("Cannot share the ccache {}: {e}", cache.display());
return;
}
let mount_point = chroot_path.join(CCACHE_MOUNT.trim_start_matches('/'));
if let Err(e) = fs::create_dir_all(&mount_point) {
log::warn!(
"Cannot create the ccache mount point {}: {e}",
mount_point.display()
);
return;
}
// Check if we're running as root to avoid unnecessary sudo
let is_root = crate::utils::root::is_root().unwrap_or(false);
let mut cmd = ctx.command(if is_root { "mount" } else { "sudo" });
if !is_root {
cmd.arg("mount");
}
match cmd.arg("--bind").arg(cache).arg(&mount_point).status() {
Ok(status) if status.success() => {
log::debug!(
"Bind-mounted the shared ccache {} over {}",
cache.display(),
mount_point.display()
);
}
outcome => {
log::warn!(
"Cannot bind-mount the shared ccache {} over {}: {:?}; \
this run's cache is not shared",
cache.display(),
mount_point.display(),
outcome
);
}
}
}
/// Install the ccache PATH shim inside the chroot: `gcc`, `g++`, `cc`
/// and `c++` symlinking to the ccache binary the essentials install, in
/// a directory nothing else populates. The absolute target resolves
/// inside the chroot, where the shim dir is prepended to PATH.
pub(crate) fn install_ccache_shim(chroot_path: &Path) -> std::io::Result<()> {
let shim_dir = chroot_path.join(CCACHE_SHIM_DIR.trim_start_matches('/'));
fs::create_dir_all(&shim_dir)?;
for compiler in ["gcc", "g++", "cc", "c++"] {
let shim = shim_dir.join(compiler);
match fs::symlink_metadata(&shim) {
Ok(_) => fs::remove_file(&shim)?, // idempotent re-install
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
std::os::unix::fs::symlink("/usr/bin/ccache", &shim)?;
}
Ok(())
}
/// Unmount `path`, falling back to a lazy unmount if the first attempt fails /// Unmount `path`, falling back to a lazy unmount if the first attempt fails
/// because something still holds the mount busy (e.g. an interrupted child /// because something still holds the mount busy (e.g. an interrupted child
/// that has not exited yet); returns whether the mount is gone /// that has not exited yet); returns whether the mount is gone
@@ -406,7 +482,7 @@ impl EphemeralContextGuard {
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).await Self::new_internal(None, series, arch, base_ctx, view, false, None).await
} }
/// Create the ephemeral build context inside `session`'s chroot. /// Create the ephemeral build context inside `session`'s chroot.
@@ -421,6 +497,9 @@ impl EphemeralContextGuard {
/// * `keep_upper` - Retain the record's staged tree after the build /// * `keep_upper` - Retain the record's staged tree after the build
/// (`pkh deb --incremental`); a plain run's record keeps metadata /// (`pkh deb --incremental`); a plain run's record keeps metadata
/// and log only /// and log only
/// * `ccache_dir` - The shared ccache directory to mount at the
/// chroot's `/var/cache/pkh-ccache` (`pkh deb` computes it from the
/// build identity; `None` leaves the chroot unshared)
pub async fn new_for_session( pub async fn new_for_session(
session: Session, session: Session,
series: &str, series: &str,
@@ -428,8 +507,18 @@ impl EphemeralContextGuard {
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
view: &dyn BuildView, view: &dyn BuildView,
keep_upper: bool, keep_upper: bool,
ccache_dir: Option<&Path>,
) -> Result<Self, Box<dyn Error>> { ) -> Result<Self, Box<dyn Error>> {
Self::new_internal(Some(session), series, arch, base_ctx, view, keep_upper).await Self::new_internal(
Some(session),
series,
arch,
base_ctx,
view,
keep_upper,
ccache_dir,
)
.await
} }
async fn new_internal( async fn new_internal(
@@ -439,6 +528,7 @@ impl EphemeralContextGuard {
base_ctx: Arc<Context>, base_ctx: Arc<Context>,
view: &dyn BuildView, view: &dyn BuildView,
keep_upper: bool, keep_upper: bool,
ccache_dir: Option<&Path>,
) -> Result<Self, Box<dyn Error>> { ) -> Result<Self, Box<dyn Error>> {
// Save the globally-installed context so Drop can restore exactly // Save the globally-installed context so Drop can restore exactly
// this handle: concurrent builds install their own ephemeral // this handle: concurrent builds install their own ephemeral
@@ -505,6 +595,7 @@ impl EphemeralContextGuard {
&chroot_path, &chroot_path,
base_ctx.clone(), base_ctx.clone(),
view, view,
ccache_dir,
) )
.await .await
{ {
@@ -584,6 +675,7 @@ impl EphemeralContextGuard {
chroot_path: &PathBuf, chroot_path: &PathBuf,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
view: &dyn BuildView, view: &dyn BuildView,
ccache_dir: Option<&Path>,
) -> Result<Option<fs::File>, Box<dyn Error>> { ) -> Result<Option<fs::File>, Box<dyn Error>> {
// The chroot is per-run scratch: nothing is reused, so never // The chroot is per-run scratch: nothing is reused, so never
// extract over stale content. // extract over stale content.
@@ -665,23 +757,25 @@ impl EphemeralContextGuard {
// Create device nodes, bind-mount /proc and share the apt cache // Create device nodes, bind-mount /proc and share the apt cache
let apt_cache_lock = let apt_cache_lock =
Self::finalize_chroot(series, arch, chroot_path, ctx_for_devices, view)?; Self::finalize_chroot(series, arch, chroot_path, ctx_for_devices, view, ccache_dir)?;
// The bootstrap's tarball digest is recorded by the caller (the // The bootstrap's tarball digest is recorded by the caller (the
// record's fork gate compares it against the cache later). // record's fork gate compares it against the cache later).
Ok(apt_cache_lock) Ok(apt_cache_lock)
} }
/// Device nodes, the /proc bind mount and the shared apt download /// Device nodes, the /proc bind mount, the shared apt download cache
/// cache for a fresh chroot: idempotent finalization, safe to run on /// and the shared ccache for a fresh chroot: idempotent
/// every run. Returns the shared cache's lock, held while the mount /// finalization, safe to run on every run. Returns the shared apt
/// is up (see [`EphemeralContextGuard::share_apt_cache`]). /// cache's lock, held while its mount is up (see
/// [`EphemeralContextGuard::share_apt_cache`]).
fn finalize_chroot( fn finalize_chroot(
series: &str, series: &str,
arch: Option<&str>, arch: Option<&str>,
chroot_path: &Path, chroot_path: &Path,
ctx: Arc<context::Context>, ctx: Arc<context::Context>,
view: &dyn BuildView, view: &dyn BuildView,
ccache_dir: Option<&Path>,
) -> Result<Option<fs::File>, Box<dyn Error>> { ) -> Result<Option<fs::File>, Box<dyn Error>> {
if !chroot_path.join("dev/null").exists() { if !chroot_path.join("dev/null").exists() {
// Create device nodes in the chroot // Create device nodes in the chroot
@@ -701,6 +795,9 @@ impl EphemeralContextGuard {
Self::bind_mount_proc(chroot_path, ctx.clone())?; Self::bind_mount_proc(chroot_path, ctx.clone())?;
} }
if let Some(dir) = ccache_dir {
share_ccache(chroot_path, dir, ctx.clone());
}
Ok(Self::share_apt_cache(series, arch, chroot_path, ctx)) Ok(Self::share_apt_cache(series, arch, chroot_path, ctx))
} }
@@ -1352,4 +1449,25 @@ mod chroot_cleanup_tests {
write_apt_keep_conf(&chroot).unwrap(); write_apt_keep_conf(&chroot).unwrap();
assert_eq!(fs::read_to_string(&conf).unwrap(), content); assert_eq!(fs::read_to_string(&conf).unwrap(), content);
} }
/// The ccache shim installs the four compiler symlinks pointing at
/// the ccache binary, and reinstalls cleanly over an existing one.
#[test]
fn ccache_shim_installs_the_four_compiler_links() {
let dir = tempfile::tempdir().unwrap();
let chroot = dir.path().join("chroot");
install_ccache_shim(&chroot).unwrap();
let shim_dir = chroot.join(CCACHE_SHIM_DIR.trim_start_matches('/'));
for compiler in ["gcc", "g++", "cc", "c++"] {
let target = fs::read_link(shim_dir.join(compiler))
.unwrap_or_else(|_| panic!("the {compiler} shim must be a symlink"));
assert_eq!(target, Path::new("/usr/bin/ccache"));
}
// A second install refreshes the links instead of failing.
install_ccache_shim(&chroot).unwrap();
assert!(fs::symlink_metadata(shim_dir.join("gcc")).is_ok());
}
} }
+115
View File
@@ -35,6 +35,58 @@ fn patch_stamp(session: &session::Session) -> Option<String> {
session::patches_stamp(&host_tree) session::patches_stamp(&host_tree)
} }
/// The ccache size cap of a build: `$PKH_CCACHE_MAXSIZE`, default 10G
/// (the unit syntax of ccache's own configuration: `10G`, `512M`, ...).
fn ccache_maxsize() -> String {
std::env::var("PKH_CCACHE_MAXSIZE").unwrap_or_else(|_| "10G".to_string())
}
/// Wire the build environment to the shared ccache (on by default,
/// `pkh deb --no-ccache` skips it entirely): the compilers resolve to
/// ccache through a PATH shim, the cache is the identity's shared
/// directory mounted in the chroot, and the cache checks are keyed on
/// content.
///
/// Mechanics: the tarball extraction preserves mtimes, so the toolchain
/// and libc headers (inside the buildd tarball) stay older than the
/// carried objects and make keeps them; the build dependencies
/// reinstall fresh each run, so their headers are newer and make
/// rebuilds their dependents — ccache turns those rebuilds into hits,
/// and a real archive library change is a cache miss that rebuilds
/// them (the correct, if slow, path). No mtime clamping anywhere:
/// clamping would silence exactly that signal.
///
/// `CCACHE_COMPILERCHECK=content` is critical: the default is
/// mtime-based and would never hit across fresh chroots, where every
/// toolchain's mtime is the extraction's.
pub(crate) fn apply_ccache_env(env: &mut HashMap<String, String>, enabled: bool, maxsize: &str) {
if !enabled {
return;
}
// The shim shadows the real compilers with ccache invocations;
// the rest of PATH keeps resolving the other build tools.
let path = env
.get("PATH")
.cloned()
.or_else(|| std::env::var("PATH").ok())
.unwrap_or_default();
env.insert(
"PATH".to_string(),
format!("{}:{path}", crate::deb::ephemeral::CCACHE_SHIM_DIR),
);
env.insert(
"CCACHE_DIR".to_string(),
crate::deb::ephemeral::CCACHE_MOUNT.to_string(),
);
env.insert("CCACHE_COMPILERCHECK".to_string(), "content".to_string());
env.insert("CCACHE_MAXSIZE".to_string(), maxsize.to_string());
env.insert(
"CCACHE_BASEDIR".to_string(),
crate::deb::session::BUILD_ROOT.to_string(),
);
env.insert("CCACHE_NOHASHDIR".to_string(), "true".to_string());
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn build( pub async fn build(
package: &str, package: &str,
@@ -52,6 +104,7 @@ pub async fn build(
jobs: Option<usize>, jobs: Option<usize>,
session: Option<&session::Session>, session: Option<&session::Session>,
build_resume: bool, build_resume: bool,
ccache: bool,
) -> Result<Vec<PathBuf>, Box<dyn Error>> { ) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let sink: Option<Arc<dyn LineSink>> = view.sink(); let sink: Option<Arc<dyn LineSink>> = view.sink();
@@ -62,6 +115,7 @@ pub async fn build(
let mut env = HashMap::<String, String>::new(); let mut env = HashMap::<String, String>::new();
env.insert("LANG".to_string(), "C".to_string()); env.insert("LANG".to_string(), "C".to_string());
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
apply_ccache_env(&mut env, ccache, &ccache_maxsize());
// Parallel building: honor an explicit -j/--jobs count, otherwise detect // Parallel building: honor an explicit -j/--jobs count, otherwise detect
// the number of cores available inside the build context (nproc). // the number of cores available inside the build context (nproc).
@@ -232,6 +286,11 @@ pub async fn build(
.arg("build-essential") .arg("build-essential")
.arg("dose-builddebcheck") .arg("dose-builddebcheck")
.arg("fakeroot"); .arg("fakeroot");
// The compiler cache comes free from the shared download cache; the
// build env wires the compilers to it (see apply_ccache_env).
if ccache {
cmd.arg("ccache");
}
if cross { if cross {
cmd.arg(format!("crossbuild-essential-{arch}")); cmd.arg(format!("crossbuild-essential-{arch}"));
cmd.arg(format!("libc6-{arch}-cross")); cmd.arg(format!("libc6-{arch}-cross"));
@@ -1270,6 +1329,62 @@ mod tests {
crate::debian::deps::parse_simple(s, true).unwrap() crate::debian::deps::parse_simple(s, true).unwrap()
} }
/// The build env carries the ccache wiring when it is enabled: the
/// compiler shim first on PATH, the shared cache dir, content-keyed
/// compiler checks and the size cap.
#[test]
fn ccache_env_is_applied_when_enabled() {
let mut env = HashMap::new();
env.insert("PATH".to_string(), "/usr/bin:/bin".to_string());
apply_ccache_env(&mut env, true, "10G");
assert_eq!(
env.get("PATH").unwrap(),
&format!("{}:/usr/bin:/bin", crate::deb::ephemeral::CCACHE_SHIM_DIR),
"the shim must shadow the real compilers"
);
assert_eq!(
env.get("CCACHE_DIR").unwrap(),
crate::deb::ephemeral::CCACHE_MOUNT
);
assert_eq!(
env.get("CCACHE_COMPILERCHECK").unwrap(),
"content",
"mtime checks would never hit across fresh chroots"
);
assert_eq!(env.get("CCACHE_MAXSIZE").unwrap(), "10G");
assert_eq!(
env.get("CCACHE_BASEDIR").unwrap(),
crate::deb::session::BUILD_ROOT
);
assert_eq!(env.get("CCACHE_NOHASHDIR").unwrap(), "true");
}
/// The size cap is the caller's choice: PKH_CCACHE_MAXSIZE flows
/// through as-is.
#[test]
fn ccache_env_honors_the_size_cap() {
let mut env = HashMap::new();
apply_ccache_env(&mut env, true, "512M");
assert_eq!(env.get("CCACHE_MAXSIZE").unwrap(), "512M");
}
/// With `--no-ccache` the build env is left untouched: no shim, no
/// cache variables.
#[test]
fn ccache_env_is_skipped_when_disabled() {
let mut env = HashMap::new();
env.insert("PATH".to_string(), "/usr/bin:/bin".to_string());
apply_ccache_env(&mut env, false, &ccache_maxsize());
assert_eq!(env.get("PATH").unwrap(), "/usr/bin:/bin");
assert!(!env.contains_key("CCACHE_DIR"));
assert!(!env.contains_key("CCACHE_COMPILERCHECK"));
assert!(!env.contains_key("CCACHE_MAXSIZE"));
}
/// Unqualified dependencies install the dpkg-satisfying variant, /// Unqualified dependencies install the dpkg-satisfying variant,
/// preferring the runnable build-architecture one: Multi-Arch: /// preferring the runnable build-architecture one: Multi-Arch:
/// foreign candidates from the build architecture, then the /// foreign candidates from the build architecture, then the
+10
View File
@@ -123,6 +123,10 @@ pub struct DebBuildOptions<'a> {
/// whether this build's own staged tree is retained. Defaults to a /// whether this build's own staged tree is retained. Defaults to a
/// plain run. /// plain run.
pub incremental: Incremental, pub incremental: Incremental,
/// Reuse the shared ccache of this target (on by default; `pkh deb
/// --no-ccache` disables it — an escape hatch for `debian/rules`
/// whose own compiler handling conflicts with the shim).
pub ccache: bool,
/// Where build events (phases, progress, outcome) are reported. /// Where build events (phases, progress, outcome) are reported.
pub view: &'a dyn BuildView, pub view: &'a dyn BuildView,
} }
@@ -215,6 +219,7 @@ impl Default for DebBuildOptions<'_> {
jobs: None, jobs: None,
ctx: None, ctx: None,
incremental: Incremental::Off, incremental: Incremental::Off,
ccache: true,
view: &QUIET, view: &QUIET,
} }
} }
@@ -258,6 +263,7 @@ async fn build_binary_package_impl(
ref jobs, ref jobs,
ref ctx, ref ctx,
ref incremental, ref incremental,
ccache,
view, view,
} = opts; } = opts;
let cwd = cwd.as_deref().unwrap_or_else(|| Path::new(".")); let cwd = cwd.as_deref().unwrap_or_else(|| Path::new("."));
@@ -375,6 +381,8 @@ async fn build_binary_package_impl(
None => opts.view, None => opts.view,
}; };
// The shared ccache of this target — skipped with --no-ccache.
let ccache_dir = (session_enabled && ccache).then(|| session::ccache_dir(&identity));
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(
@@ -384,6 +392,7 @@ async fn build_binary_package_impl(
base_ctx.clone(), base_ctx.clone(),
view, view,
keep_upper, keep_upper,
ccache_dir.as_deref(),
) )
.await?, .await?,
) )
@@ -522,6 +531,7 @@ async fn build_binary_package_impl(
*jobs, *jobs,
session.as_ref(), session.as_ref(),
build_resume, build_resume,
ccache,
) )
.await? .await?
} }
+23
View File
@@ -707,6 +707,18 @@ pub(crate) fn identity_dir(root: &Path, identity: &SessionIdentity) -> PathBuf {
root.join(slug_for(identity)) root.join(slug_for(identity))
} }
/// Root of the shared ccache directories (`/var/tmp/pkh/ccache`), one
/// per identity slug.
pub(crate) const CCACHE_ROOT: &str = "/var/tmp/pkh/ccache";
/// The ccache directory of an identity: `/var/tmp/pkh/ccache/<slug>/`,
/// bind-mounted at each chroot's `/var/cache/pkh-ccache` so compiled
/// objects are shared across runs (content-keyed, see the ccache setup
/// in `crate::deb::local`).
pub(crate) fn ccache_dir(identity: &SessionIdentity) -> PathBuf {
PathBuf::from(CCACHE_ROOT).join(slug_for(identity))
}
/// A record id not taken yet under `identity_dir`: the plain timestamp, /// A record id not taken yet under `identity_dir`: the plain timestamp,
/// suffixed `-02`, `-03`, ... when earlier builds took the same second /// suffixed `-02`, `-03`, ... when earlier builds took the same second
/// (zero-padded so the ids keep sorting in creation order). /// (zero-padded so the ids keep sorting in creation order).
@@ -2187,6 +2199,17 @@ mod tests {
assert_ne!(ids[0], ids[1]); assert_ne!(ids[0], ids[1]);
} }
/// The ccache directory of an identity lives under the shared ccache
/// root, keyed by the identity slug like the records.
#[test]
fn ccache_dir_is_keyed_by_the_identity_slug() {
let identity = test_identity();
assert_eq!(
ccache_dir(&identity),
PathBuf::from("/var/tmp/pkh/ccache").join(slug_for(&identity))
);
}
/// The fork gate: the base's staged tree is carried only while its /// The fork gate: the base's staged tree is carried only while its
/// recorded tarball sha still matches the cached tarball AND the /// recorded tarball sha still matches the cached tarball AND the
/// changelog version is unchanged. Everything else forks cold. /// changelog version is unchanged. Everything else forks cold.
+3
View File
@@ -374,6 +374,8 @@ fn main() {
.help("Incremental build: fork a previous build record of this target and keep its staged tree") .help("Incremental build: fork a previous build record of this target and keep its staged tree")
.long_help("Incremental build: fork a previous build record of this target — at an unchanged changelog version and while the chroot tarball is unchanged, the base's staged tree is copied into a new record, so make recompiles only what changed.\nWithout a value, the newest successful record of the target (among the last five carrying a staged tree) is the base; with an ID (as shown by 'pkh deb list'), that exact record — a failed one is fine if its staged tree is.\nEvery --incremental run keeps its staged tree, success or failure, so the next one can fork it; a plain 'pkh deb' keeps only metadata and its build log (the chroot is per-run and always discarded)."), .long_help("Incremental build: fork a previous build record of this target — at an unchanged changelog version and while the chroot tarball is unchanged, the base's staged tree is copied into a new record, so make recompiles only what changed.\nWithout a value, the newest successful record of the target (among the last five carrying a staged tree) is the base; with an ID (as shown by 'pkh deb list'), that exact record — a failed one is fine if its staged tree is.\nEvery --incremental run keeps its staged tree, success or failure, so the next one can fork it; a plain 'pkh deb' keeps only metadata and its build log (the chroot is per-run and always discarded)."),
) )
.arg(arg!(--"no-ccache" "Build without the shared ccache (escape hatch for kbuild-style rules that mishandle compiler shims)")
.long_help("Build without the shared ccache (its compiler PATH shim is skipped and no cache is mounted). Escape hatch for packages whose debian/rules mishandles a compiler shim, e.g. some kernel flavour configs."))
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false) .arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)
.long_help("Show raw tool output instead of the live build view.\nAlso implied by RUST_LOG=debug for pkh's own logs.")) .long_help("Show raw tool output instead of the live build view.\nAlso implied by RUST_LOG=debug for pkh's own logs."))
.subcommand( .subcommand(
@@ -1007,6 +1009,7 @@ fn main() {
inject, inject,
jobs, jobs,
incremental, incremental,
ccache: !sub_matches.get_flag("no-ccache"),
view, view,
..Default::default() ..Default::default()
}) })