mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-12 04:26:25 +00:00
40feffc8ce
Reviewed by @viniciusferrao. Each numbered point below is his; the code changes verify + fix it.
1. --build-number over-reach / dry-run / double-stamp
- The bump now runs ONLY on a real build: `--dry-run --build-number N` prints what it would
stamp and writes nothing (previously it rewrote every spec on disk during a dry run).
- Re-stamping is idempotent AND replacing: a re-run in a reused tree with a different
--build-number strips the prior .snap<ts>.<n> before applying the new one, instead of
accumulating a second stamp (…snap57.snap58). Extracted the per-line logic into the
unit-testable MockBuildUtils::restamp_release_line and covered it in t/mockbuild-all.t.
(The headline "rewrites xcat-core / xCAT-genesis-base.spec" does not occur in the real layout:
xcat-core is a sibling of $repo_root, and there is no genesis spec under the dep tree. The
legacy nested xcat-source-code case remains a non-CD layout; left as a follow-up.)
3. --finalize-xcat-dep idempotency
- cross_copy_genesis compared only SIGMD5 (content), which is blind to signature + index state.
It now also treats a same-content-but-UNSIGNED destination rpm as not-up-to-date (new
rpm_is_signed helper) so a crash between copy and sign heals on re-run.
- finalize_xcat_dep now re-indexes+signs BOTH repos of a touched pair every run, not only when
an rpm was copied, so a crash after copy+sign but before createrepo (rpm on disk, absent from
repomd) also heals.
4. Stale run-state can mask a failed build
- A real build now wipes its per-target $run_root first (run_id is derived from the deterministic
commit time, so re-runs reused the same tree). --skip-build keeps the tree; --dry-run writes nothing.
- mockbuild-perl-packages.pl clears each package's stale status.txt/error.txt BEFORE building, and
the aggregate now treats the child worker's exit code as authoritative: a package is PASS only if
its worker exited 0 AND wrote a PASS this run (a stale PASS in a reused log dir no longer counts).
5. --skip-build can publish the wrong artifacts
- --skip-build now REQUIRES an explicit --target (without it, all three EL targets collected the
same EL-agnostic roots and cross-published them).
- Collection is scoped to this target's own per-target $build_root (the same tree a normal build
populates), not the legacy build-output/list3/list5/list6 dirs.
- The manifest version-pin validation (and the "no manifest section" guard) now also run under
--skip-build, so a collection-only publish is validated exactly like a fresh build.
2. goconserver bypassed the CD bump (minimal fix; hermetic rebuild deferred)
- goconserver/mockbuild.pl gains --release-suffix, appended to its generated `Release: 4.elN`,
and mockbuild-all.pl passes the CD suffix down -- so goconserver's NVR advances per run like
every other dep package (an additive publish is no longer a silent no-op on a frozen NVR).
- Pinned the clone to an immutable upstream commit instead of the moving `master` (0.3.3 is
unreleased -- newest tag is v0.3.2 -- so a SHA pin is required; clone now fetches by ref).
- The host build + `go mod tidy` hermeticity concern is a tracked follow-up, not in this change.
7. Docs
- BUILD.md: --target is a single value, not repeatable; conserver-xcat is built for every target
(not "not required"). POD: --parallel-targets default is 1 = serial, not "auto".
- Added a manifest<->docs consistency test (conserver-xcat present in every target section).
(6, --max-parallel not a true global cap, is a documented nice-to-have and is left as a follow-up.)
Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
255 lines
13 KiB
Perl
255 lines
13 KiB
Perl
package MockBuildUtils;
|
|
# Reusable, unit-testable helpers factored out of mockbuild-all.pl. Kept free of that script's
|
|
# globals so t/mockbuild-all.t can exercise them directly. The two orchestration helpers that
|
|
# need signing / re-indexing (cross_copy_genesis, finalize_xcat_dep) take those as injected
|
|
# callbacks instead of reaching for gpg/createrepo state, so they stay pure and testable.
|
|
use strict;
|
|
use warnings;
|
|
use Exporter 'import';
|
|
use File::Basename qw(basename);
|
|
use File::Copy qw(copy);
|
|
|
|
our @EXPORT_OK = qw(
|
|
sh_quote print_step
|
|
version_matches required_pkgs have_rpm read_manifest
|
|
rpm_version rpm_sigmd5 rpm_is_signed restamp_release_line
|
|
cross_copy_genesis finalize_xcat_dep
|
|
);
|
|
|
|
# sh_quote: single-quote a string for safe use in a shell command.
|
|
sub sh_quote {
|
|
my ($s) = @_;
|
|
$s = '' if !defined $s;
|
|
$s =~ s/'/'"'"'/g;
|
|
return "'$s'";
|
|
}
|
|
|
|
# print_step: print a step banner.
|
|
sub print_step {
|
|
my ($msg) = @_;
|
|
print "\n== $msg ==\n";
|
|
}
|
|
|
|
# version_matches: does the built version $got satisfy the manifest pin $want? $want may be an
|
|
# exact version (2.19.0), a shell-style glob (2.* or 2.19.*), or '*' (any). Globs support * and
|
|
# ? and are anchored. Used so xCAT-genesis-base can pin 2.* (its Version walks with xcat-core)
|
|
# while the real xcat-dep packages stay exactly pinned.
|
|
sub version_matches {
|
|
my ($got, $want) = @_;
|
|
return 1 if !defined($want) || $want eq '*';
|
|
return ($got eq $want) unless $want =~ /[*?]/;
|
|
my $re = quotemeta($want);
|
|
$re =~ s/\\\*/.*/g;
|
|
$re =~ s/\\\?/./g;
|
|
return $got =~ /\A$re\z/ ? 1 : 0;
|
|
}
|
|
|
|
# required_pkgs: given a list of manifest package names and the skip flags, return the subset
|
|
# that must actually be built and validated. A package whose builder was skipped is NOT required:
|
|
# --skip-genesis drops xCAT-genesis-base, --skip-perl drops perl-*, --skip-xcat-dep drops the dep
|
|
# builders (everything that is neither genesis nor perl). Pure function (flags passed in) so both
|
|
# the version-pin check and assert_required_deps use it and it is unit-testable.
|
|
sub required_pkgs {
|
|
my ($pkgs, $skip_genesis, $skip_perl, $skip_dep) = @_;
|
|
return grep {
|
|
!($skip_genesis && $_ eq 'xCAT-genesis-base')
|
|
&& !($skip_perl && /^perl-/)
|
|
&& !($skip_dep && $_ ne 'xCAT-genesis-base' && $_ !~ /^perl-/)
|
|
} @$pkgs;
|
|
}
|
|
|
|
# have_rpm: is there a non-src rpm named <name>-... under $dir?
|
|
sub have_rpm {
|
|
my ($dir, $name) = @_;
|
|
my @m = grep { !/\.src\.rpm$/ } glob("$dir/${name}-*.rpm");
|
|
return scalar(@m) > 0;
|
|
}
|
|
|
|
# rpm_sigmd5: the SIGMD5 of an rpm -- the digest of its header+payload, independent of the GPG
|
|
# signature. Used to compare RPM identity/content: two rpms that share a basename but differ in
|
|
# content have different SIGMD5 (a bare filename match is not enough to call them identical).
|
|
sub rpm_sigmd5 {
|
|
my ($f) = @_;
|
|
return '' unless defined $f && -f $f;
|
|
my $v = `rpm -qp --qf '%{SIGMD5}' ${\ sh_quote($f)} 2>/dev/null`;
|
|
chomp $v;
|
|
return $v;
|
|
}
|
|
|
|
# rpm_is_signed: does the rpm carry a PGP/GPG header signature? SIGMD5 (above) is content-only and
|
|
# is identical whether or not the rpm is signed, so a cross-copied genesis that was copied but not
|
|
# yet signed (a crash between the copy and the rpmsign) still matches by SIGMD5. finalize uses this
|
|
# to treat such a rpm as NOT up to date so the copy+sign path re-runs and heals it.
|
|
sub rpm_is_signed {
|
|
my ($f) = @_;
|
|
return 0 unless defined $f && -f $f;
|
|
my $v = `rpm -qp --qf '%{SIGPGP}%{SIGGPG}' ${\ sh_quote($f)} 2>/dev/null`;
|
|
return 0 if !defined $v;
|
|
$v =~ s/\(none\)//g; # unsigned rpms report "(none)" for both tags
|
|
$v =~ s/\s+//g;
|
|
return $v ne '' ? 1 : 0;
|
|
}
|
|
|
|
# restamp_release_line: given a spec `Release: ...` line and a CD suffix (".snap<YYYYMMDDHHMM>.<n>"),
|
|
# return (new_line, changed). Idempotent: a line already ending in exactly $suffix is returned
|
|
# unchanged (changed=0). A line carrying a DIFFERENT prior .snap stamp (or several, from an earlier
|
|
# corrupted run) has it stripped before the new suffix is appended, so a re-run in a reused tree
|
|
# REPLACES the stamp instead of accumulating a second one (…snap...57 -> …snap...58, never
|
|
# …snap...57.snap...58). Only the Release token is touched; a non-Release line is returned as-is.
|
|
sub restamp_release_line {
|
|
my ($line, $suffix) = @_;
|
|
return ($line, 0) unless defined $line && $line =~ /^Release:\s*\S/i;
|
|
my $qs = quotemeta($suffix);
|
|
return ($line, 0) if $line =~ /$qs\s*$/; # already carries THIS suffix
|
|
(my $new = $line) =~ s/(?:\.snap\d{12}\.\d+)+(\s*)$/$1/; # drop any prior CD stamp(s)
|
|
$new =~ s/(^Release:\s*\S+)/$1$suffix/i;
|
|
return ($new, 1);
|
|
}
|
|
|
|
# rpm_version: %{version} of the built binary rpm named <name> under $dir (undef if absent).
|
|
# Skips src/debug rpms and confirms the rpm's real %{name} matches (glob can over-match).
|
|
# 'xCAT-genesis-base' matches the arch-suffixed rpm name (xCAT-genesis-base-x86_64 / -ppc64).
|
|
sub rpm_version {
|
|
my ($dir, $name) = @_;
|
|
my $glob = ($name eq 'xCAT-genesis-base')
|
|
? "$dir/xCAT-genesis-base-*.rpm"
|
|
: "$dir/${name}-*.rpm";
|
|
my %vers; # distinct %{version}s of the matching binary rpms
|
|
for my $f (sort glob($glob)) {
|
|
next if $f =~ /\.src\.rpm$/ || $f =~ /-debug(?:info|source)-/;
|
|
my $n = `rpm -qp --qf '%{name}' ${\ sh_quote($f)} 2>/dev/null`;
|
|
my $match = ($name eq 'xCAT-genesis-base')
|
|
? ($n =~ /^xCAT-genesis-base-/) : ($n eq $name);
|
|
next unless $match;
|
|
my $v = `rpm -qp --qf '%{version}' ${\ sh_quote($f)} 2>/dev/null`;
|
|
chomp $v;
|
|
$vers{$v} = 1 if $v ne '';
|
|
}
|
|
return undef unless %vers;
|
|
# More than one distinct version present means a stale artifact was not cleaned before the
|
|
# build -- a version pin could then pass against the wrong rpm and both could be shipped.
|
|
# (For genesis both arches share the same Version, so a normal x86_64+ppc64 pair is one entry.)
|
|
die "Multiple versions of $name present in $dir: " . join(', ', sort keys %vers)
|
|
. " (stale artifact not cleaned before the build)\n" if keys(%vers) > 1;
|
|
my ($v) = keys %vers;
|
|
return $v;
|
|
}
|
|
|
|
# read_manifest: parse packages-manifest.conf into %{ target => { package => version|'*' } }.
|
|
# INI format: [target] sections; "package=version|*" entries; blank / "#" / ";" lines ignored.
|
|
# Returns an empty hash if the file is absent (callers that build require a section per target).
|
|
sub read_manifest {
|
|
my ($path) = @_;
|
|
my %m;
|
|
return %m unless -f $path;
|
|
open my $fh, '<', $path or die "Cannot read manifest $path: $!\n";
|
|
my $sec;
|
|
while (my $line = <$fh>) {
|
|
$line =~ s/\r?\n\z//;
|
|
$line =~ s/^\s+|\s+$//g;
|
|
next if $line eq '' || $line =~ /^[#;]/;
|
|
if ($line =~ /^\[(.+?)\]$/) { $sec = $1; $m{$sec} ||= {}; next; }
|
|
next unless defined $sec;
|
|
my ($k, $v) = split /=/, $line, 2;
|
|
$k =~ s/\s+\z//;
|
|
$v = defined($v) ? ($v =~ s/^\s+//r) : '';
|
|
$m{$sec}{$k} = ($v ne '') ? $v : '*';
|
|
}
|
|
close $fh;
|
|
return %m;
|
|
}
|
|
|
|
# cross_copy_genesis: copy the noarch xCAT-genesis-base-<tarch>-*.rpm from $from into $to, dropping
|
|
# any stale foreign-arch genesis already in $to so the repo ends with exactly the fresh set.
|
|
# Returns the count of rpms newly copied (0 = already up to date, so the caller can skip
|
|
# re-indexing). Idempotent. $sign is an optional coderef ($rpm_path) invoked on each copied rpm
|
|
# (e.g. to re-sign it); pass undef to skip signing. Content is compared by SIGMD5, so a stale
|
|
# same-name rpm is refreshed rather than mistaken for up to date.
|
|
sub cross_copy_genesis {
|
|
my ($from, $to, $tarch, $sign) = @_;
|
|
my @src = grep { !/\.src\.rpm$/ } glob("$from/xCAT-genesis-base-$tarch-*.rpm");
|
|
return 0 if !@src;
|
|
my %want = map { basename($_) => $_ } @src;
|
|
my @existing = grep { !/\.src\.rpm$/ } glob("$to/xCAT-genesis-base-$tarch-*.rpm");
|
|
if (scalar(@existing) == scalar(keys %want)) {
|
|
my $up_to_date = 1;
|
|
for my $base (keys %want) {
|
|
my $dst = "$to/$base";
|
|
my $src_sig = rpm_sigmd5($want{$base});
|
|
# An empty SIGMD5 (unreadable rpm) means "cannot confirm identical" -> refresh rather
|
|
# than risk skipping on a false match (two '' would otherwise compare equal).
|
|
if (!-f $dst || $src_sig eq '' || $src_sig ne rpm_sigmd5($dst)) { $up_to_date = 0; last; }
|
|
# Content matches, but SIGMD5 cannot see the signature: a crash between the copy and the
|
|
# per-rpm sign leaves a same-content-but-UNSIGNED rpm. When a signer is configured, treat
|
|
# an unsigned dst as not-up-to-date so the copy+sign path re-runs and signs it.
|
|
if ($sign && !rpm_is_signed($dst)) { $up_to_date = 0; last; }
|
|
}
|
|
return 0 if $up_to_date;
|
|
}
|
|
for my $old (@existing) {
|
|
unlink $old or die "Failed to remove stale genesis $old: $!\n";
|
|
print "[finalize] - " . basename($old) . " (stale foreign-arch, removed from $to)\n";
|
|
}
|
|
my $copied = 0;
|
|
for my $base (sort keys %want) {
|
|
copy($want{$base}, "$to/$base")
|
|
or die "Failed to cross-copy genesis $want{$base} -> $to: $!\n";
|
|
print "[finalize] + $base ($from -> $to)\n";
|
|
$sign->("$to/$base") if $sign; # e.g. re-sign so the deploy gate never sees an unsigned rpm
|
|
$copied++;
|
|
}
|
|
return $copied;
|
|
}
|
|
|
|
# finalize_xcat_dep: cross-populate the noarch xCAT-genesis-base between each matching
|
|
# <os>/x86_64 and <os>/ppc64le repo pair (issue #7610), then re-index the repos that changed.
|
|
# %opt: sign => coderef($rpm) applied to copied rpms (or undef); reindex => coderef($dir) run on
|
|
# a repo whose rpm set changed (or undef). Both injected so this stays free of gpg/createrepo
|
|
# state and is unit-testable. Requires each arch's own genesis rpm to be present (a pair with no
|
|
# genesis is a hard error, never a silent no-op) and fails if no repo pair is found at all.
|
|
sub finalize_xcat_dep {
|
|
my ($x86_64_repo, $ppc64le_repo, %opt) = @_;
|
|
my $sign = $opt{sign};
|
|
my $reindex = $opt{reindex};
|
|
print_step('Finalize xcat-dep: cross-arch genesis-base provisioning (issue #7610)');
|
|
print "x86_64-repo: $x86_64_repo\n";
|
|
print "ppc64le-repo: $ppc64le_repo\n";
|
|
my @osdirs = grep { -d "$_/x86_64" } glob("$x86_64_repo/*");
|
|
my $pairs = 0;
|
|
for my $p (sort @osdirs) {
|
|
my $osdir = basename($p);
|
|
my $x86dir = "$x86_64_repo/$osdir/x86_64";
|
|
my $ppcdir = "$ppc64le_repo/$osdir/ppc64le";
|
|
# Require the peer repo itself: in the CD both arches build every EL, so a missing
|
|
# ppc64le peer for an x86_64 OS means an incomplete input, not something to skip past
|
|
# (skipping would leave that OS's x86_64 repo without the ppc64 genesis and still exit 0).
|
|
die "FATAL: [finalize] $osdir: no ppc64le peer repo at $ppcdir\n"
|
|
. " (both arches must build every EL before finalize)\n" if !-d $ppcdir;
|
|
# Require the expected inputs: each arch's build must have produced its OWN genesis rpm
|
|
# before finalize cross-populates them. Without this, a pair whose builds produced no
|
|
# genesis rpms would make finalize a silent no-op that still exits 0 (the bug this guards).
|
|
die "FATAL: [finalize] $osdir: no x86_64 xCAT-genesis-base rpm in $x86dir\n"
|
|
if !grep { !/\.src\.rpm$/ } glob("$x86dir/xCAT-genesis-base-x86_64-*.rpm");
|
|
die "FATAL: [finalize] $osdir: no ppc64 xCAT-genesis-base rpm in $ppcdir\n"
|
|
if !grep { !/\.src\.rpm$/ } glob("$ppcdir/xCAT-genesis-base-ppc64-*.rpm");
|
|
# xCAT collapses ppc/ppc64/ppc64le into tarch=ppc64, so the ppc genesis rpm is
|
|
# named xCAT-genesis-base-ppc64-*. Cross-copy both directions.
|
|
my $to_x86 = cross_copy_genesis($ppcdir, $x86dir, 'ppc64', $sign);
|
|
my $to_ppc = cross_copy_genesis($x86dir, $ppcdir, 'x86_64', $sign);
|
|
# Re-index+sign BOTH repos of the pair every finalize, not only when an rpm was copied this
|
|
# run. A crash after a prior run's copy+sign but before its createrepo leaves the genesis rpm
|
|
# on disk (so cross_copy_genesis now returns 0) yet ABSENT from repomd.xml -- which no
|
|
# signature gate catches. Re-indexing is cheap (these are tiny repos) and idempotent, and it
|
|
# heals that partial state; skipped only when no signer/indexer was injected.
|
|
if ($reindex) { $reindex->($x86dir); $reindex->($ppcdir); }
|
|
printf "[finalize] %s: %d ppc64 genesis -> x86_64, %d x86_64 genesis -> ppc64le\n",
|
|
$osdir, $to_x86, $to_ppc;
|
|
$pairs++;
|
|
}
|
|
die "FATAL: --finalize-xcat-dep found no <os>/x86_64 + <os>/ppc64le repo pair under\n"
|
|
. " --x86_64-repo '$x86_64_repo'\n --ppc64le-repo '$ppc64le_repo'\n" if $pairs == 0;
|
|
print_step('Finalize complete');
|
|
}
|
|
|
|
1;
|