build: only redistribute the orig tarball on new upstream (-si)

dpkg-genchanges includes the upstream tarballs in the .changes only when
the upload brings a new upstream: no previous changelog entry, a changed
upstream version or a renamed source. On a plain revision bump the
tarball already sits in the archive, and dpkg strips it (and its .asc)
from the distribution set.

pkh's native source pipeline listed every .dsc-referenced tarball
unconditionally, making every upload re-ship the orig. Implement the
dpkg source styles as --orig auto|always|never (auto being the -si
default; always/never are -sa/-sd), stripping the tarballs out of the
changes, buildinfo-free checksum set and artifact list like dpkg, with
the explicit 'never' ignored for native packages. Comparison uses the
epoch-less upstream version, exactly like dpkg's version().

Differential tests against real dpkg cover revision bumps, new upstream
versions and both forced styles.
This commit is contained in:
2026-09-17 23:45:15 +02:00
parent c18f1fe9c2
commit 4ab41e691a
4 changed files with 308 additions and 36 deletions
+133
View File
@@ -4,10 +4,58 @@
use std::path::Path;
use super::OrigSourceMode;
use crate::debian::changelog::ChangelogEntry;
use crate::debian::checksums::FileChecksums;
use crate::debian::control::{Paragraph, write_paragraph};
use crate::debian::files::FilesList;
/// Compression suffixes dpkg recognizes on source tarballs.
const TARBALL_COMPRESSIONS: &[&str] = &[".gz", ".bz2", ".xz", ".lzma", ".zst"];
/// Whether this `.dsc`-listed file is an upstream orig tarball
/// (`*.orig.tar.<ext>` or a component tarball `*.orig-<c>.tar.<ext>`),
/// mirroring dpkg-genchanges' strip pattern `\.orig(-.+)?\.tar\.$ext`.
pub fn is_orig_tarball(name: &str) -> bool {
TARBALL_COMPRESSIONS.iter().any(|ext| {
name.strip_suffix(ext)
.and_then(|s| s.strip_suffix(".tar"))
.is_some_and(|stem| stem.ends_with(".orig") || stem.contains(".orig-"))
})
}
/// Whether this `.dsc`-listed file is the Debian part of the source package
/// (`*.debian.tar.<ext>` for the 3.0 formats, `*.diff.<ext>` for 1.0).
pub fn is_debian_tarball_or_diff(name: &str) -> bool {
TARBALL_COMPRESSIONS.iter().any(|ext| {
name.ends_with(&format!(".debian.tar{ext}")) || name.ends_with(&format!(".diff{ext}"))
})
}
/// Whether the upload redistributes the upstream tarballs, mirroring the
/// dpkg-genchanges source styles: `Always`/`Never` are the forced
/// `-sa`/`-sd`, while `Auto` is the default `-si` — include them only when
/// there is no previous changelog entry (first upload) or the source name or
/// upstream version changed since it. Like dpkg, the comparison uses the
/// epoch-less upstream version: a plain revision bump reuses the tarball
/// already in the archive.
pub fn include_orig_tarball(
mode: OrigSourceMode,
current: &ChangelogEntry,
previous: Option<&ChangelogEntry>,
) -> bool {
match mode {
OrigSourceMode::Always => true,
OrigSourceMode::Never => false,
OrigSourceMode::Auto => match previous {
None => true,
Some(prev) => {
prev.source != current.source || prev.version.upstream != current.version.upstream
}
},
}
}
/// Everything needed to render a `.changes` file.
#[derive(Debug, Clone)]
pub struct ChangesInput {
@@ -159,6 +207,91 @@ pub fn save_changes(path: &Path, paragraph: &Paragraph) -> Result<(), Box<dyn st
mod tests {
use super::*;
#[test]
fn orig_tarball_detection() {
assert!(is_orig_tarball("pkg_1.0.orig.tar.gz"));
assert!(is_orig_tarball("pkg_1.0.orig.tar.xz"));
assert!(is_orig_tarball("pkg_1.0.orig.tar.zst"));
assert!(is_orig_tarball("pkg_1.0~rc1.orig.tar.bz2"));
// Component tarballs.
assert!(is_orig_tarball("pkg_1.0.orig-docs.tar.xz"));
assert!(is_orig_tarball("pkg_1.0.orig-vendor.tar.gz"));
// Not orig tarballs.
assert!(!is_orig_tarball("pkg_1.0.debian.tar.xz"));
assert!(!is_debian_tarball_or_diff("pkg_1.0.orig.tar.xz"));
assert!(!is_orig_tarball("pkg_1.0.tar.xz")); // native tarball
assert!(!is_orig_tarball("pkg_1.0.dsc"));
assert!(!is_orig_tarball("pkg_1.0.orig.tar")); // no compression suffix
}
#[test]
fn debian_tarball_detection() {
assert!(is_debian_tarball_or_diff("pkg_1.0.debian.tar.xz"));
assert!(is_debian_tarball_or_diff("pkg_1.0.diff.gz"));
assert!(!is_debian_tarball_or_diff("pkg_1.0.orig.tar.xz"));
assert!(!is_debian_tarball_or_diff("pkg_1.0.tar.xz"));
}
/// Build a minimal changelog entry for one source/version pair.
fn entry(src: &str, ver: &str) -> ChangelogEntry {
crate::debian::changelog::parse_changelog_entries_from_str(
&format!(
"{src} ({ver}) unstable; urgency=medium\n\n * x\n\n \
-- A B <a@b.c> Thu, 01 Jan 2026 00:00:00 +0000\n"
),
Some(1),
)
.unwrap()
.remove(0)
}
#[test]
fn orig_inclusion_matrix() {
let cur = entry("pkg", "1.4-2");
let prev_same_upstream = entry("pkg", "1.4-1");
let prev_new_upstream = entry("pkg", "2.0-1");
let prev_renamed = entry("renamed", "1.4-1");
// -sa / -sd force the outcome.
assert!(include_orig_tarball(
OrigSourceMode::Always,
&cur,
Some(&prev_same_upstream)
));
assert!(!include_orig_tarball(
OrigSourceMode::Never,
&cur,
Some(&prev_new_upstream)
));
// -si: first upload includes; a revision bump excludes; a new
// upstream version or a renamed source includes.
assert!(include_orig_tarball(OrigSourceMode::Auto, &cur, None));
assert!(!include_orig_tarball(
OrigSourceMode::Auto,
&cur,
Some(&prev_same_upstream)
));
assert!(include_orig_tarball(
OrigSourceMode::Auto,
&cur,
Some(&prev_new_upstream)
));
assert!(include_orig_tarball(
OrigSourceMode::Auto,
&cur,
Some(&prev_renamed)
));
// The epoch is not part of the comparison, like dpkg's version().
let cur_epoch = entry("pkg", "2:1.4-2");
assert!(!include_orig_tarball(
OrigSourceMode::Auto,
&cur_epoch,
Some(&prev_same_upstream)
));
}
#[test]
fn description_formatting() {
assert_eq!(