dpkg-genchanges/genbuildinfo handle a binary-only upload by referencing the previous source version textually (Source: pkg (prev), Binary-Only: yes, Binary-Only-Changes) while distributing no source files at all: pkh instead pulled the previous .dsc and its tarballs into both documents whenever they sat next to the artifacts, re-uploading the whole source on every binNMU. Drop that redistribution (and include_dsc_artifacts with it), and emit the missing Binary-Only: yes field, which the new differential test against real dpkg-buildpackage -b caught. The binNMU case shares its runner with the regular binary metadata differential; a unit test pins the exclusion even with the previous artifacts present.
656 lines
26 KiB
Rust
656 lines
26 KiB
Rust
//! Binary-build metadata generation: native `.buildinfo` / `.changes`
|
|
//! production for binary-only builds (`pkh deb`), the equivalent of
|
|
//! `dpkg-genbuildinfo -b` + `dpkg-genchanges -b`.
|
|
//!
|
|
//! All tree/database access goes through a [`Context`] so the generation can
|
|
//! run against a build tree living in a local directory, an ephemeral
|
|
//! chroot or a remote host. Artifact digests are computed inside the context
|
|
//! with coreutils (`md5sum`, `sha1sum`, `sha256sum`, `stat`), keeping the
|
|
//! flow binary-safe regardless of the transport.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::error::Error;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use crate::context::Context;
|
|
use crate::debian::{ChecksumEntry, ControlInfo, FileChecksums, FilesList};
|
|
|
|
/// Digests of one artifact.
|
|
#[derive(Debug, Clone, Default)]
|
|
struct ArtifactHashes {
|
|
size: u64,
|
|
md5: String,
|
|
sha1: String,
|
|
sha256: String,
|
|
}
|
|
|
|
/// Options driving binary metadata generation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BinaryMetadataOptions {
|
|
/// Active build profiles (`Built-For-Profiles`).
|
|
pub profiles: Vec<String>,
|
|
/// Vendor name (`Build-Origin`).
|
|
pub vendor: String,
|
|
/// Environment variables pkh exported to the build steps (e.g. `LANG`,
|
|
/// `DEB_BUILD_OPTIONS` with the real parallel count and `nocheck`,
|
|
/// `SOURCE_DATE_EPOCH`, cross `DEB_*` variables). Recorded — filtered to
|
|
/// dpkg's allow-list — in the `.buildinfo` `Environment` field, taking
|
|
/// precedence over whatever the host process inherited, so the metadata
|
|
/// describes the environment the build actually ran in.
|
|
pub exported_env: BTreeMap<String, String>,
|
|
/// Build architecture (the machine inside the build context).
|
|
pub build_arch: String,
|
|
/// Host architecture (the packages' target); equals the build
|
|
/// architecture except for cross builds.
|
|
pub host_arch: String,
|
|
}
|
|
|
|
/// Generate `<pkg>_<ver>_<arch>.buildinfo` and `.changes` for a finished
|
|
/// binary build, consuming `debian/files` from `package_dir` and the
|
|
/// artifacts sitting in `upload_dir`. Returns both paths (inside the
|
|
/// context).
|
|
///
|
|
/// Mirrors the observable behavior of `dpkg-genbuildinfo -b` and
|
|
/// `dpkg-genchanges -b`: sorted `Binary` list, encounter-order `Architecture`
|
|
/// accumulation, sorted `Description` lines formatted like dpkg, `.buildinfo`
|
|
/// registration in `debian/files`, and binary-NMU handling (`Source:
|
|
/// pkg (prev)` + `Binary-Only-Changes`, with no source files distributed).
|
|
pub fn generate_binary_metadata(
|
|
ctx: &Arc<Context>,
|
|
package_dir: &Path,
|
|
upload_dir: &Path,
|
|
opts: &BinaryMetadataOptions,
|
|
) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
|
|
// ------------------------------------------------------------------
|
|
// Metadata sources inside the context
|
|
// ------------------------------------------------------------------
|
|
let changelog_content = ctx.read_file(&package_dir.join("debian/changelog"))?;
|
|
let mut entries =
|
|
crate::debian::changelog::parse_changelog_entries_from_str(&changelog_content, Some(2))?;
|
|
let entry = entries.remove(0);
|
|
let previous_entry = entries.into_iter().next();
|
|
|
|
let control_content = ctx.read_file(&package_dir.join("debian/control"))?;
|
|
let control = ControlInfo::parse_content(&control_content)?;
|
|
|
|
// A missing `debian/files` is tolerated (first binary build in a fresh
|
|
// tree has nothing registered yet; that surfaces below as the "no binary
|
|
// artifacts" error), like `FilesList::load`. Any other read failure must
|
|
// not be silently mistaken for an empty registry.
|
|
let files_path = package_dir.join("debian/files");
|
|
let files_content = if ctx.exists(&files_path)? {
|
|
ctx.read_file(&files_path)
|
|
.map_err(|e| format!("cannot read '{}': {}", files_path.display(), e))?
|
|
} else {
|
|
String::new()
|
|
};
|
|
let mut files_list = FilesList::parse(&files_content)?;
|
|
|
|
// ------------------------------------------------------------------
|
|
// Collect binary artifacts registered in debian/files
|
|
// ------------------------------------------------------------------
|
|
let artifact_names: Vec<String> = files_list
|
|
.iter()
|
|
.filter(|e| matches!(e.package_type.as_deref(), Some("deb") | Some("udeb")))
|
|
.map(|e| e.filename.clone())
|
|
.collect();
|
|
|
|
if artifact_names.is_empty() {
|
|
return Err("binary build with no binary artifacts found; cannot distribute".into());
|
|
}
|
|
|
|
let mut hashes = hashes_in_context(ctx, upload_dir, &artifact_names)?;
|
|
|
|
let mut checksums = FileChecksums::new();
|
|
let mut arch_values: Vec<String> = Vec::new();
|
|
let mut arch_seen = std::collections::HashSet::new();
|
|
for name in &artifact_names {
|
|
let entry_hashes = hashes
|
|
.remove(name)
|
|
.ok_or_else(|| format!("artifact '{name}' listed in debian/files but not found"))?;
|
|
// SHA-512 stays unknown here: like dpkg-genbuildinfo, no SHA-512
|
|
// digest is computed for the artifacts, and an empty digest keeps
|
|
// the `Checksums-Sha512` field of the `.buildinfo` omitted.
|
|
checksums.insert_entry(
|
|
name,
|
|
ChecksumEntry {
|
|
size: entry_hashes.size,
|
|
md5: entry_hashes.md5,
|
|
sha1: entry_hashes.sha1,
|
|
sha256: entry_hashes.sha256,
|
|
sha512: String::new(),
|
|
},
|
|
);
|
|
// Architecture accumulation in encounter order (dpkg-genchanges).
|
|
if let Some(file_entry) = files_list.get(name)
|
|
&& let Some(arch) = file_entry
|
|
.arch
|
|
.as_ref()
|
|
.or_else(|| file_entry.attrs.get("architecture"))
|
|
&& arch_seen.insert(arch.clone())
|
|
{
|
|
arch_values.push(arch.clone());
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Binary-NMU: reference the previous source version, textually only
|
|
// ------------------------------------------------------------------
|
|
let sversion = entry.version.no_epoch();
|
|
let mut source_display = entry.source.clone();
|
|
let mut binary_only_changes = None;
|
|
|
|
if entry.binary_only {
|
|
// Like dpkg-genchanges/genbuildinfo, a binary-only upload references
|
|
// the previous source version in the `Source` field and records the
|
|
// entry in `Binary-Only-Changes`, but distributes NO source files:
|
|
// the previous `.dsc` and its tarballs already sit in the archive,
|
|
// and are not re-uploaded even when present next to the tree.
|
|
if let Some(prev) = &previous_entry {
|
|
source_display = format!("{} ({})", entry.source, prev.version.full());
|
|
binary_only_changes = Some(format!(
|
|
"{}\n\n -- {} <{}> {}",
|
|
entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw
|
|
));
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Binary package names and descriptions
|
|
// ------------------------------------------------------------------
|
|
let mut binaries: Vec<String> = Vec::new();
|
|
for name in &artifact_names {
|
|
if let Some(e) = files_list.get(name)
|
|
&& let Some(pkg) = &e.package
|
|
&& !binaries.contains(pkg)
|
|
{
|
|
binaries.push(pkg.clone());
|
|
}
|
|
}
|
|
binaries.sort();
|
|
|
|
// Description lines: first line of each binary stanza's Description,
|
|
// formatted exactly like dpkg-genchanges, sorted.
|
|
let mut descriptions = Vec::new();
|
|
for stanza in &control.binaries {
|
|
let Some(pkg) = stanza.get("Package") else {
|
|
continue;
|
|
};
|
|
if !binaries.contains(&pkg.to_string()) {
|
|
continue;
|
|
}
|
|
let summary = stanza
|
|
.get("Description")
|
|
.unwrap_or("no description available")
|
|
.lines()
|
|
.next()
|
|
.unwrap_or("no description available");
|
|
// Package-Type overrides the artifact-derived type (deb default).
|
|
let pkg_type = stanza
|
|
.get("Package-Type")
|
|
.map(str::to_string)
|
|
.unwrap_or_else(|| {
|
|
files_list
|
|
.iter()
|
|
.find(|f| f.package.as_deref() == Some(pkg))
|
|
.and_then(|f| f.package_type.clone())
|
|
.unwrap_or_else(|| "deb".to_string())
|
|
});
|
|
descriptions.push(crate::build::changes::format_description(
|
|
pkg, &pkg_type, summary,
|
|
));
|
|
}
|
|
descriptions.sort();
|
|
|
|
// ------------------------------------------------------------------
|
|
// Installed-Build-Depends closure over the context status database
|
|
// ------------------------------------------------------------------
|
|
// Like the source-build path, a status database that cannot be read is
|
|
// a hard error: silently treating it as empty would drop (or gut) the
|
|
// `Installed-Build-Depends` field of the produced metadata.
|
|
let status_path = Path::new("/var/lib/dpkg/status");
|
|
let status_content = ctx
|
|
.read_file(status_path)
|
|
.map_err(|e| format!("cannot read status file '{}': {}", status_path.display(), e))?;
|
|
let bd_fields = [
|
|
control.source.get("Build-Depends").unwrap_or(""),
|
|
control.source.get("Build-Depends-Arch").unwrap_or(""),
|
|
control.source.get("Build-Depends-Indep").unwrap_or(""),
|
|
];
|
|
let installed_build_depends =
|
|
crate::build::buildinfo::installed_build_depends_from_content(&status_content, &bd_fields)?;
|
|
|
|
// ------------------------------------------------------------------
|
|
// .buildinfo generation, then registration in debian/files
|
|
// ------------------------------------------------------------------
|
|
// Record exactly the environment that was exported to the build steps,
|
|
// overriding any host-inherited value (dpkg-style allowed-variable
|
|
// filtering, export precedence).
|
|
let environment = crate::build::env::buildinfo_environment(&opts.exported_env);
|
|
|
|
// dpkg-genbuildinfo sorts the accumulated architecture values, while
|
|
// dpkg-genchanges keeps encounter order.
|
|
let mut buildinfo_arch_values = arch_values.clone();
|
|
buildinfo_arch_values.sort();
|
|
|
|
let buildinfo_name = format!("{}_{}_{}.buildinfo", entry.source, sversion, opts.host_arch);
|
|
let buildinfo_doc =
|
|
crate::build::buildinfo::render_buildinfo(&crate::build::buildinfo::BuildInfoInput {
|
|
source: source_display.clone(),
|
|
binaries: binaries.clone(),
|
|
architecture: buildinfo_arch_values.join(" "),
|
|
version: entry.version.full(),
|
|
binary_only_changes: binary_only_changes.clone(),
|
|
build_origin: opts.vendor.clone(),
|
|
build_architecture: opts.build_arch.clone(),
|
|
build_date: chrono::Local::now().to_rfc2822(),
|
|
checksums: checksums.clone(),
|
|
installed_build_depends,
|
|
environment,
|
|
});
|
|
let buildinfo_path = upload_dir.join(&buildinfo_name);
|
|
ctx.write_file(
|
|
&buildinfo_path,
|
|
&crate::debian::control::write_paragraph(&buildinfo_doc),
|
|
)?;
|
|
|
|
// Register the .buildinfo in debian/files, like dpkg-genbuildinfo does,
|
|
// so the .changes distributes it.
|
|
files_list.add(crate::debian::FilesEntry::new(
|
|
&buildinfo_name,
|
|
control.section(),
|
|
control.priority(),
|
|
));
|
|
ctx.write_file(&package_dir.join("debian/files"), &files_list.render())?;
|
|
|
|
// Hash the freshly written .buildinfo inside the context.
|
|
let buildinfo_hashes =
|
|
hashes_in_context(ctx, upload_dir, std::slice::from_ref(&buildinfo_name))?;
|
|
if let Some(h) = buildinfo_hashes.get(&buildinfo_name) {
|
|
checksums.insert_entry(
|
|
&buildinfo_name,
|
|
ChecksumEntry {
|
|
size: h.size,
|
|
md5: h.md5.clone(),
|
|
sha1: h.sha1.clone(),
|
|
sha256: h.sha256.clone(),
|
|
// No SHA-512 digest available (see above); keeps the
|
|
// `Checksums-Sha512` `.buildinfo` field omitted.
|
|
sha512: String::new(),
|
|
},
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// .changes generation
|
|
// ------------------------------------------------------------------
|
|
let changes_name = format!("{}_{}_{}.changes", entry.source, sversion, opts.host_arch);
|
|
let changed_by = format!("{} <{}>", entry.maintainer_name, entry.maintainer_email);
|
|
let changes_doc = crate::build::changes::render_changes(&crate::build::changes::ChangesInput {
|
|
date: entry.date_raw.clone(),
|
|
source: source_display,
|
|
binaries,
|
|
binary_only: entry.binary_only,
|
|
built_for_profiles: opts.profiles.clone(),
|
|
architecture: arch_values.join(" "),
|
|
version: entry.version.full(),
|
|
distribution: entry.distribution.clone(),
|
|
urgency: entry.urgency.clone(),
|
|
maintainer: control.source.get("Maintainer").map(str::to_string),
|
|
changed_by: Some(changed_by),
|
|
descriptions,
|
|
closes: entry.closes.clone(),
|
|
changes_field: entry.changes_field.clone(),
|
|
checksums,
|
|
files_list,
|
|
});
|
|
let changes_path = upload_dir.join(&changes_name);
|
|
ctx.write_file(
|
|
&changes_path,
|
|
&crate::debian::control::write_paragraph(&changes_doc),
|
|
)?;
|
|
|
|
Ok((buildinfo_path, changes_path))
|
|
}
|
|
|
|
/// Compute md5/sha1/sha256 digests and sizes for the named files inside the
|
|
/// context directory `dir`, using coreutils.
|
|
fn hashes_in_context(
|
|
ctx: &Arc<Context>,
|
|
dir: &Path,
|
|
names: &[String],
|
|
) -> Result<BTreeMap<String, ArtifactHashes>, Box<dyn Error>> {
|
|
let mut out: BTreeMap<String, ArtifactHashes> = names
|
|
.iter()
|
|
.map(|n| (n.clone(), ArtifactHashes::default()))
|
|
.collect();
|
|
|
|
// Sizes. A failed `stat` must fail the metadata generation: an unchecked
|
|
// exit status would leave the default size 0 in the produced
|
|
// `.changes`/`.buildinfo` checksum entries.
|
|
let output = ctx
|
|
.command("stat")
|
|
.current_dir(dir)
|
|
.arg("-c")
|
|
.arg("%s %n")
|
|
.args(names)
|
|
.output()
|
|
.map_err(|e| format!("failed to run 'stat' inside the build context: {e}"))?;
|
|
if !output.status.success() {
|
|
return Err(format!(
|
|
"'stat' failed inside the build context: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
)
|
|
.into());
|
|
}
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for line in stdout.lines() {
|
|
let Some((size, name)) = line.trim().split_once(' ') else {
|
|
continue;
|
|
};
|
|
let size = size
|
|
.parse::<u64>()
|
|
.map_err(|_| format!("'stat' reported an invalid size '{size}' for '{name}'"))?;
|
|
if let Some(slot) = out.get_mut(name) {
|
|
slot.size = size;
|
|
}
|
|
}
|
|
|
|
// Digests.
|
|
for (tool, field) in [
|
|
("md5sum", 0usize),
|
|
("sha1sum", 1usize),
|
|
("sha256sum", 2usize),
|
|
] {
|
|
let output = ctx
|
|
.command(tool)
|
|
.current_dir(dir)
|
|
.args(names)
|
|
.output()
|
|
.map_err(|e| format!("failed to run '{tool}' inside the build context: {e}"))?;
|
|
if !output.status.success() {
|
|
return Err(format!(
|
|
"'{tool}' failed inside the build context: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
)
|
|
.into());
|
|
}
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for line in stdout.lines() {
|
|
let Some((digest, name)) = line.trim().split_once(" ") else {
|
|
continue;
|
|
};
|
|
let name = name.trim_start_matches('*');
|
|
if let Some(slot) = out.get_mut(name) {
|
|
match field {
|
|
0 => slot.md5 = digest.to_string(),
|
|
1 => slot.sha1 = digest.to_string(),
|
|
_ => slot.sha256 = digest.to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The recorded `.buildinfo` `Environment` must carry the environment
|
|
/// actually exported to the build steps (`parallel=N nocheck`, `LANG=C`,
|
|
/// ...), taking precedence over any host-inherited value, instead of
|
|
/// values recomputed from host state at generation time.
|
|
#[test]
|
|
fn environment_records_exported_env_not_host_defaults() {
|
|
let mut exported_env = BTreeMap::new();
|
|
exported_env.insert("LANG".to_string(), "C".to_string());
|
|
exported_env.insert(
|
|
"DEB_BUILD_OPTIONS".to_string(),
|
|
"parallel=7 nocheck".to_string(),
|
|
);
|
|
let opts = BinaryMetadataOptions {
|
|
profiles: Vec::new(),
|
|
vendor: "debian".to_string(),
|
|
exported_env,
|
|
build_arch: "amd64".to_string(),
|
|
host_arch: "amd64".to_string(),
|
|
};
|
|
|
|
let environment = crate::build::env::buildinfo_environment(&opts.exported_env);
|
|
assert!(
|
|
environment.contains("DEB_BUILD_OPTIONS=\"parallel=7 nocheck\""),
|
|
"recorded Environment must carry the exported DEB_BUILD_OPTIONS: {environment}"
|
|
);
|
|
assert!(
|
|
environment.contains("LANG=\"C\""),
|
|
"recorded Environment must carry the exported LANG: {environment}"
|
|
);
|
|
// Not in dpkg's allowed-variable list: never recorded.
|
|
assert!(!environment.contains("DEBIAN_FRONTEND"), "{environment}");
|
|
}
|
|
|
|
/// A binary-only (binNMU) build whose changelog cannot yield the
|
|
/// previous entry (malformed second header, unbalanced parenthesis) must
|
|
/// fail the metadata generation with a diagnostic naming the problem,
|
|
/// instead of silently emitting a plain `Source:` `.changes` with no
|
|
/// `Binary-Only-Changes` and no previous-version reference.
|
|
#[test]
|
|
fn binary_only_prev_version_parse_failure_errors_instead_of_wrong_metadata() {
|
|
let changelog = "\
|
|
hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes
|
|
|
|
* Binary-only rebuild.
|
|
|
|
-- A B <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
|
|
|
|
hello (1.0-1 unstable; urgency=medium
|
|
|
|
* Previous entry with an unbalanced parenthesis.
|
|
|
|
-- A B <a@b.c> Sun, 31 Dec 2023 00:00:00 +0000
|
|
";
|
|
let control = "\
|
|
Source: hello
|
|
Section: devel
|
|
Priority: optional
|
|
Maintainer: A B <a@b.c>
|
|
|
|
Package: hello
|
|
Architecture: all
|
|
Description: test package
|
|
";
|
|
let base = tempfile::tempdir().expect("tempdir");
|
|
let tree = base.path().join("hello-1.0");
|
|
std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree");
|
|
std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
|
|
std::fs::write(tree.join("debian/control"), control).expect("write control");
|
|
std::fs::write(
|
|
tree.join("debian/files"),
|
|
"hello_1.0-1+b1_all.deb devel optional\n",
|
|
)
|
|
.expect("write files");
|
|
std::fs::write(base.path().join("hello_1.0-1+b1_all.deb"), "deb payload")
|
|
.expect("write deb");
|
|
|
|
let ctx = Arc::new(
|
|
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
|
);
|
|
let opts = BinaryMetadataOptions {
|
|
profiles: Vec::new(),
|
|
vendor: "debian".to_string(),
|
|
exported_env: BTreeMap::new(),
|
|
build_arch: "amd64".to_string(),
|
|
host_arch: "amd64".to_string(),
|
|
};
|
|
let err = generate_binary_metadata(&ctx, &tree, base.path(), &opts)
|
|
.expect_err("binary-only build with an unparseable changelog must fail");
|
|
let err = err.to_string();
|
|
assert!(err.contains("unbalanced parenthesis"), "{err}");
|
|
assert!(err.contains("1.0-1 unstable"), "{err}");
|
|
}
|
|
|
|
/// An unreadable `debian/files` (e.g. permissions) must fail the
|
|
/// metadata generation with an error naming the read failure, instead of
|
|
/// being silently treated as an empty registry and reported as "no
|
|
/// binary artifacts found". A *missing* file stays tolerated (first
|
|
/// build in a fresh tree); the distinction matters.
|
|
#[test]
|
|
fn unreadable_debian_files_errors_instead_of_empty_registry() {
|
|
if crate::utils::root::is_root().unwrap_or(false) {
|
|
// Root can read files regardless of permissions.
|
|
return;
|
|
}
|
|
let changelog = "\
|
|
hello (1.0-1) unstable; urgency=medium
|
|
|
|
* Regular build.
|
|
|
|
-- A B <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
|
|
";
|
|
let control = "\
|
|
Source: hello
|
|
Section: devel
|
|
Priority: optional
|
|
Maintainer: A B <a@b.c>
|
|
|
|
Package: hello
|
|
Architecture: all
|
|
Description: test package
|
|
";
|
|
let base = tempfile::tempdir().expect("tempdir");
|
|
let tree = base.path().join("hello-1.0");
|
|
std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree");
|
|
std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
|
|
std::fs::write(tree.join("debian/control"), control).expect("write control");
|
|
let files_path = tree.join("debian/files");
|
|
std::fs::write(&files_path, "hello_1.0-1_all.deb devel optional\n").expect("write files");
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&files_path, std::fs::Permissions::from_mode(0o000))
|
|
.expect("chmod files");
|
|
}
|
|
|
|
let ctx = Arc::new(
|
|
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
|
);
|
|
let opts = BinaryMetadataOptions {
|
|
profiles: Vec::new(),
|
|
vendor: "debian".to_string(),
|
|
exported_env: BTreeMap::new(),
|
|
build_arch: "amd64".to_string(),
|
|
host_arch: "amd64".to_string(),
|
|
};
|
|
let err = generate_binary_metadata(&ctx, &tree, base.path(), &opts)
|
|
.expect_err("unreadable debian/files must fail with a read error");
|
|
let err = err.to_string();
|
|
assert!(err.contains("cannot read"), "{err}");
|
|
assert!(err.contains("debian/files"), "{err}");
|
|
#[cfg(unix)]
|
|
assert!(err.contains("Permission denied"), "{err}");
|
|
}
|
|
|
|
/// A binary-only (binNMU) build references the previous source version
|
|
/// (`Source: pkg (prev)`, `Binary-Only-Changes`) but must NOT
|
|
/// redistribute any source file: like dpkg-genchanges/genbuildinfo, the
|
|
/// previous `.dsc` and its tarballs stay out of both documents even when
|
|
/// they exist next to the artifacts.
|
|
#[test]
|
|
fn binary_only_metadata_references_previous_source_without_redistributing_it() {
|
|
let changelog = "\
|
|
hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes
|
|
|
|
* Binary-only rebuild.
|
|
|
|
-- A B <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
|
|
|
|
hello (1.0-1) unstable; urgency=medium
|
|
|
|
* Initial release.
|
|
|
|
-- A B <a@b.c> Sun, 31 Dec 2023 00:00:00 +0000
|
|
";
|
|
let control = "\
|
|
Source: hello
|
|
Section: devel
|
|
Priority: optional
|
|
Maintainer: A B <a@b.c>
|
|
|
|
Package: hello
|
|
Architecture: all
|
|
Description: test package
|
|
";
|
|
let base = tempfile::tempdir().expect("tempdir");
|
|
let tree = base.path().join("hello-1.0");
|
|
std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree");
|
|
std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
|
|
std::fs::write(tree.join("debian/control"), control).expect("write control");
|
|
std::fs::write(
|
|
tree.join("debian/files"),
|
|
"hello_1.0-1+b1_all.deb devel optional\n",
|
|
)
|
|
.expect("write files");
|
|
std::fs::write(base.path().join("hello_1.0-1+b1_all.deb"), "deb payload")
|
|
.expect("write deb");
|
|
|
|
// The trap: the previous source artifacts sit right next to the
|
|
// binaries, as they would after a source build. dpkg does not
|
|
// redistribute them for a binary-only upload, and neither must we.
|
|
std::fs::write(
|
|
base.path().join("hello_1.0-1.dsc"),
|
|
"Format: 3.0 (quilt)\nSource: hello\nBinary: hello\nArchitecture: any\nVersion: \
|
|
1.0-1\nMaintainer: A B <a@b.c>\nChecksums-Sha1:\n aaa111 12 \
|
|
hello_1.0.orig.tar.xz\n",
|
|
)
|
|
.expect("write previous dsc");
|
|
std::fs::write(base.path().join("hello_1.0.orig.tar.xz"), "tarball bytes")
|
|
.expect("write previous tarball");
|
|
|
|
let ctx = Arc::new(
|
|
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
|
|
);
|
|
let opts = BinaryMetadataOptions {
|
|
profiles: Vec::new(),
|
|
vendor: "debian".to_string(),
|
|
exported_env: BTreeMap::new(),
|
|
build_arch: "amd64".to_string(),
|
|
host_arch: "amd64".to_string(),
|
|
};
|
|
let (buildinfo_path, changes_path) =
|
|
generate_binary_metadata(&ctx, &tree, base.path(), &opts)
|
|
.expect("binNMU metadata generation must succeed");
|
|
|
|
let changes = std::fs::read_to_string(&changes_path).expect("read changes");
|
|
let buildinfo = std::fs::read_to_string(&buildinfo_path).expect("read buildinfo");
|
|
|
|
// The previous version is referenced textually.
|
|
assert!(
|
|
changes.contains("Source: hello (1.0-1)"),
|
|
"changes must reference the previous version: {changes}"
|
|
);
|
|
assert!(
|
|
buildinfo.contains("Binary-Only-Changes"),
|
|
"buildinfo must record the binary-only entry: {buildinfo}"
|
|
);
|
|
// ... but no source file is distributed, on either side.
|
|
for (doc, text) in [("changes", &changes), ("buildinfo", &buildinfo)] {
|
|
assert!(
|
|
!text.contains("hello_1.0-1.dsc"),
|
|
"{doc} must not redistribute the previous .dsc: {text}"
|
|
);
|
|
assert!(
|
|
!text.contains("hello_1.0.orig.tar.xz"),
|
|
"{doc} must not redistribute the previous tarball: {text}"
|
|
);
|
|
}
|
|
// The distributed set is exactly the binary artifacts + buildinfo.
|
|
assert!(
|
|
changes.contains("hello_1.0-1+b1_all.deb") && changes.contains(".buildinfo"),
|
|
"changes must distribute the deb and the buildinfo: {changes}"
|
|
);
|
|
}
|
|
}
|