mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-30 14:55:17 +00:00
test(openeuler): cover native dependency builds
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin qw($RealBin);
|
||||
use Test::More;
|
||||
|
||||
use lib "$RealBin/../lib", "$RealBin/lib";
|
||||
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
|
||||
use XCAT::GenesisReleaseTest qw(run_capture);
|
||||
|
||||
plan skip_all => 'Linux RPM repository tools required'
|
||||
unless $^O eq 'linux' && !grep { !command_exists($_) } qw(git rpm rpmbuild createrepo_c unshare);
|
||||
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my @namespace = $> == 0 ? () : ('unshare', '--user', '--map-root-user');
|
||||
plan skip_all => 'An unprivileged user namespace is required for the collector root check'
|
||||
if @namespace && run_capture("$tmp/namespace.log", @namespace, 'true') != 0;
|
||||
|
||||
my $collector = $ENV{XCAT_TEST_COLLECTOR} // abs_path("$RealBin/../mockbuild-all.pl");
|
||||
my $arch = capture_command('uname', '-m');
|
||||
my $epoch = 1788718796;
|
||||
my $snapshot = ('a' x 40) . '-dirty-snapshot-' . ('b' x 64);
|
||||
my $top = "$tmp/rpmbuild";
|
||||
make_path("$top/SPECS");
|
||||
write_binary("$top/SPECS/provenance-fixture.spec", <<'SPEC');
|
||||
Name: provenance-fixture
|
||||
Version: 1
|
||||
Release: 1
|
||||
Summary: Repository metadata fixture
|
||||
License: MIT
|
||||
BuildArch: noarch
|
||||
%description
|
||||
Repository metadata fixture.
|
||||
%install
|
||||
mkdir -p %{buildroot}/usr/share/provenance-fixture
|
||||
%files
|
||||
/usr/share/provenance-fixture
|
||||
SPEC
|
||||
is(run_capture("$tmp/rpm-build.log", 'rpmbuild', '--quiet', '-bb', '--define', "_topdir $top",
|
||||
"$top/SPECS/provenance-fixture.spec"), 0, 'build an isolated RPM fixture')
|
||||
or BAIL_OUT(read_binary("$tmp/rpm-build.log"));
|
||||
my $fixture = "$top/RPMS/noarch/provenance-fixture-1-1.noarch.rpm";
|
||||
my $fixture_hash = digest_file($fixture);
|
||||
|
||||
for my $case (qw(checkout export missing empty)) {
|
||||
my $root = "$tmp/$case source";
|
||||
make_path($root);
|
||||
write_binary("$root/packages-manifest.conf",
|
||||
"[openeuler-24.03sp3-$arch]\nprovenance-fixture=1\n"
|
||||
. "[alma+epel-10-$arch]\nprovenance-fixture=1\n");
|
||||
write_binary("$root/Gitepoch", "$epoch\n");
|
||||
my $expected = 'unknown';
|
||||
if ($case eq 'checkout') {
|
||||
is(run_capture("$tmp/git-init.log", 'git', '-C', $root, 'init', '--quiet'), 0,
|
||||
'initialize the real checkout fixture');
|
||||
is(run_capture("$tmp/git-add.log", 'git', '-C', $root, 'add', 'packages-manifest.conf', 'Gitepoch'), 0,
|
||||
'stage the checkout fixture');
|
||||
is(run_capture("$tmp/git-commit.log", 'git', '-C', $root,
|
||||
'-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid',
|
||||
'-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'Fixture'), 0,
|
||||
'record the checkout fixture revision');
|
||||
$expected = capture_command('git', '-C', $root, 'rev-parse', 'HEAD');
|
||||
write_binary("$root/Gitinfo", "$snapshot\n");
|
||||
} elsif ($case eq 'export') {
|
||||
write_binary("$root/Gitinfo", "$snapshot\r\n");
|
||||
$expected = $snapshot;
|
||||
} elsif ($case eq 'empty') {
|
||||
write_binary("$root/Gitinfo", " \t\r\n");
|
||||
}
|
||||
|
||||
for my $target ("openeuler-24.03sp3-$arch", "alma+epel-10-$arch") {
|
||||
my $output = "$tmp/$case-$target-output";
|
||||
my $repo = "$tmp/$case-$target-repo";
|
||||
my $log = "$tmp/$case-$target.log";
|
||||
local $ENV{MOCKBUILD_ALL_MOUNTNS} = 1;
|
||||
my $status = run_capture($log, @namespace, $^X, $collector,
|
||||
'--repo-root', $root, '--target', $target, '--output', $output,
|
||||
'--repo-dep', $repo, '--run-id', 'provenance', '--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball', '--no-verify-repo',
|
||||
'--collect-dir', "$top/RPMS/noarch");
|
||||
is($status, 0, "$case $target full collector succeeds") or diag(read_binary($log));
|
||||
my $subdir = $target =~ /^openeuler/ ? "openeuler24.03sp3/$arch" : "rh10/$arch";
|
||||
my $metadata_path = "$repo/$subdir/buildinfo.txt";
|
||||
ok(-f $metadata_path, "$case $target writes repository buildinfo");
|
||||
next unless -f $metadata_path;
|
||||
my %metadata = map { split /=/, $_, 2 } split /\n/, read_binary($metadata_path);
|
||||
is($metadata{COMMIT_ID_LONG}, $expected, "$case $target preserves the complete source identity");
|
||||
is($metadata{COMMIT_ID}, substr($expected, 0, 7), "$case $target preserves the short identity contract");
|
||||
is($metadata{SOURCE_DATE_EPOCH}, "$epoch", "$case $target retains the explicit epoch");
|
||||
is($metadata{TARGET}, $subdir, "$case $target retains the target repository path");
|
||||
is(digest_file("$repo/$subdir/provenance-fixture-1-1.noarch.rpm"), $fixture_hash,
|
||||
"$case $target collection preserves the RPM bytes");
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin qw($RealBin);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use Digest::SHA qw(sha256_hex);
|
||||
use JSON::PP qw(decode_json);
|
||||
use Test::More;
|
||||
|
||||
plan skip_all => 'Linux user namespaces are required for the unchanged root-only CLI' unless $^O eq 'linux';
|
||||
my @namespace = $> ? ('unshare', '--user', '--map-root-user', '--') : ();
|
||||
if (@namespace) {
|
||||
my $pid = fork();
|
||||
die $! unless defined $pid;
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', '/dev/null') or die $!;
|
||||
open(STDERR, '>&', \*STDOUT) or die $!;
|
||||
exec(@namespace, $^X, '-e', 'exit($> != 0)') or die $!;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
plan skip_all => 'Unprivileged user namespaces are unavailable' if $?;
|
||||
}
|
||||
|
||||
my $root = "$RealBin/..";
|
||||
my $builder = $ENV{XCAT_TEST_GO_BUILDER} || "$root/goconserver/mockbuild.pl";
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my $commit = '0123456789abcdef0123456789abcdef01234567';
|
||||
my $payload = "private compiler download fixture\n";
|
||||
my $hash = sha256_hex($payload);
|
||||
my $sequence = 0;
|
||||
|
||||
sub write_file {
|
||||
my ($path, $text) = @_;
|
||||
make_path(dirname($path));
|
||||
open(my $fh, '>', $path) or die "$path: $!";
|
||||
print {$fh} $text;
|
||||
close($fh) or die "$path: $!";
|
||||
}
|
||||
|
||||
sub read_file {
|
||||
my ($path) = @_;
|
||||
return '' unless -f $path;
|
||||
open(my $fh, '<', $path) or die "$path: $!";
|
||||
my $text = do { local $/; <$fh> };
|
||||
close($fh) or die "$path: $!";
|
||||
return $text // '';
|
||||
}
|
||||
|
||||
my $double = <<'DOUBLE';
|
||||
#!/usr/bin/perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use File::Basename qw(basename dirname);
|
||||
use File::Path qw(make_path);
|
||||
use JSON::PP qw(encode_json);
|
||||
my $command = basename($0);
|
||||
open(my $log, '>>', $ENV{FIXTURE_LOG}) or die $!;
|
||||
print {$log} encode_json({command => $command, args => [@ARGV], goarch => $ENV{GOARCH} // ''}), "\n";
|
||||
close($log) or die $!;
|
||||
sub put {
|
||||
my ($path, $text) = @_;
|
||||
make_path(dirname($path));
|
||||
open(my $fh, '>', $path) or die $!;
|
||||
print {$fh} $text;
|
||||
close($fh) or die $!;
|
||||
}
|
||||
sub option {
|
||||
my ($name) = @_;
|
||||
for my $i (0 .. $#ARGV - 1) { return $ARGV[$i + 1] if $ARGV[$i] eq $name; }
|
||||
die "Missing option $name";
|
||||
}
|
||||
if ($command eq 'uname') {
|
||||
die 'Unexpected uname arguments' unless "@ARGV" eq '-m';
|
||||
print "$ENV{FIXTURE_ARCH}\n";
|
||||
} elsif ($command eq 'bash') {
|
||||
die 'Unexpected bash invocation' unless @ARGV == 2 && $ARGV[0] eq '-lc';
|
||||
if ($ARGV[1] eq 'source /etc/os-release; echo $ID') {
|
||||
print "$ENV{FIXTURE_OS}\n";
|
||||
} elsif ($ARGV[1] eq 'source /etc/os-release; echo "$VERSION"') {
|
||||
print "$ENV{FIXTURE_VERSION}\n";
|
||||
} else { die "Unexpected OS query: $ARGV[1]"; }
|
||||
} elsif ($command eq 'git') {
|
||||
if ($ARGV[0] eq 'init') {
|
||||
my $path = $ARGV[-1];
|
||||
make_path("$path/.git");
|
||||
put("$path/goconserver.go", "package main\n");
|
||||
put("$path/cmd/congo.go", "package main\n");
|
||||
put("$path/storage/etcd.go", "package storage\n");
|
||||
} elsif ($ARGV[0] eq '-C' && $ARGV[2] eq 'rev-parse') {
|
||||
die 'Commit queried after .git removal' unless -d "$ARGV[1]/.git";
|
||||
die 'Unexpected commit query' unless "@ARGV[3 .. $#ARGV]" eq '--verify HEAD^{commit}';
|
||||
print "$ENV{FIXTURE_COMMIT}\n";
|
||||
exit($ENV{FIXTURE_COMMIT_RC} || 0);
|
||||
} elsif ($ARGV[0] eq '-C' && $ARGV[2] eq 'log') {
|
||||
print "1600000000\n";
|
||||
} elsif ($ARGV[0] eq '-C' && $ARGV[2] =~ /\A(?:remote|fetch|checkout)\z/) {
|
||||
exit 0;
|
||||
} else { die "Unexpected git arguments: @ARGV"; }
|
||||
} elsif ($command eq 'wget') {
|
||||
put(option('-O'), $ENV{FIXTURE_DOWNLOAD});
|
||||
} elsif ($command eq 'mock') {
|
||||
if (grep { $_ eq '--buildsrpm' } @ARGV) {
|
||||
put(option('--resultdir') . '/goconserver-0.3.3-4.src.rpm', "fixture source RPM\n");
|
||||
} elsif (grep { $_ eq '--rebuild' } @ARGV) {
|
||||
put(option('--resultdir') . "/goconserver-0.3.3-4.$ENV{FIXTURE_TARGET}.rpm", "fixture binary RPM\n");
|
||||
} elsif (grep { $_ eq '--print-root-path' || /^--scrub=/ } @ARGV) {
|
||||
print "/private/mock/root\n";
|
||||
} else { die "Unexpected mock arguments: @ARGV"; }
|
||||
} elsif ($command eq 'go') {
|
||||
die 'Unexpected go invocation' unless $ARGV[0] eq 'build';
|
||||
my $output = option('-o');
|
||||
put($output, "#!/bin/sh\nexit 0\n");
|
||||
chmod 0755, $output;
|
||||
} elsif ($command eq 'rpmbuild') {
|
||||
my ($top) = map { /^_topdir (.+)$/ ? $1 : () } @ARGV;
|
||||
die 'Missing rpmbuild topdir' unless defined $top;
|
||||
my $arch = option('--target');
|
||||
put("$top/RPMS/$arch/goconserver-0.3.3-4.$arch.rpm", "fixture binary RPM\n");
|
||||
put("$top/SRPMS/goconserver-0.3.3-4.src.rpm", "fixture source RPM\n");
|
||||
} elsif ($command eq 'cpio') {
|
||||
for my $name (qw(goconserver congo)) {
|
||||
put("usr/bin/$name", "#!/bin/sh\nexit 0\n");
|
||||
chmod 0755, "usr/bin/$name";
|
||||
}
|
||||
} elsif ($command ne 'rpm' && $command ne 'rpm2cpio') {
|
||||
die "Unexpected command $command";
|
||||
}
|
||||
DOUBLE
|
||||
|
||||
sub run_case {
|
||||
my (%options) = @_;
|
||||
my $directory = "$tmp/" . ++$sequence;
|
||||
my $checkout = "$directory/source";
|
||||
my $bin = "$directory/bin";
|
||||
make_path($checkout, $bin);
|
||||
for my $relative ('MockBuildUtils.pm', 'lib/XCAT/BuildUtils.pm', 'goconserver/gomod/go.mod', 'goconserver/gomod/go.sum') {
|
||||
make_path(dirname("$checkout/$relative"));
|
||||
copy("$root/$relative", "$checkout/$relative") or die $!;
|
||||
}
|
||||
copy($builder, "$checkout/goconserver/mockbuild.pl") or die $!;
|
||||
write_file("$checkout/goconserver/toolchains/go1.25.12.sha256",
|
||||
join('', map { "$hash go1.25.12.linux-$_.tar.gz\n" } qw(amd64 ppc64le)));
|
||||
write_file("$bin/double", $double);
|
||||
chmod 0755, "$bin/double";
|
||||
symlink('double', "$bin/$_") or die $! for qw(uname bash git wget mock rpm go rpmbuild rpm2cpio cpio);
|
||||
my @arguments = ('--work-dir', "$directory/work", '--result-dir', "$directory/results",
|
||||
'--log-dir', "$directory/logs", '--mock-uniqueext', 'contract', '--go-ref', 'refs/tags/fixture');
|
||||
push @arguments, ('--build-timestamp', $options{epoch} // 1700000000) unless $options{omit_epoch};
|
||||
push @arguments, ('--mock-cfg', $options{config}) if defined $options{config};
|
||||
push @arguments, ('--target-arch', $options{target}) if defined $options{target};
|
||||
local $ENV{PATH} = "$bin:/usr/bin:/bin";
|
||||
local $ENV{TZ} = 'Pacific/Honolulu';
|
||||
local $ENV{SOURCE_DATE_EPOCH};
|
||||
delete $ENV{SOURCE_DATE_EPOCH};
|
||||
$ENV{SOURCE_DATE_EPOCH} = $options{environment_epoch} if exists $options{environment_epoch};
|
||||
local $ENV{FIXTURE_LOG} = "$directory/commands.jsonl";
|
||||
local $ENV{FIXTURE_ARCH} = $options{arch} || 'x86_64';
|
||||
local $ENV{FIXTURE_TARGET} = $options{target} || $ENV{FIXTURE_ARCH};
|
||||
local $ENV{FIXTURE_OS} = $options{os} || 'openEuler';
|
||||
local $ENV{FIXTURE_VERSION} = $options{os_version} || '24.03 (LTS-SP3)';
|
||||
local $ENV{FIXTURE_DOWNLOAD} = $options{corrupt} ? "corrupted payload\n" : $payload;
|
||||
local $ENV{FIXTURE_COMMIT} = exists($options{commit}) ? $options{commit} : $commit;
|
||||
local $ENV{FIXTURE_COMMIT_RC} = $options{commit_rc} || 0;
|
||||
my $pid = fork();
|
||||
die $! unless defined $pid;
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', "$directory/output") or die $!;
|
||||
open(STDERR, '>&', \*STDOUT) or die $!;
|
||||
exec(@namespace, $^X, "$checkout/goconserver/mockbuild.pl", @arguments) or die $!;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $status = $?;
|
||||
my @commands = map { decode_json($_) } grep { length } split /\n/, read_file("$directory/commands.jsonl");
|
||||
return { directory => $directory, status => $status, output => read_file("$directory/output"),
|
||||
spec => read_file("$directory/work/goconserver.spec"), commands => \@commands };
|
||||
}
|
||||
|
||||
sub calls {
|
||||
my ($case, $command, $argument) = @_;
|
||||
return [grep { $_->{command} eq $command && (!defined($argument) || grep { $_ eq $argument } @{$_->{args}}) } @{$case->{commands}}];
|
||||
}
|
||||
|
||||
sub build_metadata {
|
||||
my ($case, $time, $label) = @_;
|
||||
for my $binary (qw(goconserver congo)) {
|
||||
like($case->{spec}, qr/^go build [^\n]*-ldflags "-X main.Version=%\{version\} -X main.Commit=\Q$commit\E -X main.BuildTime=\Q$time\E" -o \Q$binary\E /m,
|
||||
"$label $binary records fetched commit and UTC build time");
|
||||
}
|
||||
}
|
||||
|
||||
my @cells = (
|
||||
['20.03sp4', '20.03 (LTS-SP4)', 'x86_64', 'amd64'],
|
||||
['22.03sp4', '22.03 (LTS-SP4)', 'x86_64', 'amd64'],
|
||||
['24.03sp1', '24.03 (LTS-SP1)', 'x86_64', 'amd64'],
|
||||
['24.03sp3', '24.03 (LTS-SP3)', 'x86_64', 'amd64'],
|
||||
['24.03sp4', '24.03 (LTS-SP4)', 'x86_64', 'amd64'],
|
||||
['24.03', '24.03 (LTS)', 'ppc64le', 'ppc64le'],
|
||||
);
|
||||
for my $cell (@cells) {
|
||||
my ($version, $os_version, $arch, $goarch) = @$cell;
|
||||
my $config = "openeuler-$version-$arch";
|
||||
my $case = run_case(os_version => $os_version, arch => $arch);
|
||||
is($case->{status}, 0, "$config full CLI succeeds with external build doubles") or diag($case->{output});
|
||||
like(read_file("$case->{directory}/work/mock-deterministic.cfg"), qr/^include\('\/etc\/mock\/\Q$config\E\.cfg'\)/m,
|
||||
"$config builds inside its exact native config");
|
||||
like($case->{spec}, qr/^Release:\s+4$/m, "$config retains the native empty dist macro");
|
||||
like($case->{spec}, qr/^BuildArch:\s+\Q$arch\E$/m, "$config retains its native architecture");
|
||||
like($case->{spec}, qr/^Source3:\s+https:\/\/go\.dev\/dl\/go1\.25\.12\.linux-\Q$goarch\E\.tar\.gz$/m,
|
||||
"$config stages the matching pinned compiler");
|
||||
like($case->{spec}, qr/^BuildRequires:\s+coreutils tar gzip ca-certificates$/m, "$config uses the private compiler prerequisites");
|
||||
like($case->{spec}, qr/^echo '\Q$hash\E %\{SOURCE3\}' \| sha256sum -c -$/m, "$config verifies the compiler again in RPM prep");
|
||||
is(scalar @{calls($case, 'mock', '--buildsrpm')}, 1, "$config reaches SRPM construction after verification");
|
||||
is(scalar @{calls($case, 'mock', '--rebuild')}, 1, "$config reaches native RPM reconstruction");
|
||||
is(read_file("$case->{directory}/results/goconserver-0.3.3-4.$arch.rpm"), "fixture binary RPM\n", "$config collects the build output");
|
||||
ok(!-d "$case->{directory}/work/goconserver-src/.git", "$config removes fetched Git metadata from the sources");
|
||||
build_metadata($case, '2023-11-14T22:13:20Z', $config);
|
||||
}
|
||||
|
||||
{
|
||||
my $case = run_case(config => 'openeuler-22.03sp4-x86_64');
|
||||
is($case->{status}, 0, 'explicit native target overrides host release detection');
|
||||
is(scalar @{calls($case, 'bash')}, 0, 'explicit target requires no host release query');
|
||||
like(read_file("$case->{directory}/work/mock-deterministic.cfg"), qr/openeuler-22\.03sp4-x86_64\.cfg/, 'explicit service pack is retained');
|
||||
}
|
||||
|
||||
for my $options (
|
||||
{ config => 'openeuler-24.03sp3-x86_64', target => 'ppc64le' },
|
||||
{ config => 'openeuler-24.03-ppc64le', arch => 'x86_64' },
|
||||
{ config => 'openeuler-24.03-ppc64le', arch => 'ppc64le', target => 'x86_64' },
|
||||
) {
|
||||
my $case = run_case(%$options);
|
||||
isnt($case->{status}, 0, 'native target rejects a foreign builder or cross target');
|
||||
like($case->{output}, qr/openEuler goconserver requires a native .* builder/, 'native mismatch reports its required builder');
|
||||
ok(!-d "$case->{directory}/work", 'native mismatch fails before staging sources');
|
||||
is(scalar @{calls($case, 'git')} + scalar @{calls($case, 'mock')} + scalar @{calls($case, 'wget')}, 0,
|
||||
'native mismatch starts no source fetch, toolchain fetch, or package build');
|
||||
}
|
||||
|
||||
for my $arch (qw(x86_64 ppc64le)) {
|
||||
my $config = $arch eq 'x86_64' ? 'openeuler-24.03sp3-x86_64' : 'openeuler-24.03-ppc64le';
|
||||
my $case = run_case(arch => $arch, config => $config, corrupt => 1);
|
||||
isnt($case->{status}, 0, "$arch corrupt compiler fails");
|
||||
like($case->{output}, qr/Go toolchain checksum mismatch:/, "$arch reports compiler checksum mismatch");
|
||||
is(scalar @{calls($case, 'mock', '--buildsrpm')}, 0, "$arch corrupt compiler fails before SRPM construction");
|
||||
ok(!-f "$case->{directory}/work/goconserver.spec", "$arch corrupt compiler leaves no generated spec");
|
||||
}
|
||||
|
||||
for my $options ({ commit => '' }, { commit => 'not-a-commit' }, { commit_rc => 1 }) {
|
||||
my $case = run_case(%$options);
|
||||
isnt($case->{status}, 0, 'unresolved fetched commit fails');
|
||||
like($case->{output}, qr/Cannot resolve fetched goconserver commit/, 'unresolved commit has a specific diagnostic');
|
||||
is(scalar @{calls($case, 'wget')} + scalar @{calls($case, 'mock')}, 0, 'unresolved commit fails before compiler or package work');
|
||||
}
|
||||
|
||||
for my $row (
|
||||
[{ omit_epoch => 1, environment_epoch => 946684800 }, '2000-01-01T00:00:00Z', 'native environment epoch'],
|
||||
[{ environment_epoch => 946684800 }, '2023-11-14T22:13:20Z', 'explicit epoch precedence'],
|
||||
[{ epoch => 0 }, '1970-01-01T00:00:00Z', 'zero epoch'],
|
||||
) {
|
||||
my ($options, $time, $label) = @$row;
|
||||
my $case = run_case(%$options);
|
||||
is($case->{status}, 0, "$label succeeds") or diag($case->{output});
|
||||
build_metadata($case, $time, $label);
|
||||
}
|
||||
for my $options ({ omit_epoch => 1, environment_epoch => 'invalid' }, { epoch => -1 }) {
|
||||
my $case = run_case(%$options);
|
||||
isnt($case->{status}, 0, 'invalid native epoch fails');
|
||||
like($case->{output}, qr/Invalid native build timestamp:/, 'invalid native epoch has a specific diagnostic');
|
||||
ok(!-d "$case->{directory}/work", 'invalid native epoch fails before source staging');
|
||||
}
|
||||
|
||||
for my $row (
|
||||
[{ config => 'rocky+epel-9-x86_64' }, 'rocky+epel-10-x86_64', '9'],
|
||||
[{ os => 'rocky' }, 'rocky+epel-10-x86_64', '10'],
|
||||
) {
|
||||
my ($options, $config, $release) = @$row;
|
||||
my $case = run_case(%$options);
|
||||
is($case->{status}, 0, "EL$release full CLI succeeds") or diag($case->{output});
|
||||
like(read_file("$case->{directory}/work/mock-deterministic.cfg"), qr/\Q$config\E\.cfg/, "EL$release retains the EL10 build peer");
|
||||
like($case->{spec}, qr/^Release:\s+4\.el\Q$release\E$/m, "EL$release retains its target dist suffix");
|
||||
like($case->{spec}, qr/^BuildRequires:\s+golang$/m, "EL$release retains the distro compiler");
|
||||
unlike($case->{spec}, qr/^Source3:/m, "EL$release has no native compiler source");
|
||||
is(scalar @{calls($case, 'wget')} + scalar @{calls($case, 'git', 'rev-parse')}, 0, "EL$release adds no native source or metadata fetch");
|
||||
for my $binary (qw(goconserver congo)) {
|
||||
like($case->{spec}, qr/^go build [^\n]*-ldflags "-X main.Version=%\{version\}" -o \Q$binary\E /m,
|
||||
"EL$release $binary preserves its existing linker flags");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
my $case = run_case(config => 'rocky-10-riscv64-xcat', target => 'riscv64');
|
||||
is($case->{status}, 0, 'existing EL cross packaging completes with external build doubles') or diag($case->{output});
|
||||
my $go = calls($case, 'go');
|
||||
is(scalar @$go, 2, 'EL cross path invokes both host compiler outputs');
|
||||
is_deeply([map { $_->{goarch} } @$go], ['riscv64', 'riscv64'], 'EL cross path selects the target GOARCH');
|
||||
is(scalar @{calls($case, 'rpmbuild', 'riscv64')}, 1, 'EL cross path packages for the requested target');
|
||||
is(scalar @{calls($case, 'mock')} + scalar @{calls($case, 'wget')}, 0, 'EL cross path does not invoke native mock or compiler staging');
|
||||
is(read_file("$case->{directory}/results/goconserver-0.3.3-4.riscv64.rpm"), "fixture binary RPM\n", 'EL cross path collects its package output');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,338 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin qw($RealBin);
|
||||
use JSON::PP;
|
||||
use Test::More;
|
||||
|
||||
use lib "$RealBin/..", "$RealBin/../lib", "$RealBin/lib";
|
||||
use MockBuildUtils qw(read_manifest);
|
||||
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
|
||||
use XCAT::GenesisReleaseTest qw(run_capture dies_like);
|
||||
use XCAT::NativeInputs qw(load_inputs stage_inputs publisher_trust verify_input validate_outputs);
|
||||
|
||||
plan skip_all => 'Native Linux RPM tools are required' unless $^O eq 'linux'
|
||||
&& !grep { !command_exists($_) } qw(rpm rpmkeys rpmbuild rpmsign gpg gpgconf createrepo_c unshare python3);
|
||||
my $build_user = $ENV{XCAT_TEST_BUILD_USER} // '';
|
||||
plan skip_all => 'Set XCAT_TEST_BUILD_USER to an unprivileged fixture builder' if $> == 0 && !$build_user;
|
||||
my $build_uid = $> == 0 ? getpwnam($build_user) : $>;
|
||||
plan skip_all => 'The fixture builder must be unprivileged' unless defined($build_uid) && $build_uid != 0;
|
||||
my @rpm_user = $> == 0 ? ('runuser', '-u', $build_user, '--') : ();
|
||||
my $tmp = tempdir(CLEANUP => !$ENV{XCAT_TEST_KEEP});
|
||||
diag("native input fixtures: $tmp");
|
||||
my $repo = abs_path("$RealBin/..");
|
||||
my $owner = $ENV{XCAT_TEST_COLLECTOR} // "$repo/mockbuild-all.pl";
|
||||
my $target = 'openeuler-24.03-ppc64le';
|
||||
my $json = JSON::PP->new->canonical->pretty;
|
||||
my $epoch = 1788718796;
|
||||
my $host_arch = capture_command('uname', '-m');
|
||||
my %manifest = read_manifest("$repo/packages-manifest.conf");
|
||||
my $production_plan = eval { load_inputs($repo, $manifest{$target}); };
|
||||
ok($production_plan, 'the shipped full POWER manifest has an executable native input plan') or BAIL_OUT($@);
|
||||
is($production_plan->{nodes}{'xCAT-genesis-base'}{build_uid}, 0, 'the shipped Genesis owner declares its root assembly exception');
|
||||
my %homes;
|
||||
my %keys;
|
||||
make_path("$tmp/bin", "$tmp/rpmbuild/SPECS");
|
||||
chmod 0755, $tmp;
|
||||
chown $build_uid, -1, "$tmp/rpmbuild", "$tmp/rpmbuild/SPECS" if $> == 0;
|
||||
for my $key (qw(publisher build foreign)) {
|
||||
my $home = "$tmp/key-$key";
|
||||
$homes{$key} = $home;
|
||||
make_path($home);
|
||||
chmod 0700, $home;
|
||||
is(run_capture("$tmp/key-$key.log", 'gpg', '--homedir', $home, '--batch', '--pinentry-mode', 'loopback',
|
||||
'--passphrase', '', '--quick-generate-key', "$key\@example.invalid", 'rsa2048', 'sign', '0'), 0,
|
||||
"create private $key key") or BAIL_OUT(read_binary("$tmp/key-$key.log"));
|
||||
my $listing = capture_command('gpg', '--homedir', $home, '--with-colons', '--list-keys');
|
||||
($keys{$key}) = $listing =~ /^fpr:::::::::([0-9A-F]+):/m;
|
||||
write_binary("$home/public.asc", capture_command('gpg', '--homedir', $home, '--armor', '--export', $keys{$key}));
|
||||
}
|
||||
END {
|
||||
for my $home (values %homes) {
|
||||
run_capture("$home/cleanup.log", 'gpgconf', '--homedir', $home, '--kill', 'gpg-agent') if -d $home;
|
||||
}
|
||||
}
|
||||
|
||||
my %rpm;
|
||||
for my $name (qw(native-leaf native-child publisher-package publisher-elf publisher-arch)) {
|
||||
my $arch = $name eq 'publisher-arch' ? $host_arch : 'noarch';
|
||||
my $payload = $name eq 'publisher-elf' ? q{printf '\177ELFfixture\n'} : q{printf 'fixture\n'};
|
||||
my $spec = <<'SPEC';
|
||||
Name: NAME
|
||||
Version: 1
|
||||
Release: 1.oe2403
|
||||
Summary: Native input contract fixture
|
||||
License: MIT
|
||||
BuildArch: ARCH
|
||||
%description
|
||||
Native input contract fixture.
|
||||
%install
|
||||
mkdir -p %{buildroot}/usr/share/native-inputs
|
||||
PAYLOAD > %{buildroot}/usr/share/native-inputs/%{name}
|
||||
%check
|
||||
test "$(id -u)" -ne 0
|
||||
%files
|
||||
/usr/share/native-inputs/%{name}
|
||||
SPEC
|
||||
$spec =~ s/NAME/$name/;
|
||||
$spec =~ s/ARCH/$arch/;
|
||||
$spec =~ s/PAYLOAD/$payload/;
|
||||
write_binary("$tmp/rpmbuild/SPECS/$name.spec", $spec);
|
||||
is(run_capture("$tmp/fixture-$name.log", @rpm_user, 'rpmbuild', '-ba', '--define', "_topdir $tmp/rpmbuild",
|
||||
"$tmp/rpmbuild/SPECS/$name.spec"), 0, "build real $name fixture with nonroot check")
|
||||
or BAIL_OUT(read_binary("$tmp/fixture-$name.log"));
|
||||
$rpm{$name} = "$tmp/rpmbuild/RPMS/$arch/$name-1-1.oe2403.$arch.rpm";
|
||||
$rpm{"$name-src"} = "$tmp/rpmbuild/SRPMS/$name-1-1.oe2403.src.rpm";
|
||||
}
|
||||
|
||||
sub signed_copy {
|
||||
my ($source, $name, $key) = @_;
|
||||
my $dest = "$tmp/$name.rpm";
|
||||
copy($source, $dest) or die $!;
|
||||
local $ENV{GNUPGHOME} = $homes{$key};
|
||||
is(run_capture("$tmp/sign-$name.log", 'rpmsign', '--define', "_gpg_name $keys{$key}",
|
||||
'--define', '__gpg /usr/bin/gpg', '--addsign', $dest), 0, "sign $name with $key key")
|
||||
or BAIL_OUT(read_binary("$tmp/sign-$name.log"));
|
||||
return $dest;
|
||||
}
|
||||
my %signed;
|
||||
for my $name (qw(native-leaf-src native-child-src publisher-package publisher-elf publisher-arch)) {
|
||||
$signed{$name} = signed_copy($rpm{$name}, "signed-$name", 'publisher');
|
||||
}
|
||||
my $foreign = signed_copy($rpm{'native-leaf-src'}, 'foreign-source', 'foreign');
|
||||
|
||||
write_binary("$tmp/bin/wget", <<'PY');
|
||||
#!/usr/bin/python3
|
||||
import json, os, pathlib, shutil, sys
|
||||
args = sys.argv[1:]
|
||||
source = json.loads(pathlib.Path(os.environ['NATIVE_DOWNLOADS']).read_text())[args[-1]]
|
||||
with open(os.environ['NATIVE_CALLS'], 'a') as f: f.write(json.dumps({'wget': args[-1]}) + '\n')
|
||||
shutil.copyfile(source, args[args.index('-O')+1])
|
||||
PY
|
||||
write_binary("$tmp/bin/mock", <<'PY');
|
||||
#!/usr/bin/python3
|
||||
import json, os, pathlib, shutil, subprocess, sys
|
||||
args = sys.argv[1:]
|
||||
call = {'mock': args}
|
||||
def value(name): return args[args.index(name)+1]
|
||||
if '--rebuild' in args or '--buildsrpm' in args:
|
||||
loader = '''import json, pathlib, sys, mockbuild
|
||||
from mockbuild.util import load_config
|
||||
config = load_config('/etc/mock', sys.argv[1], None, 'native-contract', str(pathlib.Path(mockbuild.__file__).parent))
|
||||
print(json.dumps({'uid': config['chrootuid'], 'dnf': config['dnf.conf']}))
|
||||
'''
|
||||
config = subprocess.run([sys.executable, '-c', loader, value('-r')],
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
call['config_rc'] = config.returncode
|
||||
if config.returncode:
|
||||
print(config.stdout)
|
||||
sys.exit(config.returncode)
|
||||
if '--rebuild' in args:
|
||||
name = pathlib.Path(value('--rebuild')).name.split('-1-1.oe2403')[0]
|
||||
call['name'] = name
|
||||
if '--buildsrpm' in args:
|
||||
call['spec'] = pathlib.Path(value('--spec')).read_text()
|
||||
with open(os.environ['NATIVE_CALLS'], 'a') as f: f.write(json.dumps(call) + '\n')
|
||||
if '--buildsrpm' in args:
|
||||
dest = pathlib.Path(value('--resultdir')); dest.mkdir(parents=True, exist_ok=True)
|
||||
source = json.loads(pathlib.Path(os.environ['NATIVE_OUTPUTS']).read_text())['native-leaf'][1]
|
||||
shutil.copyfile(source, dest / pathlib.Path(source).name)
|
||||
sys.exit(0)
|
||||
if '--rebuild' not in args: sys.exit(0)
|
||||
if name == os.environ.get('NATIVE_FAIL'): sys.exit(42)
|
||||
dest = pathlib.Path(value('--resultdir')); dest.mkdir(parents=True, exist_ok=True)
|
||||
if name == os.environ.get('NATIVE_EMPTY'): sys.exit(0)
|
||||
fixtures = json.loads(pathlib.Path(os.environ['NATIVE_OUTPUTS']).read_text())
|
||||
for source in fixtures[name]: shutil.copyfile(source, dest / pathlib.Path(source).name)
|
||||
PY
|
||||
chmod 0755, "$tmp/bin/wget", "$tmp/bin/mock";
|
||||
local $ENV{PATH} = "$tmp/bin:$ENV{PATH}";
|
||||
local $ENV{NATIVE_DOWNLOADS} = "$tmp/downloads.json";
|
||||
local $ENV{NATIVE_OUTPUTS} = "$tmp/outputs.json";
|
||||
local $ENV{NATIVE_CALLS} = "$tmp/calls.jsonl";
|
||||
write_binary($ENV{NATIVE_OUTPUTS}, $json->encode({map { $_ => [$rpm{$_}, $rpm{"$_-src"}] } qw(native-leaf native-child)}));
|
||||
|
||||
sub catalog {
|
||||
return {version => 1, target => $target, publisher_key => {
|
||||
path => 'openeuler/publisher.asc', sha256 => digest_file("$homes{publisher}/public.asc"),
|
||||
fingerprint => $keys{publisher}}, build_inputs => [], inputs => [
|
||||
{name => 'native-leaf', type => 'srpm', build_uid => 1000, needs => [], outputs => ['native-leaf'],
|
||||
url => 'https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/native-leaf-1-1.oe2403.src.rpm',
|
||||
sha256 => digest_file($signed{'native-leaf-src'})},
|
||||
{name => 'native-child', type => 'srpm', build_uid => 1000, needs => ['native-leaf'], outputs => ['native-child'],
|
||||
url => 'https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/native-child-1-1.oe2403.src.rpm',
|
||||
sha256 => digest_file($signed{'native-child-src'})},
|
||||
{name => 'publisher-package', type => 'publisher', needs => [], outputs => ['publisher-package'],
|
||||
url => 'https://repo.openeuler.org/openEuler-24.03-LTS/Everything/x86_64/Packages/publisher-package-1-1.oe2403.noarch.rpm',
|
||||
sha256 => digest_file($signed{'publisher-package'})}]};
|
||||
}
|
||||
|
||||
sub prepare {
|
||||
my ($name, $mutate) = @_;
|
||||
my $root = "$tmp/$name source";
|
||||
make_path("$root/openeuler", "$root/mock-configs/templates");
|
||||
my $data = catalog();
|
||||
$mutate->($data) if $mutate;
|
||||
copy("$homes{publisher}/public.asc", "$root/openeuler/publisher.asc") or die $!;
|
||||
write_binary("$root/openeuler/24.03-ppc64le.inputs.json", $json->encode($data));
|
||||
write_binary("$root/packages-manifest.conf", "[$target]\nnative-child=1\npublisher-package=1\n");
|
||||
write_binary("$root/Gitepoch", "$epoch\n");
|
||||
write_binary("$root/Gitinfo", ('a' x 40) . "\n");
|
||||
write_binary("$root/mock-configs/$target.cfg", "config_opts['root'] = 'native-contract'\nconfig_opts['dnf.conf'] = ''\n");
|
||||
my %downloads = map { $_->{url} => $signed{$_->{name} . ($_->{type} eq 'srpm' ? '-src' : '')} } @{$data->{inputs}};
|
||||
write_binary($ENV{NATIVE_DOWNLOADS}, $json->encode(\%downloads));
|
||||
unlink $ENV{NATIVE_CALLS};
|
||||
return ($root, $data);
|
||||
}
|
||||
|
||||
my ($valid) = prepare('valid');
|
||||
my $plan = load_inputs($valid, {'native-child' => '1', 'publisher-package' => '1'});
|
||||
is_deeply($plan->{order}, [qw(native-leaf native-child publisher-package)], 'public plan orders prerequisites before consumers');
|
||||
stage_inputs($plan, "$tmp/valid-stage");
|
||||
is(digest_file($plan->{nodes}{'publisher-package'}{staged}), digest_file($signed{'publisher-package'}), 'publisher admission preserves signed bytes');
|
||||
is(digest_file($plan->{nodes}{'native-leaf'}{staged}), digest_file($signed{'native-leaf-src'}), 'source admission preserves signed bytes');
|
||||
ok(-f "$tmp/valid-stage/inputs.json", 'admission records input identity and catalog digest');
|
||||
validate_outputs($plan->{nodes}{'native-leaf'}, [$rpm{'native-leaf'}], 1);
|
||||
pass('declared real native output passes ownership validation');
|
||||
dies_like(sub { validate_outputs($plan->{nodes}{'native-leaf'}, [$rpm{'native-child'}], 1) }, qr/Unexpected output/, 'wrong owner output fails');
|
||||
dies_like(sub { validate_outputs($plan->{nodes}{'native-leaf'}, [$rpm{'native-leaf'}, $rpm{'native-leaf'}], 1) }, qr/Duplicate output/, 'duplicate output fails');
|
||||
dies_like(sub { validate_outputs($plan->{nodes}{'native-leaf'}, [], 1) }, qr/Missing output/, 'empty successful build fails');
|
||||
|
||||
my @bad = (
|
||||
['cycle', sub { $_[0]{inputs}[0]{needs} = ['native-child'] }, qr/Cyclic native dependency/],
|
||||
['missing', sub { $_[0]{inputs}[0]{needs} = ['absent'] }, qr/Missing native dependency/],
|
||||
['conflict', sub { $_[0]{inputs}[1]{outputs} = ['native-leaf'] }, qr/Conflicting output ownership/],
|
||||
['uid', sub { $_[0]{inputs}[0]{build_uid} = 0 }, qr/Invalid native build UID/],
|
||||
['foreign-release', sub { $_[0]{inputs}[2]{url} =~ s/LTS\//LTS-SP3\// }, qr/Publisher binary must be exact GA/],
|
||||
['unsafe-define', sub { $_[0]{inputs}[0]{defines} = ['llvmjit 0; touch injected'] }, qr/Invalid native spec definition/],
|
||||
['missing-patch', sub { $_[0]{inputs}[0]{patches} = [{path => 'absent.patch', sha256 => 'a' x 64}] }, qr/Missing input/],
|
||||
['source-needs-owner', sub {
|
||||
push @{$_[0]{inputs}}, {name => 'goconserver', type => 'owner', build_uid => 1000,
|
||||
outputs => ['goconserver'], needs => []};
|
||||
$_[0]{inputs}[0]{needs} = ['goconserver'];
|
||||
}, qr/Unsupported native execution edge/],
|
||||
['owner-needs-owner', sub {
|
||||
push @{$_[0]{inputs}}, {name => 'goconserver', type => 'owner', build_uid => 1000,
|
||||
outputs => ['goconserver'], needs => ['ipmitool-xcat']},
|
||||
{name => 'ipmitool-xcat', type => 'owner', build_uid => 1000, outputs => ['ipmitool-xcat'], needs => []};
|
||||
}, qr/Unsupported native execution edge/],
|
||||
);
|
||||
for my $case (@bad) {
|
||||
my ($root) = prepare($case->[0], $case->[1]);
|
||||
dies_like(sub { load_inputs($root, {'native-child' => '1'}) }, $case->[2], "$case->[0] fails before input acquisition");
|
||||
ok(!-f $ENV{NATIVE_CALLS}, "$case->[0] executes no downloader or builder");
|
||||
}
|
||||
|
||||
for my $case (
|
||||
['bad-hash', $signed{'native-leaf-src'}, 'b' x 64, qr/SHA256 mismatch/],
|
||||
['unsigned', $rpm{'native-leaf-src'}, digest_file($rpm{'native-leaf-src'}), qr/Publisher signature missing/],
|
||||
['wrong-key', $foreign, digest_file($foreign), qr/Command failed/],
|
||||
) {
|
||||
my %node = %{$plan->{nodes}{'native-leaf'}};
|
||||
$node{sha256} = $case->[2];
|
||||
dies_like(sub { verify_input($plan, \%node, $case->[1], $plan->{trust_db}) }, $case->[3], "$case->[0] cannot enter a native root");
|
||||
}
|
||||
for my $case (['publisher-elf', qr/ELF payload/], ['publisher-arch', qr/not a noarch binary/]) {
|
||||
my %node = (%{$plan->{nodes}{'publisher-package'}}, name => $case->[0], sha256 => digest_file($signed{$case->[0]}));
|
||||
dies_like(sub { verify_input($plan, \%node, $signed{$case->[0]}, $plan->{trust_db}) }, $case->[1], "$case->[0] is rejected using the real RPM payload/header");
|
||||
}
|
||||
|
||||
my @namespace = ('unshare', ($> == 0 ? () : ('--user', '--map-root-user')), '--mount', '--propagation', 'private');
|
||||
my $can_owner = $host_arch eq 'ppc64le'
|
||||
&& run_capture("$tmp/mock-loader.log", 'python3', '-c', 'from mockbuild.util import load_config') == 0
|
||||
&& run_capture("$tmp/namespace.log", @namespace, 'true') == 0;
|
||||
SKIP: {
|
||||
skip 'Whole native owner requires POWER, native Mock and a private mount namespace', 52 unless $can_owner;
|
||||
for my $case (@bad[0..2, 7, 8]) {
|
||||
my ($root) = prepare("owner-$case->[0]", $case->[1]);
|
||||
my $rc = run_capture("$tmp/owner-$case->[0].log", @namespace,
|
||||
$^X, $owner, '--repo-root', $root, '--target', $target, '--xcat-source', $root,
|
||||
'--output', "$tmp/owner-$case->[0]-output", '--skip-genesis', '--skip-tarball', '--gpg-sign',
|
||||
'--gpg-home', $homes{build}, '--gpg-key-name', $keys{build}, '--scrub-all-chroots');
|
||||
isnt($rc, 0, "$case->[0] fails the whole owner");
|
||||
like(read_binary("$tmp/owner-$case->[0].log"), $case->[2], "$case->[0] reports its graph error at the owner boundary");
|
||||
ok(!-f $ENV{NATIVE_CALLS}, "$case->[0] precedes even mock scrub");
|
||||
}
|
||||
for my $scenario ('success', 'failed-child', 'empty-child', 'patch-path') {
|
||||
my ($root, $data) = prepare("owner-$scenario");
|
||||
if ($scenario eq 'patch-path') {
|
||||
write_binary("$root/native.patch", "--- a/native-leaf.spec\n+++ b/native-leaf.spec\n@@ -4 +4 @@\n-Summary: Native input contract fixture\n+Summary: Patched native input contract fixture\n");
|
||||
$data->{inputs}[0]{patches} = [{path => 'native.patch', sha256 => digest_file("$root/native.patch")}];
|
||||
$data->{inputs}[0]{defines} = ['llvmjit 0', 'runselftest 1'];
|
||||
write_binary("$root/openeuler/24.03-ppc64le.inputs.json", $json->encode($data));
|
||||
}
|
||||
my $out = "$tmp/owner-$scenario-output";
|
||||
my $dest = "$out/xcat-dep/openeuler24.03/ppc64le";
|
||||
make_path($dest, "$root/etc-mock");
|
||||
copy("$root/mock-configs/$target.cfg", "$root/etc-mock/$target.cfg") or die $!;
|
||||
write_binary("$dest/sentinel", 'old repository');
|
||||
local $ENV{NATIVE_FAIL} = $scenario eq 'failed-child' ? 'native-child' : '';
|
||||
local $ENV{NATIVE_EMPTY} = $scenario eq 'empty-child' ? 'native-child' : '';
|
||||
local $ENV{MOCKBUILD_ALL_MOUNTNS} = 1;
|
||||
my @cmd = ($^X, $owner, '--repo-root', $root, '--target', $target, '--xcat-source', $root,
|
||||
'--output', $out, '--run-id', 'contract', '--skip-genesis', '--skip-tarball', '--gpg-sign',
|
||||
'--gpg-home', $homes{build}, '--gpg-key-name', $keys{build}, '--max-parallel', 1);
|
||||
my $rc = run_capture("$tmp/owner-$scenario.log", @namespace,
|
||||
'sh', '-c', 'mount --bind "$1" /etc/mock && shift && exec "$@"', 'native-test', "$root/etc-mock", @cmd);
|
||||
my @calls = -f $ENV{NATIVE_CALLS} ? map { JSON::PP->new->decode($_) } split /\n/, read_binary($ENV{NATIVE_CALLS}) : ();
|
||||
my @built = map { $_->{name} } grep { exists $_->{name} } @calls;
|
||||
is_deeply(\@built, ['native-leaf', 'native-child'], "$scenario executes the prerequisite then its dependent through the owner");
|
||||
is_deeply([map { $_->{config_rc} } grep { exists $_->{config_rc} } @calls],
|
||||
[map { 0 } 1 .. ($scenario eq 'patch-path' ? 3 : 2)],
|
||||
"$scenario loads generated configurations through the installed native Mock");
|
||||
my $publisher = "$dest/publisher-package-1-1.oe2403.noarch.rpm";
|
||||
if ($scenario eq 'success' || $scenario eq 'patch-path') {
|
||||
is($rc, 0, 'whole owner signs and collects the completed native chain') or diag(read_binary("$tmp/owner-$scenario.log"));
|
||||
is(-f $publisher ? digest_file($publisher) : '', digest_file($signed{'publisher-package'}), 'final publisher package remains byte-identical');
|
||||
ok(-f "$dest/native-child-1-1.oe2403.noarch.rpm", 'dependent native output reaches the repository');
|
||||
ok(!-f "$dest/native-leaf-1-1.oe2403.noarch.rpm", 'build-only native prerequisite stays private');
|
||||
if ($scenario eq 'success' && $rc == 0) {
|
||||
my @verify = ($^X, $owner, '--repo-root', $root, '--target', $target,
|
||||
'--verify-repo', $dest, '--gpg-home', $homes{build}, '--gpg-key-name', $keys{build});
|
||||
is(run_capture("$tmp/final-verify.log", @verify), 0, 'standalone gate accepts the declared publisher and build signers');
|
||||
copy($publisher, "$tmp/publisher-preserved.rpm") or die $!;
|
||||
{
|
||||
local $ENV{GNUPGHOME} = $homes{build};
|
||||
is(run_capture("$tmp/publisher-resign.log", 'rpmsign', '--define', "_gpg_name $keys{build}",
|
||||
'--define', '__gpg /usr/bin/gpg', '--resign', $publisher), 0, 'negative control re-signs a publisher copy with the build key');
|
||||
}
|
||||
isnt(run_capture("$tmp/final-resigned-publisher.log", @verify), 0, 'collector rejects a re-signed publisher input even with an otherwise allowed key');
|
||||
like(read_binary("$tmp/final-resigned-publisher.log"), qr/SHA256 mismatch/, 'the publisher failure identifies lost byte identity');
|
||||
copy("$tmp/publisher-preserved.rpm", $publisher) or die $!;
|
||||
is(digest_file($publisher), digest_file($signed{'publisher-package'}), 'restore the original publisher bytes after the negative control');
|
||||
my $generated = "$dest/native-child-1-1.oe2403.noarch.rpm";
|
||||
copy($generated, "$tmp/generated-preserved.rpm") or die $!;
|
||||
{
|
||||
local $ENV{GNUPGHOME} = $homes{publisher};
|
||||
is(run_capture("$tmp/generated-resign.log", 'rpmsign', '--define', "_gpg_name $keys{publisher}",
|
||||
'--define', '__gpg /usr/bin/gpg', '--resign', $generated), 0, 'negative control signs generated output with the publisher key');
|
||||
}
|
||||
isnt(run_capture("$tmp/final-wrong-generated-key.log", @verify), 0, 'collector rejects the publisher key for generated outputs');
|
||||
like(read_binary("$tmp/final-wrong-generated-key.log"), qr/NOKEY|WRONGKEY|checksig/i, 'the generated output failure reports its signer mismatch');
|
||||
copy("$tmp/generated-preserved.rpm", $generated) or die $!;
|
||||
}
|
||||
if ($scenario eq 'patch-path') {
|
||||
my @prepared = grep { exists $_->{spec} } @calls;
|
||||
is(scalar @prepared, 1, 'tracked patch uses the existing native buildsrpm path once');
|
||||
like($prepared[0]{spec} // '', qr/^Summary: Patched native input contract fixture$/m,
|
||||
'the real patch modifies the extracted source spec');
|
||||
my @args = @{$prepared[0]{mock} // []};
|
||||
ok(grep($_ eq 'llvmjit 0', @args), 'vendor disable option remains one quoted argument');
|
||||
ok(grep($_ eq 'runselftest 1', @args), 'vendor test option remains enabled');
|
||||
my $original = "$out/mockbuild-all/$target-contract/native-inputs/native-leaf/native-leaf-1-1.oe2403.src.rpm";
|
||||
is(digest_file($original), digest_file($signed{'native-leaf-src'}), 'patch preparation preserves the original signed source');
|
||||
}
|
||||
} else {
|
||||
isnt($rc, 0, "$scenario fails collection");
|
||||
is(read_binary("$dest/sentinel"), 'old repository', "$scenario preserves the old repository");
|
||||
ok(!-f $publisher, "$scenario does not publish partial publisher inputs");
|
||||
ok(!-f "$dest/native-child-1-1.oe2403.noarch.rpm", "$scenario does not publish partial native outputs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,250 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin qw($RealBin);
|
||||
use JSON::PP qw(decode_json);
|
||||
use Test::More;
|
||||
|
||||
use lib "$RealBin/../lib", "$RealBin/lib";
|
||||
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
|
||||
use XCAT::GenesisReleaseTest qw(run_capture);
|
||||
|
||||
plan skip_all => 'Linux RPM tools and user namespaces required'
|
||||
unless $^O eq 'linux' && !grep { !command_exists($_) } qw(rpm rpmkeys rpmbuild createrepo_c unshare python3 gpg gpgconf);
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my @namespace = $> == 0 ? () : ('unshare', '--user', '--map-root-user');
|
||||
plan skip_all => 'User namespace unavailable for the collector root check'
|
||||
if @namespace && run_capture("$tmp/namespace.log", @namespace, 'true') != 0;
|
||||
my $collector = $ENV{XCAT_TEST_COLLECTOR} // abs_path("$RealBin/../mockbuild-all.pl");
|
||||
my $source = abs_path("$RealBin/../python-scp/python-scp-0.14.5-1.oe2403.src.rpm");
|
||||
my $hash = '3461d2a3fe0122cac2893d8465ad1271ae21e5570a31d4402e3f887ef545a0e8';
|
||||
my $arch = capture_command('uname', '-m');
|
||||
plan skip_all => 'Source package closure is selected only for x86_64' if $arch ne 'x86_64';
|
||||
is(digest_file($source), $hash, 'the official source RPM is pinned');
|
||||
my $epoch = 1788718796;
|
||||
my $target = 'openeuler-20.03sp4-x86_64';
|
||||
my $key_home = "$tmp/gnupg";
|
||||
my $key_name = 'source-contract@example.invalid';
|
||||
make_path("$tmp/bin", "$tmp/fixture/SPECS", $key_home);
|
||||
chmod 0700, $key_home;
|
||||
is(run_capture("$tmp/key.log", 'gpg', '--homedir', $key_home, '--batch', '--pinentry-mode', 'loopback',
|
||||
'--passphrase', '', '--quick-generate-key', $key_name, 'rsa2048', 'sign', '0'), 0,
|
||||
'create a private ephemeral signing identity for the unsigned-output gate')
|
||||
or BAIL_OUT(read_binary("$tmp/key.log"));
|
||||
END {
|
||||
run_capture("$tmp/key-cleanup.log", 'gpgconf', '--homedir', $key_home, '--kill', 'gpg-agent')
|
||||
if defined($key_home) && -d $key_home;
|
||||
}
|
||||
write_binary("$tmp/fixture/SPECS/python3-scp.spec", <<'SPEC');
|
||||
Name: python3-scp
|
||||
Version: 0.14.5
|
||||
Release: 1
|
||||
Summary: Collector contract fixture
|
||||
License: MIT
|
||||
BuildArch: noarch
|
||||
%description
|
||||
Collector contract fixture.
|
||||
%install
|
||||
mkdir -p %{buildroot}/usr/share/scp-contract
|
||||
printf 'fixture\n' > %{buildroot}/usr/share/scp-contract/payload
|
||||
%files
|
||||
/usr/share/scp-contract
|
||||
SPEC
|
||||
is(run_capture("$tmp/fixture.log", 'rpmbuild', '--quiet', '-ba', '--define', "_topdir $tmp/fixture",
|
||||
"$tmp/fixture/SPECS/python3-scp.spec"), 0, 'build real RPM fixtures for the command boundary')
|
||||
or BAIL_OUT(read_binary("$tmp/fixture.log"));
|
||||
write_binary("$tmp/bin/mock", <<'PYTHON');
|
||||
#!/usr/bin/python3
|
||||
import hashlib, json, os, pathlib, shutil, sys
|
||||
args = sys.argv[1:]
|
||||
entry = {'argv': args}
|
||||
def option(name):
|
||||
return args[args.index(name) + 1]
|
||||
if '-r' in args and pathlib.Path(option('-r')).is_file():
|
||||
entry['config'] = pathlib.Path(option('-r')).read_text()
|
||||
if '--spec' in args:
|
||||
entry['spec'] = pathlib.Path(option('--spec')).read_text()
|
||||
if '--rebuild' in args:
|
||||
src = pathlib.Path(option('--rebuild'))
|
||||
entry['source'] = str(src)
|
||||
entry['sha256'] = hashlib.sha256(src.read_bytes()).hexdigest()
|
||||
with open(os.environ['SCP_CALLS'], 'a') as stream:
|
||||
stream.write(json.dumps(entry) + '\n')
|
||||
if any(x.startswith('--scrub=') for x in args):
|
||||
sys.exit(0)
|
||||
if '--buildsrpm' in args:
|
||||
dest = pathlib.Path(option('--resultdir')); dest.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(os.environ['SCP_FIXTURE_SOURCE'], dest / 'python3-scp-0.14.5-1.src.rpm')
|
||||
sys.exit(0)
|
||||
if os.environ.get('SCP_MUTATE_SOURCE'):
|
||||
with open(os.environ['SCP_MUTATE_SOURCE'], 'ab') as stream:
|
||||
stream.write(b'changed after staging')
|
||||
if os.environ.get('SCP_BUILD_STATUS', '43') != '0':
|
||||
sys.exit(43)
|
||||
if os.environ.get('SCP_EMPTY_OUTPUT') != '1':
|
||||
dest = pathlib.Path(option('--resultdir')); dest.mkdir(parents=True, exist_ok=True)
|
||||
for key in ('SCP_FIXTURE_BINARY', 'SCP_FIXTURE_SOURCE'):
|
||||
source = pathlib.Path(os.environ[key]); shutil.copyfile(source, dest / source.name)
|
||||
PYTHON
|
||||
chmod 0755, "$tmp/bin/mock";
|
||||
|
||||
sub scenario {
|
||||
my ($name, %opt) = @_;
|
||||
my $root = "$tmp/$name source";
|
||||
my $out = "$tmp/$name output";
|
||||
my $repo = "$tmp/$name-repo";
|
||||
my $selected = $opt{target} // $target;
|
||||
my $manifest = $opt{packages} // 'python3-scp=0.14.5';
|
||||
make_path("$root/python-scp", "$root/grub2-xcat", "$repo/openeuler20.03sp4/x86_64");
|
||||
write_binary("$root/packages-manifest.conf", "[$selected]\n$manifest\n");
|
||||
write_binary("$root/Gitinfo", ('a' x 40) . "-dirty-snapshot-" . ('b' x 64) . "\n");
|
||||
write_binary("$root/Gitepoch", "$epoch\n");
|
||||
write_binary("$root/grub2-xcat/grub2-xcat.spec", "Name: grub2-xcat\nRelease: 1\n");
|
||||
write_binary("$root/grub2-xcat/mockbuild.pl", <<'PERL');
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::PP qw(encode_json);
|
||||
open my $fh, '>>', $ENV{SCP_CALLS} or die $!;
|
||||
print {$fh} encode_json({script => 'grub2-xcat', argv => \@ARGV}) . "\n";
|
||||
close $fh or die $!;
|
||||
exit 41;
|
||||
PERL
|
||||
my $input = "$root/python-scp/" . (split m{/}, $source)[-1];
|
||||
copy($source, $input) or die $! unless $opt{missing};
|
||||
write_binary($input, 'corrupt') if $opt{corrupt};
|
||||
write_binary("$repo/openeuler20.03sp4/x86_64/sentinel", 'previous repository');
|
||||
local $ENV{PATH} = "$tmp/bin:$ENV{PATH}";
|
||||
local $ENV{MOCKBUILD_ALL_MOUNTNS} = 1;
|
||||
local $ENV{SCP_CALLS} = "$tmp/$name calls.jsonl";
|
||||
local $ENV{SCP_BUILD_STATUS} = $opt{success} ? '0' : '43';
|
||||
local $ENV{SCP_EMPTY_OUTPUT} = $opt{empty} // '';
|
||||
local $ENV{SCP_MUTATE_SOURCE} = $opt{mutate} ? $input : '';
|
||||
local $ENV{SCP_FIXTURE_BINARY} = "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm";
|
||||
local $ENV{SCP_FIXTURE_SOURCE} = "$tmp/fixture/SRPMS/python3-scp-0.14.5-1.src.rpm";
|
||||
my @options = @{$opt{options} // []};
|
||||
my $rc = run_capture("$tmp/$name.log", @namespace, $^X, $collector,
|
||||
'--repo-root', $root, '--xcat-source', $root, '--target', $selected,
|
||||
'--output', $out, '--repo-dep', $repo, '--run-id', 'source-contract',
|
||||
'--build-timestamp', $epoch, '--max-parallel', 1, '--parallel-builds', 1,
|
||||
'--skip-genesis', '--skip-perl', '--skip-tarball', @options);
|
||||
my @calls = -f $ENV{SCP_CALLS}
|
||||
? map { decode_json($_) } split /\n/, read_binary($ENV{SCP_CALLS}) : ();
|
||||
return {rc => $rc, calls => \@calls, log => read_binary("$tmp/$name.log"), input => $input,
|
||||
repo => $repo, out => $out, root => $root};
|
||||
}
|
||||
|
||||
my $selected = scenario('selected');
|
||||
isnt($selected->{rc}, 0, 'a failed native source rebuild fails the full owner');
|
||||
my @builds = grep { grep { $_ eq '--rebuild' } @{$_->{argv}} } @{$selected->{calls}};
|
||||
is(scalar @builds, 1, 'the exact native manifest selects one source rebuild');
|
||||
if (@builds) {
|
||||
my $call = $builds[0];
|
||||
like($call->{config}, qr/\Ainclude\('\/etc\/mock\/\Q$target\E\.cfg'\)\n/,
|
||||
'the source rebuild includes the exact native target');
|
||||
like($call->{config}, qr/\Qconfig_opts['environment']['SOURCE_DATE_EPOCH'] = '$epoch'\E/,
|
||||
'the epoch enters the mock build environment');
|
||||
unlike($call->{config}, qr/epel|forcearch|bootstrap_image/, 'the overlay introduces no foreign target policy');
|
||||
isnt($call->{source}, $selected->{input}, 'mock consumes a private staged source');
|
||||
is($call->{sha256}, $hash, 'the staged source retains the official digest');
|
||||
my %args;
|
||||
for my $i (0 .. $#{$call->{argv}} - 1) { $args{$call->{argv}[$i]} = $call->{argv}[$i + 1]; }
|
||||
like($args{'--uniqueext'}, qr/^mba-01-openeuler-20\.03-[0-9a-f]{8}-python3-scp$/,
|
||||
'mock uses the existing bounded target, run digest and package suffix');
|
||||
like($args{'--resultdir'}, qr/\Q$target\E-source-contract\/build-results\/python3-scp\z/,
|
||||
'result collection remains target and package specific');
|
||||
for my $macro ('use_source_date_epoch_as_buildtime 1', 'clamp_mtime_to_source_date_epoch 1', '_buildhost xcat-build') {
|
||||
ok(grep($_ eq $macro, @{$call->{argv}}), "mock retains deterministic macro $macro");
|
||||
}
|
||||
}
|
||||
like($selected->{log}, qr/required build step|every build step failed/, 'mock failure reaches the owner failure gate');
|
||||
is(read_binary("$selected->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
|
||||
'failed source build preserves the previous repository');
|
||||
|
||||
for my $case (['corrupt', 'SHA256 mismatch'], ['missing', 'Missing source RPM']) {
|
||||
my $result = scenario($case->[0], $case->[0] => 1, options => ['--scrub-all-chroots']);
|
||||
isnt($result->{rc}, 0, "$case->[0] selected source fails");
|
||||
like($result->{log}, qr/\Q$case->[1]\E/, "$case->[0] source identifies the input failure");
|
||||
is_deeply($result->{calls}, [], "$case->[0] source fails before any mock action, including scrub");
|
||||
}
|
||||
my $staged = scenario('immutable-stage', mutate => 1);
|
||||
isnt(digest_file($staged->{input}), $hash, 'the command double changes the original after staging');
|
||||
my @staged_builds = grep { exists $_->{sha256} } @{$staged->{calls}};
|
||||
is(scalar @staged_builds, 1, 'the staged source reaches mock once');
|
||||
is($staged_builds[0]{sha256}, $hash, 'changing the original does not change the build input') if @staged_builds;
|
||||
|
||||
for my $other ('openeuler-24.03sp3-x86_64', 'openeuler-24.03sp4-x86_64', 'alma+epel-9-x86_64') {
|
||||
my $result = scenario("unselected-$other", target => $other, packages => 'grub2-xcat=1.0', missing => 1);
|
||||
isnt($result->{rc}, 0, "$other propagates the existing script failure");
|
||||
my @script = grep { ($_->{script} // '') eq 'grub2-xcat' } @{$result->{calls}};
|
||||
is(scalar @script, 1, "$other keeps the existing script builder");
|
||||
is(scalar(grep { exists $_->{source} } @{$result->{calls}}), 0, "$other does not select the source RPM");
|
||||
unlike($result->{log}, qr/Missing source RPM|SHA256 mismatch/, "$other does not require the unselected source input");
|
||||
if (@script) {
|
||||
my %args = @{$script[0]{argv}};
|
||||
is($args{'--mock-cfg'}, $other, "$other preserves the script target argument");
|
||||
is($args{'--build-timestamp'}, "$epoch", "$other preserves the script epoch argument");
|
||||
}
|
||||
}
|
||||
my $absent = scenario('native-manifest-absence', packages => 'grub2-xcat=1.0', missing => 1);
|
||||
is(scalar(grep { exists $_->{source} } @{$absent->{calls}}), 0, '20 SP4 also requires explicit manifest selection');
|
||||
unlike($absent->{log}, qr/Missing source RPM/, 'an absent native manifest entry needs no source input');
|
||||
my $cross = scenario('cross', target => 'openeuler-24.03-ppc64le');
|
||||
isnt($cross->{rc}, 0, 'native cross-architecture source build is rejected');
|
||||
like($cross->{log}, qr/requires a ppc64le build host/, 'cross rejection names the native host requirement');
|
||||
is_deeply($cross->{calls}, [], 'cross rejection precedes every build command');
|
||||
|
||||
my $dry = scenario('dry', options => ['--dry-run']);
|
||||
is($dry->{rc}, 0, 'the selected source has a successful dry-run plan');
|
||||
like($dry->{log}, qr/--rebuild.*python-scp-0\.14\.5-1\.oe2403\.src\.rpm/, 'dry run reports the source rebuild');
|
||||
is_deeply($dry->{calls}, [], 'dry run executes no mock action');
|
||||
ok(!-d "$dry->{out}/mockbuild-all/$target-source-contract/source-rpms", 'dry run stages no source or mock overlay');
|
||||
my $restamp = scenario('restamp', options => ['--build-number', 7]);
|
||||
isnt($restamp->{rc}, 0, 'restamped rebuild failure remains fatal');
|
||||
my @sources = grep { exists $_->{spec} } @{$restamp->{calls}};
|
||||
is(scalar @sources, 1, 'a build number first creates one native source RPM');
|
||||
like($sources[0]{spec}, qr/^Release:\s+1\.snap202609061819\.7\s*$/m,
|
||||
'source spec reuses the existing release suffix policy') if @sources;
|
||||
my @restamped = grep { exists $_->{source} } @{$restamp->{calls}};
|
||||
is(scalar @restamped, 1, 'the generated source RPM is rebuilt once');
|
||||
like($restamped[0]{source}, qr/restamp-srpm\/python3-scp-0\.14\.5-1\.src\.rpm\z/,
|
||||
'binary build consumes the new source RPM') if @restamped;
|
||||
is(digest_file($restamp->{input}), $hash, 'Release restamping preserves the official input bytes');
|
||||
|
||||
my $empty = scenario('empty', success => 1, empty => 1);
|
||||
isnt($empty->{rc}, 0, 'mock success without an RPM cannot close the owner build');
|
||||
like($empty->{log}, qr/No binary RPMs were collected/, 'empty results reach the existing collection gate');
|
||||
my $unsigned = scenario('unsigned', success => 1,
|
||||
options => ['--gpg-home', $key_home, '--gpg-key-name', $key_name]);
|
||||
isnt($unsigned->{rc}, 0, 'unsigned command-double output cannot pass the repository signature gate');
|
||||
like($unsigned->{log}, qr/UNSIGNED repomd/,
|
||||
'the unsigned output reports a repository trust failure');
|
||||
like($unsigned->{log}, qr/UNSIGNED rpm python3-scp-0\.14\.5-1\.noarch\.rpm/,
|
||||
'the existing gate also rejects the unsigned binary');
|
||||
is(read_binary("$unsigned->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
|
||||
'a publication gate failure preserves the previous repository');
|
||||
my $collected = scenario('collection', success => 1, options => ['--no-verify-repo']);
|
||||
is($collected->{rc}, 0, 'the debug collection path accepts successful RPM-producing command output')
|
||||
or diag($collected->{log});
|
||||
my $published = "$collected->{repo}/openeuler20.03sp4/x86_64/python3-scp-0.14.5-1.noarch.rpm";
|
||||
ok(-f $published, 'native binary reaches the exact repository subdirectory');
|
||||
is(digest_file($published), digest_file("$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm"),
|
||||
'the existing collector preserves binary bytes') if -f $published;
|
||||
ok(-f "$collected->{out}/mockbuild-all/$target-source-contract/repo-src/python3-scp-0.14.5-1.src.rpm",
|
||||
'the existing collector also retains the generated source RPM');
|
||||
|
||||
my $skipped = scenario('skip-dep', missing => 1, options => ['--skip-xcat-dep', '--dry-run']);
|
||||
is($skipped->{rc}, 0, 'skipping dependency builds does not require the source RPM');
|
||||
is_deeply($skipped->{calls}, [], 'skip-dep executes no source action');
|
||||
my $replay = scenario('replay', missing => 1, options => ['--skip-build', '--no-verify-repo',
|
||||
'--collect-dir', "$tmp/fixture/RPMS/noarch"]);
|
||||
is($replay->{rc}, 0, 'build-free artifact collection does not require the original source RPM');
|
||||
is_deeply($replay->{calls}, [], 'build-free collection executes no source action');
|
||||
my $incomplete = scenario('incomplete', success => 1, packages => "python3-scp=0.14.5\nclosure-gap=1");
|
||||
isnt($incomplete->{rc}, 0, 'a successful source rebuild does not bypass the manifest gate');
|
||||
like($incomplete->{log}, qr/MISSING closure-gap\b/, 'the manifest gate identifies the missing required package');
|
||||
|
||||
done_testing();
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use FindBin qw($RealBin);
|
||||
use lib "$RealBin/..";
|
||||
use File::Temp qw(tempdir);
|
||||
use File::Path qw(make_path);
|
||||
use JSON::PP qw(decode_json);
|
||||
use Test::More;
|
||||
use MockBuildUtils qw(openeuler_build_target openeuler_repo_subdir install_deps_command install_deps_packages);
|
||||
|
||||
my @cells = (
|
||||
['20.03sp4', '20.03-LTS-SP4', '20.03LTS_SP4', 'x86_64'],
|
||||
['22.03sp4', '22.03-LTS-SP4', '22.03LTS_SP4', 'x86_64'],
|
||||
['24.03sp1', '24.03-LTS-SP1', '24.03LTS_SP1', 'x86_64'],
|
||||
['24.03sp3', '24.03-LTS-SP3', '24.03LTS_SP3', 'x86_64'],
|
||||
['24.03sp4', '24.03-LTS-SP4', '24.03LTS_SP4', 'x86_64'],
|
||||
['24.03', '24.03-LTS', '24.03LTS', 'ppc64le'],
|
||||
);
|
||||
for my $cell (@cells) {
|
||||
my ($version, $release, undef, $arch) = @$cell;
|
||||
my ($base, $sp) = $version =~ /^(\d+\.\d+)(?:sp(\d+))?$/;
|
||||
my $native_version = "$base (LTS" . (defined($sp) ? "-SP$sp" : '') . ')';
|
||||
my $target = "openeuler-$version-$arch";
|
||||
is(openeuler_build_target({ID => 'openEuler', VERSION => $native_version}, $arch), $target,
|
||||
"$target retains the native service pack");
|
||||
is(openeuler_repo_subdir($target), "openeuler$version/$arch", "$target preserves repository provenance");
|
||||
}
|
||||
is(openeuler_build_target({ID => 'rocky', VERSION_ID => '9.6'}, 'x86_64'), undef, 'EL uses existing target selection');
|
||||
is(openeuler_repo_subdir('alma+epel-10-x86_64'), undef, 'EL uses existing repository layout');
|
||||
for my $target ('openeuler-24.09-x86_64', 'openeuler-24.03sp0-x86_64', 'openeuler-24.03-ppc64') {
|
||||
eval {openeuler_repo_subdir($target)};
|
||||
like($@, qr/Unsupported openEuler build target/, "$target is rejected");
|
||||
}
|
||||
my @native_install = install_deps_command('openEuler');
|
||||
is_deeply([@native_install[0..6]], ['dnf', '--setopt=gpgcheck=1', '--setopt=*.gpgcheck=1', '--setopt=strict=1', '--setopt=install_weak_deps=False', '-y', 'install'],
|
||||
'native prerequisites require signatures and dependency closure');
|
||||
ok(grep($_ eq '/usr/bin/systemd-nspawn', @native_install), 'native prerequisites request the mock isolation executable across package splits');
|
||||
ok(!grep(/epel|crb|codeready/i, @native_install), 'native prerequisites do not enable EL repositories');
|
||||
is_deeply([install_deps_command('rocky')], ['dnf', '-y', 'install', install_deps_packages('rocky')], 'EL prerequisite command remains unchanged');
|
||||
|
||||
{
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
open(my $manifest, '>', "$tmp/packages-manifest.conf") or die $!;
|
||||
for my $cell (@cells) {
|
||||
my ($version, undef, undef, $arch) = @$cell;
|
||||
print {$manifest} "[openeuler-$version-$arch]\nnative-fixture-$version=1\n";
|
||||
}
|
||||
close($manifest) or die $!;
|
||||
for my $cell (@cells) {
|
||||
my ($version, undef, undef, $arch) = @$cell;
|
||||
my $repo = "$tmp/openeuler$version/$arch";
|
||||
make_path($repo);
|
||||
my $pid = fork();
|
||||
die $! unless defined $pid;
|
||||
if (!$pid) {
|
||||
open(STDOUT, '>', "$tmp/output") or die $!;
|
||||
open(STDERR, '>&', \*STDOUT) or die $!;
|
||||
exec($^X, "$RealBin/../mockbuild-all.pl", '--verify-repo', $repo, '--repo-root', $tmp) or die $!;
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
isnt($? >> 8, 0, "$version/$arch empty repository fails the full publication gate");
|
||||
open(my $output, '<', "$tmp/output") or die $!;
|
||||
my $text = do {local $/; <$output>};
|
||||
close($output);
|
||||
like($text, qr/MISSING native-fixture-\Q$version\E\b/, "$version/$arch path selects its own exact manifest section");
|
||||
}
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'native mock Python library and templates required', 66
|
||||
if system('python3 -c "import mockbuild.config" >/dev/null 2>&1') != 0
|
||||
|| !-f '/etc/mock/templates/openeuler-24.03.tpl';
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my $loader = "$tmp/load.py";
|
||||
open(my $fh, '>', $loader) or die $!;
|
||||
print {$fh} <<'PYTHON';
|
||||
import configparser
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from mockbuild.config import load_config
|
||||
|
||||
source = Path(sys.argv[1]).resolve()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config_path = Path(directory)
|
||||
(config_path / 'templates').mkdir()
|
||||
for parent in ('openeuler-20.03-sp4.tpl', 'openeuler-22.03-sp4.tpl', 'openeuler-24.03.tpl'):
|
||||
shutil.copyfile(Path('/etc/mock/templates') / parent, config_path / 'templates' / parent)
|
||||
shutil.copyfile(source / 'templates/openeuler-lts-xcat.tpl', config_path / 'templates/openeuler-lts-xcat.tpl')
|
||||
result = {}
|
||||
for wrapper in sorted(source.glob('openeuler-*.cfg')):
|
||||
config = load_config(str(config_path), str(wrapper))
|
||||
repos = configparser.ConfigParser(interpolation=None)
|
||||
repos.read_string(config['dnf.conf'])
|
||||
result[wrapper.stem] = {key: config[key] for key in ('root', 'target_arch', 'legal_host_arches', 'releasever', 'dist', 'use_bootstrap_image')}
|
||||
result[wrapper.stem]['repos'] = {section: dict(repos[section]) for section in repos.sections()}
|
||||
print(json.dumps(result))
|
||||
PYTHON
|
||||
close($fh) or die $!;
|
||||
open(my $pipe, '-|', 'python3', $loader, "$RealBin/../mock-configs") or die $!;
|
||||
my $json = do {local $/; <$pipe>};
|
||||
close($pipe) or die "native mock config loader failed: $?";
|
||||
my $configs = decode_json($json);
|
||||
for my $cell (@cells) {
|
||||
my ($version, $release, $releasever, $arch) = @$cell;
|
||||
my $target = "openeuler-$version-$arch";
|
||||
my $config = $configs->{$target};
|
||||
is($config->{root}, $target, "$target selects its own buildroot");
|
||||
is($config->{target_arch}, $arch, "$target selects its native architecture");
|
||||
is_deeply($config->{legal_host_arches}, [$arch], "$target requires a native host");
|
||||
is($config->{releasever}, $releasever, "$target retains the release package convention");
|
||||
is($config->{dist}, '', "$target retains the native empty dist macro");
|
||||
ok(!$config->{use_bootstrap_image}, "$target constructs its bootstrap from signed native RPMs");
|
||||
my $repos = $config->{repos};
|
||||
my @names = $arch eq 'ppc64le' ? ('OS') : ('OS', 'everything', 'update');
|
||||
is_deeply([sort grep {$_ ne 'main'} keys %$repos], [sort @names], "$target selects only published native repositories");
|
||||
is($repos->{main}{gpgcheck}, '1', "$target requires native package signatures");
|
||||
my $base = "https://repo.openeuler.org/openEuler-$release";
|
||||
is_deeply([map {$repos->{$_}{baseurl}} @names], [map {"$base/$_/$arch/"} @names], "$target pins repository URLs to its exact release");
|
||||
is_deeply([map {$repos->{$_}{gpgkey}} @names], [map {"$base/OS/$arch/RPM-GPG-KEY-openEuler"} @names], "$target uses the release signing key");
|
||||
ok(!grep({$repos->{$_}{gpgcheck} ne '1' || $repos->{$_}{skip_if_unavailable} ne '0'} @names), "$target fails on unsigned packages or unavailable repositories");
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
@@ -0,0 +1,134 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Glob qw(bsd_glob);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin qw($RealBin);
|
||||
use JSON::PP qw(encode_json);
|
||||
use Test::More;
|
||||
use Text::ParseWords qw(shellwords);
|
||||
|
||||
use lib "$RealBin/../lib", "$RealBin/lib";
|
||||
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
|
||||
use XCAT::GenesisReleaseTest qw(run_capture);
|
||||
|
||||
plan skip_all => 'Linux RPM packaging tools and namespaces are required'
|
||||
unless $^O eq 'linux' && !grep { !command_exists($_) } qw(rpm rpmbuild rpm2cpio cpio tar unshare);
|
||||
my $tmp = tempdir(CLEANUP => !$ENV{XCAT_TEST_KEEP});
|
||||
diag("xNBA release fixtures: $tmp");
|
||||
my @namespace = $> == 0 ? () : ('unshare', '--user', '--map-root-user');
|
||||
plan skip_all => 'User namespace is unavailable for the packaging owner root check'
|
||||
if @namespace && run_capture("$tmp/namespace.log", @namespace, 'true') != 0;
|
||||
my $repo = abs_path("$RealBin/..");
|
||||
my $owner = $ENV{XCAT_TEST_XNBA} // "$repo/xnba/mockbuild.pl";
|
||||
my $collector = $ENV{XCAT_TEST_COLLECTOR} // "$repo/mockbuild-all.pl";
|
||||
my $epoch = 1788718796;
|
||||
my $suffix = '.snap202609061819.21';
|
||||
my $default_release = capture_command('rpm', '--eval', '1%{?dist}');
|
||||
my $source = "$tmp/source tree";
|
||||
make_path("$source/xnba/binary");
|
||||
copy($owner, "$source/xnba/mockbuild.pl") or die $!;
|
||||
copy("$repo/MockBuildUtils.pm", "$source/MockBuildUtils.pm") or die $!;
|
||||
copy("$repo/xnba/xnba-undi.spec", "$source/xnba/xnba-undi.spec") or die $!;
|
||||
copy("$repo/xnba/binary/$_", "$source/xnba/binary/$_") or die $! for qw(xnba.kpxe xnba.efi);
|
||||
my %specs;
|
||||
for my $case (['default', []], ['suffix', ['--release-suffix', $suffix]]) {
|
||||
my ($name, $options) = @$case;
|
||||
my $work = "$tmp/$name-work";
|
||||
my $result = "$tmp/$name-result";
|
||||
my $rc = run_capture("$tmp/$name.log", @namespace, $^X, "$source/xnba/mockbuild.pl",
|
||||
'--mock-cfg', 'openeuler-24.03-ppc64le', '--work-dir', $work,
|
||||
'--result-dir', $result, '--log-dir', "$tmp/$name logs", '--build-timestamp', $epoch, @$options);
|
||||
is($rc, 0, "$name actual xNBA packaging owner succeeds") or diag(read_binary("$tmp/$name.log"));
|
||||
next if $rc;
|
||||
$specs{$name} = read_binary("$work/rpmbuild/SPECS/xnba-undi.spec");
|
||||
my @rpms = bsd_glob("$result/*.rpm");
|
||||
is(scalar @rpms, 2, "$name produces exactly a binary RPM and SRPM");
|
||||
my ($binary) = grep { /\.noarch\.rpm\z/ } @rpms;
|
||||
my ($srpm) = grep { /\.src\.rpm\z/ } @rpms;
|
||||
ok($binary && $srpm, "$name output includes both RPM kinds");
|
||||
next unless $binary && $srpm;
|
||||
my $release = $default_release . ($name eq 'suffix' ? $suffix : '');
|
||||
is(capture_command('rpm', '-qp', '--qf', '%{RELEASE}', $_), $release,
|
||||
"$name records the expected Release in $_") for ($binary, $srpm);
|
||||
is(capture_command('rpm', '-qp', '--qf', '%{SOURCEPACKAGE}', $srpm), '1', "$name source output is an SRPM");
|
||||
my $unpack = "$tmp/$name payload";
|
||||
make_path($unpack);
|
||||
is(run_capture("$tmp/$name.cpio", 'rpm2cpio', $binary), 0, "$name native RPM payload decodes");
|
||||
is(run_capture("$tmp/$name-extract.log", 'bash', '-c',
|
||||
'cd "$1" && cpio --quiet -idm --no-absolute-filenames < "$2"', 'extract', $unpack, "$tmp/$name.cpio"),
|
||||
0, "$name native cpio payload extracts");
|
||||
for my $file (qw(xnba.kpxe xnba.efi)) {
|
||||
is(digest_file("$unpack/tftpboot/xcat/$file"), digest_file("$repo/xnba/binary/$file"),
|
||||
"$name preserves committed $file bytes");
|
||||
}
|
||||
is(capture_command('rpm', '-qpl', $binary), "/tftpboot/xcat/xnba.efi\n/tftpboot/xcat/xnba.kpxe",
|
||||
"$name ships only the two boot payloads");
|
||||
}
|
||||
if (exists $specs{default} && exists $specs{suffix}) {
|
||||
(my $without_suffix = $specs{suffix}) =~ s/\Q$suffix\E//;
|
||||
is($without_suffix, $specs{default}, 'the suffix changes only the generated Release token');
|
||||
}
|
||||
|
||||
my $arch = capture_command('uname', '-m');
|
||||
my @targets = ("alma+epel-9-$arch");
|
||||
push @targets, 'openeuler-24.03-ppc64le' if $arch eq 'ppc64le';
|
||||
push @targets, 'openeuler-24.03sp3-x86_64' if $arch eq 'x86_64';
|
||||
for my $target (@targets) {
|
||||
for my $number (undef, 21) {
|
||||
my $label = defined($number) ? 'suffix' : 'default';
|
||||
my $root = "$tmp/plan $target $label";
|
||||
make_path(map { "$root/$_" } qw(xnba goconserver grub2-xcat openeuler));
|
||||
write_binary("$root/packages-manifest.conf", "[$target]\nxnba-undi=1.*\ngoconserver=0.*\ngrub2-xcat=2.*\n");
|
||||
for my $dir (qw(xnba goconserver grub2-xcat)) {
|
||||
write_binary("$root/$dir/mockbuild.pl", "die qq{dry-run executed a builder\\n};\n");
|
||||
}
|
||||
if ($target eq 'openeuler-24.03-ppc64le') {
|
||||
my $key = 'openeuler/publisher.key';
|
||||
write_binary("$root/$key", 'dry-run key fixture');
|
||||
write_binary("$root/openeuler/24.03-ppc64le.inputs.json", encode_json({
|
||||
version => 1, target => $target,
|
||||
publisher_key => {path => $key, sha256 => digest_file("$root/$key"), fingerprint => ('A' x 40)},
|
||||
inputs => [map { {name => $_, type => 'owner', outputs => [$_],
|
||||
build_uid => ($_ eq 'xnba-undi' ? 0 : 1000)} } qw(xnba-undi goconserver grub2-xcat)],
|
||||
}));
|
||||
}
|
||||
local $ENV{MOCKBUILD_ALL_MOUNTNS} = 1;
|
||||
my $log = "$tmp/plan-$target-$label.log";
|
||||
my $rc = run_capture($log, @namespace, $^X, $collector, '--repo-root', $root,
|
||||
'--xcat-source', $root, '--target', $target, '--output', "$root/output",
|
||||
'--build-timestamp', $epoch, '--run-id', 'release-contract', '--skip-genesis', '--gpg-sign',
|
||||
'--max-parallel', 1, '--parallel-builds', 1, '--dry-run',
|
||||
(defined($number) ? ('--build-number', $number) : ()));
|
||||
is($rc, 0, "$target $label whole collector dry-run succeeds") or diag(read_binary($log));
|
||||
my $text = read_binary($log);
|
||||
for my $dir (qw(xnba goconserver grub2-xcat)) {
|
||||
my ($command) = $text =~ /^\+ ([^\n]*\Q$root\/$dir\/mockbuild.pl\E[^\n]*)$/m;
|
||||
ok(defined($command), "$target $label plans the $dir owner");
|
||||
next unless defined $command;
|
||||
my @argv = shellwords($command);
|
||||
for (1 .. 3) {
|
||||
last if $argv[0] eq 'perl';
|
||||
@argv = shellwords($argv[-1]);
|
||||
}
|
||||
is($argv[0], 'perl', 'the planned command reaches the Perl owner');
|
||||
my %options;
|
||||
for my $i (0 .. $#argv - 1) { $options{$argv[$i]} = $argv[$i + 1]; }
|
||||
if (defined($number) && $dir ne 'grub2-xcat') {
|
||||
is($options{'--release-suffix'}, $suffix, "$dir receives the exact CD suffix");
|
||||
} else {
|
||||
ok(!exists($options{'--release-suffix'}), "$dir retains its existing suffix option behavior");
|
||||
}
|
||||
if ($dir eq 'goconserver') {
|
||||
like($options{'--go-ref'}, qr/\A[0-9a-f]{40}\z/, 'goconserver retains its immutable source pin');
|
||||
} else {
|
||||
ok(!exists($options{'--go-ref'}), "$dir receives no Go-specific option");
|
||||
}
|
||||
}
|
||||
ok(!-d "$root/output/mockbuild-all/$target-release-contract/build-results", 'the dry-run executes no package build');
|
||||
}
|
||||
}
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user