deb: add resumable build sessions
Every local build records a session under /var/tmp/pkh/sessions: the bootstrapped chroot, the installed build dependencies, the phase journal and the staged tree with a persistent overlay upperdir. Rebuilding after a failure currently redoes the tarball extraction, apt update, build-dep resolution and the whole compile; a kernel-sized package loses half an hour per iteration. Recording is always on, reuse is opt-in (--resume [<id>]): the newest session of the tree, or the one matching an id as shown by 'pkh deb list' (one session per series/arch/cross identity, ids are build-start timestamps). Resume skips the chroot bootstrap (integrity marker + tarball check), re-mounts the previous upperdir so make recompiles only what changed, pops the quilt series first when debian/patches changed, and clears debian/files before re-packaging so artifact collection stays exact. A version bump keeps the environment and discards the build artifacts. Explicit selectors disagreeing with the adopted session are an error, never a silent environment switch. Failures and Ctrl+C keep the session (the interrupt hook unmounts but preserves the tree); a success consumes it unless --keep. Concurrent same-identity builds serialize on a lock file kept OUTSIDE the session root: teardown removes the root while holding the lock, and a lock inside it would be deleted under the holder, letting the next opener lock a fresh inode. The apt phases rerun on resume (idempotent, seconds-cheap) rather than being stamp-gated.
This commit is contained in:
+111
-5
@@ -12,6 +12,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::apt;
|
||||
use crate::deb::cross;
|
||||
use crate::deb::session;
|
||||
use crate::debian::control::ControlInfo;
|
||||
use crate::debian::deps::{Deps, Facts, ParseOpts, PkgRelation};
|
||||
|
||||
@@ -26,6 +27,37 @@ fn cap<'a>(
|
||||
cmd
|
||||
}
|
||||
|
||||
/// The session journal stamps this build works with: the input stamps of
|
||||
/// the patch and build-dependency phases, computed from the host tree and
|
||||
/// the resolved selectors.
|
||||
struct JournalStamps {
|
||||
patches: Option<String>,
|
||||
control: Option<String>,
|
||||
}
|
||||
|
||||
/// Compute the journal stamps for a session build; `None` without one.
|
||||
fn journal_stamps(
|
||||
session: &session::Session,
|
||||
arch: &str,
|
||||
series: &str,
|
||||
pocket: Option<&str>,
|
||||
cross: bool,
|
||||
ppa: &[String],
|
||||
inject_packages: &[String],
|
||||
) -> JournalStamps {
|
||||
let host_tree = PathBuf::from(session.manifest().host_tree);
|
||||
let selectors = format!(
|
||||
"{arch}\n{cross}\n{series}\n{}\n{}\n{}",
|
||||
pocket.unwrap_or(""),
|
||||
ppa.join(","),
|
||||
inject_packages.join(",")
|
||||
);
|
||||
JournalStamps {
|
||||
patches: session::patches_stamp(&host_tree),
|
||||
control: session::control_stamp(&host_tree, &selectors),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn build(
|
||||
package: &str,
|
||||
@@ -41,9 +73,15 @@ pub async fn build(
|
||||
ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
jobs: Option<usize>,
|
||||
session: Option<&session::Session>,
|
||||
resume: bool,
|
||||
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
|
||||
let sink: Option<Arc<dyn LineSink>> = view.sink();
|
||||
|
||||
// Journal stamps for a session build; recording is a no-op without one.
|
||||
let stamps =
|
||||
session.map(|s| journal_stamps(s, arch, series, pocket, cross, ppa, inject_packages));
|
||||
|
||||
// Environment
|
||||
let mut env = HashMap::<String, String>::new();
|
||||
env.insert("LANG".to_string(), "C".to_string());
|
||||
@@ -207,6 +245,9 @@ pub async fn build(
|
||||
or re-run with RUST_LOG=debug for more details."
|
||||
.into());
|
||||
}
|
||||
if let Some(s) = session {
|
||||
s.record_phase("apt_update", None);
|
||||
}
|
||||
|
||||
// Install essential packages
|
||||
log::debug!("Installing essential packages for local build...");
|
||||
@@ -231,6 +272,9 @@ pub async fn build(
|
||||
if !status.success() {
|
||||
return Err("Could not install essential packages for the build".into());
|
||||
}
|
||||
if let Some(s) = session {
|
||||
s.record_phase("essentials", None);
|
||||
}
|
||||
|
||||
// The package directory was resolved by the caller (the staged copy of
|
||||
// the tree the user pointed at, or the name-pattern search fallback)
|
||||
@@ -249,8 +293,23 @@ pub async fn build(
|
||||
Err(e) => log::debug!("cannot read changelog for SOURCE_DATE_EPOCH: {}", e),
|
||||
}
|
||||
|
||||
// Apply quilt patches if the package provides a patch series
|
||||
apply_quilt_patches(package_dir_str, &env, ctx.clone(), view, &sink)?;
|
||||
// Apply quilt patches if the package provides a patch series. On a
|
||||
// resumed session the previous attempt's patches may still be applied:
|
||||
// pop them first when the patch tree changed since.
|
||||
let patch_stamp = stamps.as_ref().and_then(|s| s.patches.clone());
|
||||
let pop_patches = resume && {
|
||||
let previous = session
|
||||
.and_then(|s| s.manifest().phases.get("patches").map(|p| p.stamp.clone()))
|
||||
.unwrap_or(None);
|
||||
let applied = ctx
|
||||
.exists(&Path::new(package_dir_str).join(".pc/applied-patches"))
|
||||
.unwrap_or(false);
|
||||
applied && previous != patch_stamp
|
||||
};
|
||||
apply_quilt_patches(package_dir_str, &env, ctx.clone(), view, &sink, pop_patches)?;
|
||||
if let Some(s) = session {
|
||||
s.record_phase("patches", patch_stamp);
|
||||
}
|
||||
|
||||
// Install injected packages if specified
|
||||
if !inject_packages.is_empty() {
|
||||
@@ -274,6 +333,12 @@ pub async fn build(
|
||||
view,
|
||||
&sink,
|
||||
)?;
|
||||
if let Some(s) = session {
|
||||
s.record_phase(
|
||||
"build_deps",
|
||||
stamps.as_ref().and_then(|st| st.control.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
// Run the build step
|
||||
log::debug!("Building (debian/rules build) package...");
|
||||
@@ -289,6 +354,20 @@ pub async fn build(
|
||||
if !status.success() {
|
||||
return Err("Error while building the package".into());
|
||||
}
|
||||
if let Some(s) = session {
|
||||
s.record_phase("build", None);
|
||||
}
|
||||
|
||||
// On a resumed session the previous attempt's debian/files would make
|
||||
// the artifact collection below surface its outputs: only the new
|
||||
// attempt's registration may remain.
|
||||
if resume {
|
||||
let _ = ctx
|
||||
.command("rm")
|
||||
.arg("-f")
|
||||
.arg(Path::new(package_dir_str).join("debian/files"))
|
||||
.status();
|
||||
}
|
||||
|
||||
// Run the 'binary' step to produce deb
|
||||
enter_phase(view, Phase::ProducingBinaries);
|
||||
@@ -334,6 +413,10 @@ pub async fn build(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(s) = session {
|
||||
s.record_phase("binary", None);
|
||||
}
|
||||
|
||||
Ok(artifacts)
|
||||
}
|
||||
|
||||
@@ -843,13 +926,15 @@ fn generate_upload_metadata(
|
||||
}
|
||||
|
||||
/// Apply quilt patches before building, if the package provides a
|
||||
/// 'debian/patches/series' file
|
||||
/// 'debian/patches/series' file. `pop_first` un-applies the previous
|
||||
/// attempt's patches first (resumed session whose patch tree changed).
|
||||
fn apply_quilt_patches(
|
||||
package_dir: &str,
|
||||
env: &HashMap<String, String>,
|
||||
ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
sink: &Option<Arc<dyn LineSink>>,
|
||||
pop_first: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let series_path = Path::new(package_dir).join("debian/patches/series");
|
||||
if !ctx.exists(&series_path)? {
|
||||
@@ -907,13 +992,33 @@ fn apply_quilt_patches(
|
||||
return Err("Could not install 'quilt', required to apply patches".into());
|
||||
}
|
||||
|
||||
let mut patch_env = env.clone();
|
||||
patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string());
|
||||
|
||||
// Un-apply the previous attempt's patches when the patch tree changed:
|
||||
// best-effort (a failing pop leaves the state quilt reports; the fresh
|
||||
// `push -a` below then fails loudly on the actual problem).
|
||||
if pop_first {
|
||||
log::info!("Patch series changed since the previous attempt, un-applying patches");
|
||||
let status = cap(
|
||||
ctx.command("quilt")
|
||||
.current_dir(package_dir)
|
||||
.envs(patch_env.clone())
|
||||
.arg("pop")
|
||||
.arg("-a"),
|
||||
sink,
|
||||
)
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
warn!("'quilt pop -a' failed; continuing with 'quilt push -a'");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all patches listed in the series
|
||||
view.phase(
|
||||
Phase::ApplyingPatches.label(),
|
||||
Box::new(QuiltClassifier::new(total_patches)),
|
||||
);
|
||||
let mut patch_env = env.clone();
|
||||
patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string());
|
||||
let status = cap(
|
||||
ctx.command("quilt")
|
||||
.current_dir(package_dir)
|
||||
@@ -1395,6 +1500,7 @@ mod tests {
|
||||
ctx,
|
||||
&crate::report::Quiet,
|
||||
&None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user