mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-12 04:26:25 +00:00
fix(xcat-dep): address PR #62 review -- finalize/genesis/skip correctness + tests
Review feedback (viniciusferrao):
1. --finalize-xcat-dep no longer succeeds with no genesis rpms and no longer treats
a shared filename as up to date when the content differs.
- finalize_xcat_dep now REQUIRES each arch's own genesis rpm for every repo pair
it processes (a pair with none is a hard FATAL, not a silent exit-0 no-op).
- cross_copy_genesis compares RPM identity by SIGMD5 (header+payload digest,
independent of the GPG signature), so a stale rpm that merely shares a basename
is refreshed instead of being mistaken for up to date.
2. Remove the genesis workaround. xcat-core #7696 is merged, so buildrpms.pl now
exits 0 iff it produced the genesis rpm; the zero-tolerance check no longer
ignores a genesis failure when a matching (possibly stale) rpm already exists --
any failed build step, genesis included, fails the run.
3. Skip modes work. required_pkgs() drops the packages whose builder was skipped, and
both the version-pin check and assert_required_deps use it, so a clean
--skip-genesis / --skip-xcat-dep / --skip-perl run no longer fails validating
packages it deliberately did not build.
4. Tests. The reusable, side-effect-free helpers are factored into MockBuildUtils.pm
(cross_copy_genesis and finalize_xcat_dep take injected sign/reindex callbacks so
they carry no gpg/createrepo state) and t/mockbuild-all.t adds focused fixture
tests for all of the above: skip-mode selection, version-pin globs, SIGMD5-based
RPM-identity comparison + cross_copy refresh/idempotency, and the finalize
require-inputs guard. Run with `prove t/`.
Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
@@ -304,6 +304,19 @@ find <REPO_ROOT>/build-output/mockbuild-all/<RUN_ID>/build-logs -type f | sort
|
||||
- `mock target not found`
|
||||
- Validate with `mock -r <TARGET> --print-root-path` and install the required mock config packages.
|
||||
|
||||
# Tests
|
||||
|
||||
The reusable, side-effect-free helpers live in `MockBuildUtils.pm` (package selection under the
|
||||
`--skip-*` flags, version-pin matching incl. globs, RPM-identity comparison, and the cross-arch
|
||||
genesis `finalize` logic). Focused fixture tests cover them:
|
||||
|
||||
```bash
|
||||
prove t/ # or: perl t/mockbuild-all.t
|
||||
```
|
||||
|
||||
The RPM-identity / `cross_copy_genesis` cases build tiny fixture rpms and are skipped
|
||||
automatically if `rpmbuild` is unavailable.
|
||||
|
||||
# References
|
||||
|
||||
- [mock project repository](https://github.com/rpm-software-management/mock)
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
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_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";
|
||||
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;
|
||||
return $v;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
# 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";
|
||||
if (!-f $dst || rpm_sigmd5($want{$base}) ne rpm_sigmd5($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";
|
||||
if (!-d $ppcdir) {
|
||||
print "[finalize] $osdir: no ppc64le peer at $ppcdir -- skipping\n";
|
||||
next;
|
||||
}
|
||||
# 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);
|
||||
$reindex->($x86dir) if $to_x86 && $reindex;
|
||||
$reindex->($ppcdir) if $to_ppc && $reindex;
|
||||
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;
|
||||
+29
-163
@@ -11,6 +11,10 @@ use File::Path qw(make_path);
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use Parallel::ForkManager;
|
||||
use POSIX qw(strftime);
|
||||
use FindBin qw($RealBin);
|
||||
use lib $RealBin;
|
||||
use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs have_rpm
|
||||
read_manifest rpm_version rpm_sigmd5 cross_copy_genesis finalize_xcat_dep);
|
||||
|
||||
my $script_dir = abs_path(dirname(__FILE__));
|
||||
my $repo_root = abs_path($script_dir);
|
||||
@@ -131,7 +135,16 @@ if ($finalize_xcat_dep) {
|
||||
my $ppc = abs_path($ppc64le_repo) or die "--ppc64le-repo '$ppc64le_repo' not found\n";
|
||||
die "--x86_64-repo '$x86' is not a directory\n" if !-d $x86;
|
||||
die "--ppc64le-repo '$ppc' is not a directory\n" if !-d $ppc;
|
||||
finalize_xcat_dep($x86, $ppc);
|
||||
# Inject the per-rpm gpg re-sign and the repo re-index as callbacks so the finalize logic in
|
||||
# MockBuildUtils stays free of this script's gpg/createrepo state.
|
||||
finalize_xcat_dep($x86, $ppc,
|
||||
sign => ($gpg_sign ? sub {
|
||||
my ($rpm) = @_;
|
||||
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
|
||||
run_simple(qq(rpmsign --define "%_gpg_name $gpg_key_name" --addsign ) . sh_quote($rpm));
|
||||
} : undef),
|
||||
reindex => \&reindex_and_sign_repo,
|
||||
);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
@@ -517,22 +530,12 @@ if (!$skip_build) {
|
||||
}
|
||||
}
|
||||
|
||||
# Zero-tolerance: any required (manifest) package that failed to build fails the run.
|
||||
# genesis is the one exception -- xcat-core's buildrpms.pl exits non-zero on an unrelated
|
||||
# post-build xCAT-release-latest cp even when the genesis rpm IS produced, so genesis
|
||||
# counts as failed only if its rpm is absent, not on exit code.
|
||||
my @hard;
|
||||
for my $id (@failed) {
|
||||
if ($id eq 'genesis') {
|
||||
my @g = grep { !/\.src\.rpm$/ }
|
||||
glob("$xcat_src/dist/$target/rpms/xCAT-genesis-base-*.rpm");
|
||||
push @hard, $id unless @g;
|
||||
}
|
||||
else {
|
||||
push @hard, $id;
|
||||
}
|
||||
}
|
||||
die "FATAL: required build step(s) failed for $target: @hard\n" if @hard;
|
||||
# Zero-tolerance: any build step that failed fails the whole run -- genesis included.
|
||||
# (xcat-core #7696 is merged: buildrpms.pl now exits 0 iff it actually produced the
|
||||
# genesis rpm, so there is no cosmetic non-zero exit left to tolerate. The old workaround
|
||||
# -- ignore a genesis failure when a matching rpm already exists in dist/ -- is gone; a
|
||||
# stale artifact from a previous build must never mask a failed genesis build.)
|
||||
die "FATAL: required build step(s) failed for $target: @failed\n" if @failed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +593,9 @@ if (!$skip_genesis && !$dry_run) {
|
||||
# (which carries the per-EL dist tag and the genesis snap timestamp).
|
||||
if (!$dry_run && !$skip_build) {
|
||||
my @vmiss;
|
||||
for my $pkg (sort keys %req) {
|
||||
# Only validate packages whose builder was NOT skipped -- so a clean --skip-* run does not
|
||||
# fail on packages it deliberately did not build.
|
||||
for my $pkg (required_pkgs([sort keys %req], $skip_genesis, $skip_perl, $skip_xcat_dep)) {
|
||||
my $want = $req{$pkg};
|
||||
next if !defined($want) || $want eq '*';
|
||||
my $got = rpm_version($repo_dir, $pkg);
|
||||
@@ -798,77 +803,7 @@ EOF
|
||||
close $b;
|
||||
}
|
||||
|
||||
# --finalize-xcat-dep: cross-populate the noarch xCAT-genesis-base between each matching
|
||||
# <os>/x86_64 and <os>/ppc64le repo pair, then re-index + re-sign the repos that changed.
|
||||
# <x86_64-repo>/<ppc64le-repo> are the two per-arch repo roots (each holding rh8/rh9/rh10/<arch>).
|
||||
# They may be the same path (both arches built into one tree) or two separate trees (one per
|
||||
# build host); either way pairs are matched by <os> subdir.
|
||||
sub finalize_xcat_dep {
|
||||
my ($x86_64_repo, $ppc64le_repo) = @_;
|
||||
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";
|
||||
# Every OS-release dir under the x86_64 repo that actually has an x86_64 sub-repo.
|
||||
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";
|
||||
if (!-d $ppcdir) {
|
||||
print "[finalize] $osdir: no ppc64le peer at $ppcdir -- skipping\n";
|
||||
next;
|
||||
}
|
||||
# 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');
|
||||
my $to_ppc = cross_copy_genesis($x86dir, $ppcdir, 'x86_64');
|
||||
reindex_and_sign_repo($x86dir) if $to_x86;
|
||||
reindex_and_sign_repo($ppcdir) if $to_ppc;
|
||||
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');
|
||||
}
|
||||
|
||||
# Cross-copy the noarch xCAT-genesis-base-<tarch>-*.rpm from $from into $to. Drops any
|
||||
# stale foreign-arch genesis already in $to (e.g. issue #7610's 2.16.3 ppc leftover) so
|
||||
# the repo ends with exactly the fresh set. Returns the number of rpms newly copied
|
||||
# (0 = already up to date, so the caller can skip re-indexing). Idempotent.
|
||||
sub cross_copy_genesis {
|
||||
my ($from, $to, $tarch) = @_;
|
||||
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");
|
||||
my %have = map { basename($_) => 1 } @existing;
|
||||
# Already exactly the fresh set (same basenames)? idempotent no-op.
|
||||
if (scalar(keys %want) == scalar(keys %have) && !grep { !$have{$_} } keys %want) {
|
||||
return 0;
|
||||
}
|
||||
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";
|
||||
# The source rpm is already signed by the build, but re-assert it under --gpg-sign
|
||||
# so the deploy signing gate never sees an unsigned cross-copied rpm.
|
||||
if ($gpg_sign) {
|
||||
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
|
||||
run_simple(qq(rpmsign --define "%_gpg_name $gpg_key_name" --addsign )
|
||||
. sh_quote("$to/$base"));
|
||||
}
|
||||
$copied++;
|
||||
}
|
||||
return $copied;
|
||||
}
|
||||
|
||||
# Re-run createrepo_c on a repo whose rpm set changed, and (under --gpg-sign) re-sign +
|
||||
# re-export repomd. Does NOT re-sign the rpms (cross_copy_genesis already did the copied
|
||||
@@ -950,10 +885,6 @@ Notes:
|
||||
USAGE
|
||||
}
|
||||
|
||||
sub print_step {
|
||||
my ($msg) = @_;
|
||||
print "\n== $msg ==\n";
|
||||
}
|
||||
|
||||
sub require_command {
|
||||
my ($cmd) = @_;
|
||||
@@ -1119,71 +1050,11 @@ sub run_build_steps_parallel {
|
||||
return sort keys %failed;
|
||||
}
|
||||
|
||||
# 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_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).
|
||||
# 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;
|
||||
}
|
||||
|
||||
sub rpm_version {
|
||||
my ($dir, $name) = @_;
|
||||
my $glob = ($name eq 'xCAT-genesis-base')
|
||||
? "$dir/xCAT-genesis-base-*.rpm"
|
||||
: "$dir/${name}-*.rpm";
|
||||
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;
|
||||
return $v;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
# assert_required_deps: the per-EL dep repo is unusable without these, so a MISSING one is
|
||||
# fatal even though individual builder failures are tolerated above. genesis-base is required
|
||||
@@ -1196,9 +1067,10 @@ sub assert_required_deps {
|
||||
# elilo-xcat is noarch but xCAT hard-requires it (Requires: elilo-xcat >= 3.14-6) on EVERY arch,
|
||||
# so a missing elilo makes the whole dep repo uninstallable -- it MUST be required here, not
|
||||
# silently tolerated (it builds from a tracked prebuilt on ppc64le/EL8, compiled elsewhere).
|
||||
my @req = qw(elilo-xcat ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
|
||||
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB);
|
||||
push @req, 'xCAT-genesis-base' unless $skip_genesis;
|
||||
# A package whose builder was skipped is not required (else a clean --skip-* run fails).
|
||||
my @all = qw(elilo-xcat ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
|
||||
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB xCAT-genesis-base);
|
||||
my @req = required_pkgs(\@all, $skip_genesis, $skip_perl, $skip_xcat_dep);
|
||||
my @missing = grep { !have_rpm($dir, $_) } @req;
|
||||
die "FATAL: required deps missing from $dir: @missing\n" if @missing;
|
||||
print "[deps] required set present in $dir: @req\n";
|
||||
@@ -1443,9 +1315,3 @@ sub slurp_chomp {
|
||||
return $line // '';
|
||||
}
|
||||
|
||||
sub sh_quote {
|
||||
my ($s) = @_;
|
||||
$s = '' if !defined $s;
|
||||
$s =~ s/'/'"'"'/g;
|
||||
return "'$s'";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/perl
|
||||
# Focused fixture tests for the xcat-dep build helpers (MockBuildUtils.pm), covering the review
|
||||
# feedback on PR #62: skip-mode package selection, version pins, RPM-identity comparison in the
|
||||
# cross-arch genesis finalize, and the "require the genesis input" guard.
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use FindBin qw($RealBin);
|
||||
use lib "$RealBin/..";
|
||||
use File::Temp qw(tempdir);
|
||||
use File::Path qw(make_path);
|
||||
use File::Basename qw(basename);
|
||||
use MockBuildUtils qw(required_pkgs version_matches rpm_sigmd5
|
||||
cross_copy_genesis finalize_xcat_dep read_manifest);
|
||||
|
||||
# Run a printing sub with STDOUT muted so its progress lines do not pollute TAP.
|
||||
sub quiet(&) {
|
||||
my ($code) = @_;
|
||||
open(my $save, '>&', \*STDOUT) or die "dup STDOUT: $!";
|
||||
open(STDOUT, '>', '/dev/null') or die "mute STDOUT: $!";
|
||||
my @r = eval { $code->() };
|
||||
my $err = $@;
|
||||
open(STDOUT, '>&', $save) or die "restore STDOUT: $!";
|
||||
die $err if $err;
|
||||
return wantarray ? @r : $r[0];
|
||||
}
|
||||
|
||||
# ---- required_pkgs: a skipped builder's packages are not required (clean --skip-* runs) -------
|
||||
my @all = qw(elilo-xcat ipmitool-xcat perl-IO-Stty perl-Sys-Virt xCAT-genesis-base);
|
||||
is_deeply([required_pkgs(\@all, 0, 0, 0)], \@all,
|
||||
'no skips -> every package required');
|
||||
is_deeply([required_pkgs(\@all, 1, 0, 0)], [qw(elilo-xcat ipmitool-xcat perl-IO-Stty perl-Sys-Virt)],
|
||||
'--skip-genesis drops xCAT-genesis-base');
|
||||
is_deeply([required_pkgs(\@all, 0, 1, 0)], [qw(elilo-xcat ipmitool-xcat xCAT-genesis-base)],
|
||||
'--skip-perl drops perl-*');
|
||||
is_deeply([required_pkgs(\@all, 0, 0, 1)], [qw(perl-IO-Stty perl-Sys-Virt xCAT-genesis-base)],
|
||||
'--skip-xcat-dep drops the dep builders');
|
||||
is_deeply([required_pkgs(\@all, 1, 1, 1)], [],
|
||||
'all skips -> nothing required (a clean skip run validates nothing)');
|
||||
|
||||
# ---- version_matches: exact + shell-glob pins ------------------------------------------------
|
||||
ok( version_matches('2.19.0', '2.*'), '2.* matches 2.19.0');
|
||||
ok( version_matches('2.18.2', '2.*'), '2.* matches 2.18.2 (walks with xcat-core)');
|
||||
ok(!version_matches('3.0.0', '2.*'), '2.* rejects 3.0.0');
|
||||
ok(!version_matches('20.0', '2.*'), '2.* rejects 20.0 (anchored, literal dot)');
|
||||
ok( version_matches('2.19.0', '2.19.*'), '2.19.* matches 2.19.0');
|
||||
ok(!version_matches('2.20.0', '2.19.*'), '2.19.* rejects 2.20.0');
|
||||
ok( version_matches('1.8.18', '1.8.18'), 'exact pin matches');
|
||||
ok(!version_matches('1.8.19', '1.8.18'), 'exact pin rejects a different version');
|
||||
ok( version_matches('anything', '*'), "'*' matches any version");
|
||||
|
||||
# ---- read_manifest: sections + entries -------------------------------------------------------
|
||||
{
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $f = "$dir/m.conf";
|
||||
open my $fh, '>', $f or die;
|
||||
print $fh "# comment\n[alma+epel-8-x86_64]\nelilo-xcat=3.14\nxCAT-genesis-base=2.*\n\n"
|
||||
. "[alma+epel-9-x86_64]\nperl-Sys-Virt=11.10.0\n";
|
||||
close $fh;
|
||||
my %m = read_manifest($f);
|
||||
is($m{'alma+epel-8-x86_64'}{'elilo-xcat'}, '3.14', 'read_manifest: exact pin');
|
||||
is($m{'alma+epel-8-x86_64'}{'xCAT-genesis-base'},'2.*', 'read_manifest: glob pin');
|
||||
is($m{'alma+epel-9-x86_64'}{'perl-Sys-Virt'}, '11.10.0', 'read_manifest: second section');
|
||||
is_deeply({read_manifest("$dir/nope.conf")}, {}, 'read_manifest: missing file -> empty');
|
||||
}
|
||||
|
||||
# ---- RPM-identity comparison + cross_copy_genesis (needs rpmbuild for real rpms) --------------
|
||||
SKIP: {
|
||||
skip 'rpmbuild not available', 5 if system('command -v rpmbuild >/dev/null 2>&1') != 0;
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my $seq = 0;
|
||||
my $mk = sub { # build a genesis-named rpm with a given marker payload
|
||||
my ($tarch, $content) = @_;
|
||||
my $out = "$tmp/out" . (++$seq); # unique dir: same NVR would overwrite in a shared one
|
||||
my $spec = "$tmp/$tarch-$seq.spec";
|
||||
open my $fh, '>', $spec or die;
|
||||
print $fh <<"SPEC";
|
||||
Name: xCAT-genesis-base-$tarch
|
||||
Version: 2.19.0
|
||||
Release: snapTEST
|
||||
Summary: test fixture
|
||||
License: EPL
|
||||
BuildArch: noarch
|
||||
%description
|
||||
test fixture
|
||||
%install
|
||||
mkdir -p %{buildroot}/opt/xcat/t
|
||||
echo '$content' > %{buildroot}/opt/xcat/t/marker
|
||||
%files
|
||||
/opt/xcat/t/marker
|
||||
SPEC
|
||||
close $fh;
|
||||
system("rpmbuild -bb --quiet --define '_topdir $tmp/rpmb$seq' --define '_rpmdir $out' "
|
||||
. "'$spec' >/dev/null 2>&1") == 0 or die "rpmbuild failed for $tarch/$content";
|
||||
my ($rpm) = glob("$out/noarch/xCAT-genesis-base-$tarch-*.rpm");
|
||||
return $rpm;
|
||||
};
|
||||
my $rpmA = $mk->('ppc64', 'CONTENT_A');
|
||||
my $rpmB = $mk->('ppc64', 'CONTENT_B_is_different'); # same NVR/basename, different payload
|
||||
|
||||
isnt(rpm_sigmd5($rpmA), rpm_sigmd5($rpmB),
|
||||
'rpm_sigmd5 differs for same-name rpms with different content');
|
||||
|
||||
my $base = basename($rpmA);
|
||||
my ($from, $to) = ("$tmp/from", "$tmp/to");
|
||||
make_path($from, $to);
|
||||
system("cp '$rpmA' '$from/$base'"); # the fresh source
|
||||
system("cp '$rpmB' '$to/$base'"); # a STALE dest rpm sharing the filename
|
||||
|
||||
my $n = quiet { cross_copy_genesis($from, $to, 'ppc64', undef) };
|
||||
ok($n >= 1, "cross_copy refreshes a stale same-name rpm by content (copied=$n)");
|
||||
is(rpm_sigmd5("$to/$base"), rpm_sigmd5($rpmA),
|
||||
'after cross_copy the dest matches the source content');
|
||||
|
||||
my $n2 = quiet { cross_copy_genesis($from, $to, 'ppc64', undef) };
|
||||
is($n2, 0, 'cross_copy is a no-op when content is already identical (idempotent)');
|
||||
|
||||
# A signer callback is invoked for each copied rpm.
|
||||
my ($from2, $to2) = ("$tmp/from2", "$tmp/to2");
|
||||
make_path($from2, $to2);
|
||||
system("cp '$rpmA' '$from2/$base'");
|
||||
my @signed;
|
||||
quiet { cross_copy_genesis($from2, $to2, 'ppc64', sub { push @signed, $_[0] }) };
|
||||
is_deeply(\@signed, ["$to2/$base"], 'the sign callback runs on each copied rpm');
|
||||
}
|
||||
|
||||
# ---- finalize_xcat_dep: require the genesis inputs (no silent no-op) --------------------------
|
||||
{
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
make_path("$tmp/x/rh9/x86_64", "$tmp/p/rh9/ppc64le"); # a pair exists, but NO genesis rpms
|
||||
my $ok = eval { quiet { finalize_xcat_dep("$tmp/x", "$tmp/p") }; 1 };
|
||||
ok(!$ok, 'finalize dies when a repo pair has no genesis rpms (was a silent success)');
|
||||
like($@, qr/no (x86_64|ppc64) xCAT-genesis-base/,
|
||||
'finalize error names the missing genesis input');
|
||||
|
||||
my $tmp2 = tempdir(CLEANUP => 1); # no <os>/x86_64 pair at all
|
||||
make_path("$tmp2/x", "$tmp2/p");
|
||||
my $ok2 = eval { quiet { finalize_xcat_dep("$tmp2/x", "$tmp2/p") }; 1 };
|
||||
ok(!$ok2, 'finalize dies when no <os>/x86_64 + <os>/ppc64le pair is found');
|
||||
}
|
||||
|
||||
done_testing;
|
||||
Reference in New Issue
Block a user