2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-30 14:55:17 +00:00

Merge pull request #77 from xcat2/feature/openeuler-lts

feat(os): build native openEuler LTS dependency repositories
This commit is contained in:
Daniel Hilst
2026-09-29 11:59:05 -03:00
committed by GitHub
38 changed files with 4034 additions and 88 deletions
+1
View File
@@ -0,0 +1 @@
postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch whitespace=-blank-at-eol
+16 -1
View File
@@ -23,7 +23,7 @@ jobs:
sudo apt-get install -y --no-install-recommends \
apt-utils createrepo-c dpkg-dev gnupg \
libfile-slurper-perl \
libparallel-forkmanager-perl libperl-critic-perl rpm
libparallel-forkmanager-perl libperl-critic-perl rpm rpm2cpio cpio
- name: Run static checks
run: |
@@ -76,3 +76,18 @@ jobs:
sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t
prove -v t/riscv64_perl_cell.t
sudo -E prove -v -It/lib t/common-repo-gate.t
prove -v t/openeuler.t
# Ubuntu's AppArmor policy blocks these unprivileged test namespaces.
- name: Run native packaging tests
run: |
set -o pipefail
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
prove -v native/buildinfo_provenance.t native/goconserver-openeuler.t \
native/openeuler-power-inputs.t native/openeuler-srpm.t \
native/xnba-release-suffix.t 2>&1 | tee native.tap
# prove returns success when a test file skips all its checks.
if grep -E '^1\.\.0 # SKIP|^native/[^ ]+\.t \.+ skipped' native.tap; then
echo 'a native test file was skipped' >&2
exit 1
fi
+102 -12
View File
@@ -1,8 +1,5 @@
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.
# Build helpers use explicit settings and callbacks instead of script globals.
use strict;
use warnings;
use Exporter 'import';
@@ -22,11 +19,13 @@ our @EXPORT_OK = qw(
version_matches required_pkgs skipped_builder carry_over_rpms rpm_name rpm_arch rpm_source_rpm
source_package rpm_digests_ok
have_rpm read_manifest
verify_repo_packages verify_repo_signature verify_rpm_signatures
derive_target_from_repo_path verify_repo_packages verify_repo_signature verify_rpm_signatures
parse_evr evr_cmp evr_constraint_ok parse_pin rpmkeys_checksig_problem
rpm_version rpm_release rpm_sigmd5 rpm_is_signed restamp_release_line
cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix
createrepo_c_cmd sign_and_index_repo
build_mock_uniqueext rpm_in_cell resolve_mock_cfg
openeuler_build_target openeuler_repo_subdir
recover_common_repository
);
@@ -37,6 +36,7 @@ our @EXPORT_OK = qw(
sub install_deps_packages {
my ($os_id) = @_;
$os_id = '' unless defined $os_id;
return (install_deps_packages(''), '/usr/bin/systemd-nspawn') if lc($os_id) eq 'openeuler';
# The perl modules are what actually break a run; the rest is the toolchain the script drives.
return qw(perl perl-File-Slurper perl-IPC-Cmd perl-Parallel-ForkManager perl-Digest-SHA
mock createrepo_c tar findutils rpm rpm-build rpm-sign rpmdevtools gnupg2 wget git)
@@ -53,9 +53,31 @@ sub install_deps_command {
my @pkgs = install_deps_packages($os_id);
return ('zypper', '--non-interactive', 'install', '--no-recommends', @pkgs)
if $os_id =~ /^(?:opensuse|sles|sled)/;
return ('dnf', '--setopt=gpgcheck=1', '--setopt=*.gpgcheck=1', '--setopt=strict=1', '--setopt=install_weak_deps=False', '-y', 'install', @pkgs)
if lc($os_id) eq 'openeuler';
return ('dnf', '-y', 'install', @pkgs);
}
sub openeuler_build_target {
my ($os, $arch) = @_;
return undef unless lc($os->{ID} // '') eq 'openeuler';
my $version = $os->{VERSION} || $os->{VERSION_ID} || '';
if ($version =~ /\A(20|22|24)\.03\s+\(LTS(?:-SP([1-9][0-9]*))?\)\z/) {
$version = "$1.03" . (defined($2) ? "sp$2" : '');
}
my $target = "openeuler-$version-$arch";
openeuler_repo_subdir($target);
return $target;
}
sub openeuler_repo_subdir {
my ($target) = @_;
return undef unless defined($target) && $target =~ /\Aopeneuler-/;
die "Unsupported openEuler build target '$target'\n"
unless $target =~ /\Aopeneuler-((?:20|22|24)\.03(?:sp[1-9][0-9]*)?)-(x86_64|ppc64le)\z/;
return "openeuler$1/$2";
}
# missing_perl_modules(@modules): those that cannot be loaded, in order. The point of --install-deps
# is that the run AFTER it cannot die on a missing module, so the modules are proven by loading
# them, not by trusting the package manager's exit code.
@@ -278,6 +300,17 @@ sub parse_pin {
return ('version');
}
sub derive_target_from_repo_path {
my ($dir) = @_;
my $tgt;
return $tgt unless defined $dir;
$tgt = "alma+epel-$1-$2" if $dir =~ m{/rh(\d+)/([^/]+)/*$};
if ($dir =~ m{/openeuler((?:20|22|24)\.03(?:sp[1-9][0-9]*)?)/(x86_64|ppc64le)/*$}) {
$tgt = "openeuler-$1-$2";
}
return $tgt;
}
sub verify_repo_packages {
my ($expected, $present_ver, $present_evr, $vercmp) = @_;
$present_evr //= $present_ver;
@@ -486,6 +519,53 @@ sub read_manifest {
return %m;
}
sub createrepo_c_cmd {
my ($dir, $epoch) = @_;
return 'createrepo_c --update '
. '--revision ' . sh_quote($epoch) . ' --set-timestamp-to-revision '
. sh_quote($dir);
}
sub sign_and_index_repo {
my ($dir, $native, %options) = @_;
my $run = $options{run} // die "Repository signing requires a command runner\n";
my @rpms = grep { !/\.src\.rpm$/ } bsd_glob("$dir/*.rpm");
if ($native) {
require XCAT::NativeInputs;
require XCAT::BuildUtils;
my @built;
for my $rpm (@rpms) {
my $id = XCAT::NativeInputs::rpm_identity($rpm);
my $owner = $native->{outputs}{$id->{name}} // die "Undeclared native output: $id->{name}\n";
my $node = $native->{nodes}{$owner};
if ($node->{type} eq 'publisher') {
die "Publisher input changed before signing: $rpm\n"
unless XCAT::BuildUtils::digest_file($rpm) eq $node->{sha256};
} else {
push @built, $rpm;
}
}
@rpms = @built;
}
if ($options{gpg_sign} && @rpms) {
local $ENV{GNUPGHOME} = $options{gpg_home} if $options{gpg_home};
$run->('rpmsign --define ' . sh_quote("%_gpg_name $options{gpg_key_name}")
. ' --define ' . sh_quote("%__gpg $options{gpg_program}") . ' --addsign '
. join(' ', map { sh_quote($_) } @rpms));
}
$run->(createrepo_c_cmd($dir, $options{source_date_epoch}));
if ($options{gpg_sign}) {
local $ENV{GNUPGHOME} = $options{gpg_home} if $options{gpg_home};
my $repomd = "$dir/repodata/repomd.xml";
unlink "$repomd.asc" if -f "$repomd.asc";
$run->("gpg -a --detach-sign --default-key " . sh_quote($options{gpg_key_name}) . ' ' . sh_quote($repomd));
$run->("gpg -a --export " . sh_quote($options{gpg_key_name}) . " > " . sh_quote("$repomd.key"));
if ($native) {
$run->('cat ' . sh_quote($native->{publisher_key}) . ' >> ' . sh_quote("$repomd.key"));
}
}
}
# 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
@@ -528,12 +608,9 @@ sub cross_copy_genesis {
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.
# Cross-populate Genesis RPMs between matching legacy architecture repositories; skip openEuler cells.
# Both peers and their own Genesis RPMs are required. Empty roots are errors.
# The sign and reindex callbacks operate on copied RPMs and selected destination repositories.
# Architectures whose xCAT-genesis-base is cross-provisioned into every peer repo, so a management
# node can netboot nodes of any arch (issue #7610). Each entry maps the repo/subdir arch name to the
# genesis rpm's xCAT "tarch" (xCAT collapses ppc/ppc64le into tarch ppc64; x86_64 stays x86_64). This
@@ -571,10 +648,23 @@ sub finalize_xcat_dep {
# <os> built for only the OTHER arch slip through unseen -- finalize then never cross-populated
# that cell and still exited 0 (PR #62 review). Every discovered <os> must carry every arch below.
my %os;
my $native_cells = 0;
for my $a (@GENESIS_ARCHES) {
my $root = $repo{ $a->{arch} }
// die "FATAL: [finalize] no repo root configured for arch '$a->{arch}' (wire it in %repo)\n";
$os{ basename($_) } = 1 for grep { -d "$_/$a->{arch}" } glob("$root/*");
for my $dir (grep { -d "$_/$a->{arch}" } glob("$root/*")) {
my $name = basename($dir);
if ($name =~ /^openeuler/) {
$native_cells++;
next;
}
$os{$name} = 1;
}
}
if (!%os && $native_cells) {
print "[finalize] openEuler repositories do not use cross-arch Genesis; skipping\n";
return;
}
my $pairs = 0;
+67 -9
View File
@@ -7,6 +7,11 @@ use File::Basename qw(dirname basename);
use File::Copy qw(copy);
use File::Path qw(make_path remove_tree);
use Getopt::Long qw(GetOptions);
use POSIX qw(strftime);
use FindBin;
use lib "$FindBin::Bin/..", "$FindBin::Bin/../lib";
use MockBuildUtils qw(openeuler_build_target openeuler_repo_subdir);
use XCAT::BuildUtils qw(digest_file read_lines);
my $script_dir = abs_path(dirname(__FILE__));
my $repo_root = abs_path("$script_dir/..");
@@ -46,11 +51,18 @@ die "Run as root (current uid=$>)\n" if $> != 0;
my $arch = capture('uname -m');
if (!$mock_cfg) {
my $os_id = capture(q{bash -lc 'source /etc/os-release; echo $ID'});
$mock_cfg = resolve_mock_cfg($os_id, '10', $arch);
if (lc($os_id) eq 'openeuler') {
my $os_version = capture(q{bash -lc 'source /etc/os-release; echo "$VERSION"'});
$mock_cfg = openeuler_build_target({ ID => $os_id, VERSION => $os_version }, $arch);
} else {
$mock_cfg = resolve_mock_cfg($os_id, '10', $arch);
}
}
my $native_repo = openeuler_repo_subdir($mock_cfg);
my ($rel) = $mock_cfg =~ /-(\d+)-/;
$rel //= '10';
my $dist_suffix = defined($native_repo) ? '' : ".el$rel";
# --target-arch names the arch of the rpm to produce. It differs from the host arch only for a
# forcearch target (rocky-10-riscv64-xcat on an x86_64 host; see BUILD.md "riscv64").
@@ -58,12 +70,17 @@ $target_arch = $arch if $target_arch eq '';
my %goarch = (x86_64 => 'amd64', aarch64 => 'arm64', ppc64le => 'ppc64le', s390x => 's390x', riscv64 => 'riscv64');
my $cross = $target_arch ne $arch;
die "No GOARCH known for target arch $target_arch\n" if $cross && !exists $goarch{$target_arch};
if (defined($native_repo)) {
my ($native_arch) = $native_repo =~ m{/([^/]+)$};
die "openEuler goconserver requires a native $native_arch builder\n"
if $cross || $arch ne $native_arch;
}
# For the host arch the Go compile happens INSIDE the mock chroot (BuildRequires: golang), so the
# host only fetches the pinned source and drives mock. A forcearch chroot would run that compile
# under qemu, so the cross build instead cross-compiles on the host and packages the result with
# rpmbuild --target.
for my $bin (qw(git rpm), ($cross ? qw(go rpmbuild) : qw(mock))) {
for my $bin (qw(git rpm), (defined($native_repo) ? 'wget' : ()), ($cross ? qw(go rpmbuild) : qw(mock))) {
run("command -v " . sh_quote($bin) . " >/dev/null 2>&1");
}
@@ -83,13 +100,21 @@ unless ($SOURCE_DATE_EPOCH && $SOURCE_DATE_EPOCH =~ /^\d+$/) {
chomp $SOURCE_DATE_EPOCH;
}
$SOURCE_DATE_EPOCH = time() unless $SOURCE_DATE_EPOCH =~ /^\d+$/;
if (defined($native_repo)) {
my $native_epoch = defined($build_timestamp) ? $build_timestamp : $ENV{SOURCE_DATE_EPOCH};
if (defined($native_epoch)) {
die "Invalid native build timestamp: $native_epoch\n" unless $native_epoch =~ /\A\d+\z/;
$SOURCE_DATE_EPOCH = $native_epoch;
}
}
$ENV{SOURCE_DATE_EPOCH} = $SOURCE_DATE_EPOCH;
# goconserver is a CGO-free static Go binary. el8/el9 chroots ship a Go too old to build 0.3.3, so
# always COMPILE in the el10 chroot for this arch (regardless of the target EL), then ship the static
# binary to every EL repo. The Release still carries the target's dist tag (4.el$rel) so each EL repo
# gets a correctly-named, byte-identical rpm.
(my $build_cfg = $mock_cfg) =~ s/-\d+-/-10-/;
my $build_cfg = $mock_cfg;
$build_cfg =~ s/-\d+-/-10-/ unless defined($native_repo);
print_step("Configuration");
print "repo_root: $repo_root\n";
@@ -97,8 +122,8 @@ print "pkg_dir: $pkg_dir\n";
print "work_dir: $work_dir\n";
print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg (target dist tag: el$rel)\n";
print "build_cfg: $build_cfg (el10 -- portable static build for arch $arch)\n" if !$cross;
print "mock_cfg: $mock_cfg (target dist suffix: $dist_suffix)\n";
print "build_cfg: $build_cfg\n" if !$cross;
print "arch: $arch\n";
print "target_arch: $target_arch" . ($cross ? " (GOARCH=$goarch{$target_arch}, rpmbuild --target)" : '') . "\n";
print "version: $version\n";
@@ -121,6 +146,15 @@ run("git -C " . sh_quote($src_dir) . " remote add origin " . sh_quote($go_repo)
run("git -C " . sh_quote($src_dir) . " fetch --depth 1 origin " . sh_quote($go_ref) . " >>$clone_log 2>&1");
run("git -C " . sh_quote($src_dir) . " checkout -q FETCH_HEAD >>$clone_log 2>&1");
my $go_ldflags = '-X main.Version=%{version}';
if (defined($native_repo)) {
my $source_commit = capture("git -C " . sh_quote($src_dir) . " rev-parse --verify 'HEAD^{commit}'");
die "Cannot resolve fetched goconserver commit\n"
if $? != 0 || $source_commit !~ /\A[0-9a-f]{40}\z/;
my $build_time = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($SOURCE_DATE_EPOCH));
$go_ldflags .= " -X main.Commit=$source_commit -X main.BuildTime=$build_time";
}
# etcd storage backend has broken deps with modern Go modules; xCAT only uses file storage.
unlink "$src_dir/storage/etcd.go";
remove_tree("$src_dir/storage/etcd") if -d "$src_dir/storage/etcd";
@@ -190,6 +224,26 @@ run("tar --sort=name --owner=0 --group=0 --mtime=\@$SOURCE_DATE_EPOCH" .
write_file("$sources_dir/goconserver.service", $service_unit);
write_file("$sources_dir/server.conf", $server_conf);
my ($go_source, $go_prep, $go_environment) = ('', '', '');
my $native_changelog = '';
my $go_requires = 'golang';
if (defined($native_repo)) {
my $go_file = "go1.25.12.linux-$goarch{$arch}.tar.gz";
my %checksums = map { my ($hash, $name) = split /\s+/, $_; ($name, $hash) }
read_lines("$pkg_dir/toolchains/go1.25.12.sha256");
my $go_hash = $checksums{$go_file};
die "No pinned checksum for $go_file\n" unless defined($go_hash) && $go_hash =~ /\A[0-9a-f]{64}\z/;
my $go_url = "https://go.dev/dl/$go_file";
my $go_archive = "$sources_dir/$go_file";
run("wget --https-only --tries=3 --timeout=60 -q -O " . sh_quote($go_archive) . " " . sh_quote($go_url));
die "Go toolchain checksum mismatch: $go_file\n" unless digest_file($go_archive) eq $go_hash;
$go_source = "Source3: $go_url\n";
$go_requires = 'coreutils tar gzip ca-certificates';
$go_prep = "echo '$go_hash %{SOURCE3}' | sha256sum -c -\ntar -C %{_builddir} -xzf %{SOURCE3}\n";
$go_environment = 'export PATH=%{_builddir}/go/bin:$PATH' . "\ngo version\n";
$native_changelog = "* Tue Sep 08 2026 xCAT build - $version-4\n- Build in the native openEuler target with the verified Go 1.25.12 source archive.\n\n";
}
# --- Spec: the Go compile runs in %build INSIDE the chroot; modules fetched from the proxy, pinned by go.sum ---
print_step("Write spec");
my $spec_file = "$work_dir/goconserver.spec";
@@ -199,7 +253,7 @@ write_file($spec_file, <<"SPEC");
%global debug_package %{nil}
Name: goconserver
Version: $version
Release: 4.el$rel$release_suffix
Release: 4$dist_suffix$release_suffix
Summary: Console server written in Go for xCAT
License: EPL-1.0
URL: https://github.com/xcat2/goconserver
@@ -208,8 +262,9 @@ BuildArch: $arch
Source0: goconserver-%{version}.tar.gz
Source1: goconserver.service
Source2: server.conf
$go_source
BuildRequires: golang
BuildRequires: $go_requires
%description
goconserver is a scalable console server written in Go. It provides
@@ -217,15 +272,17 @@ console logging and management for xCAT cluster nodes.
%prep
%setup -q -n goconserver-%{version}
$go_prep
%build
# Compile in-chroot. Modules are downloaded from the Go proxy at build time (mock networking is on)
# but PINNED + integrity-checked by the committed go.sum, so the build is reproducible without a
# vendored tree. GOTOOLCHAIN=local pins the chroot's Go (never auto-downloads a toolchain).
$go_environment
export GOFLAGS=-mod=mod GOTOOLCHAIN=local CGO_ENABLED=0
export GOCACHE=%{_builddir}/.gocache GOPATH=%{_builddir}/.gopath GOMODCACHE=%{_builddir}/.gomodcache
go build -trimpath -buildvcs=false -ldflags "-X main.Version=%{version}" -o goconserver goconserver.go
go build -trimpath -buildvcs=false -ldflags "-X main.Version=%{version}" -o congo cmd/congo.go
go build -trimpath -buildvcs=false -ldflags "$go_ldflags" -o goconserver goconserver.go
go build -trimpath -buildvcs=false -ldflags "$go_ldflags" -o congo cmd/congo.go
%install
install -Dm0755 goconserver %{buildroot}/usr/bin/goconserver
@@ -243,6 +300,7 @@ mkdir -p %{buildroot}/var/log/goconserver %{buildroot}/var/lib/goconserver
%dir /var/lib/goconserver
%changelog
$native_changelog
* Mon Aug 10 2026 xCAT build - $version-4.el$rel
- Build inside a mock chroot (no host build). Modules are downloaded at build time but pinned +
integrity-checked by a committed go.sum (no `go mod tidy`, no vendored tree). Compiled in the
+12
View File
@@ -0,0 +1,12 @@
# Native openEuler Go build input
The native openEuler RPM build uses Go 1.25.12 inside the exact target mock root.
The committed module graph requires this version. The build retains GOTOOLCHAIN=local and CGO_ENABLED=0.
The checksums in go1.25.12.sha256 come from the [official Go release list](https://go.dev/dl/#go1.25.12).
The builder verifies the archive before including it as Source3, and the generated spec verifies it again before extraction.
The toolchain stays in the RPM build directory and is excluded from the goconserver binary package.
The source RPM contains the exact compiler archive, including its Go sources and license, for subsequent rebuilds.
Build x86_64 and ppc64le packages on matching native architecture builders. The package release retains the native empty dist suffix.
Existing EL compilation and cross-build paths retain their original toolchain selection.
+2
View File
@@ -0,0 +1,2 @@
234828b7a89e0e303d2556310ee549fbcf253d28de937bac3da13d6294262ac1 go1.25.12.linux-amd64.tar.gz
64adb4ddefef4f0a6f11af550547f39bf510350da69ab308438a21eacfde97ad go1.25.12.linux-ppc64le.tar.gz
+3
View File
@@ -30,6 +30,9 @@ grub2-xcat provides some grub2 resources generated by grub2-mknetdir,including g
and the EL grub2 UEFI image for riscv64 nodes, which boot through UEFI and grub2 only.
%define _binaries_in_noarch_packages_terminate_build 0
%if 0%{?openEuler}
%global __strip /bin/true
%endif
%prep
#cp ./grub2-res.tar.gz /root/rpmbuild/SOURCES/
+291
View File
@@ -0,0 +1,291 @@
package XCAT::NativeInputs;
use strict;
use warnings;
use Cwd qw(abs_path);
use Digest::SHA ();
use Exporter qw(import);
use File::Basename qw(basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use JSON::PP;
our @EXPORT_OK = qw(load_inputs stage_inputs verify_input rpm_identity validate_outputs publisher_trust);
sub read_file {
my ($path) = @_;
open my $fh, '<', $path or die "Cannot read $path: $!\n";
local $/;
my $data = <$fh>;
close $fh or die "Cannot close $path: $!\n";
return $data;
}
sub sha256 {
my ($path) = @_;
open my $fh, '<', $path or die "Cannot read $path: $!\n";
binmode $fh;
my $sha = Digest::SHA->new(256)->addfile($fh)->hexdigest;
close $fh or die "Cannot close $path: $!\n";
return $sha;
}
sub capture {
my (@args) = @_;
open my $fh, '-|', @args or die "Cannot execute $args[0]: $!\n";
local $/;
my $out = <$fh> // '';
close $fh or die "Command failed: @args\n";
$out =~ s/\s+\z//;
return $out;
}
sub run {
my (@args) = @_;
system(@args) == 0 or die "Command failed: @args\n";
}
sub pinned_file {
my ($root, $entry) = @_;
die "Invalid pinned path\n" unless ($entry->{path} // '') =~ m{\A[\w./-]+\z}
&& $entry->{path} !~ m{(?:\A|/)\.\.(?:/|\z)|\A/};
die "Missing input $entry->{path}\n" unless -f "$root/$entry->{path}";
my $path = abs_path("$root/$entry->{path}") // die "Missing input $entry->{path}\n";
die "Input escapes repository: $entry->{path}\n" unless index($path, "$root/") == 0;
die "Input SHA256 mismatch: $entry->{path}\n" unless sha256($path) eq ($entry->{sha256} // '');
return $path;
}
sub load_inputs {
my ($root, $required) = @_;
$root = abs_path($root) // die "Missing repository\n";
my $catalog_path = "$root/openeuler/24.03-ppc64le.inputs.json";
my $catalog = JSON::PP->new->decode(read_file($catalog_path));
die "Unsupported native input catalog\n" unless ($catalog->{version} // 0) == 1
&& ($catalog->{target} // '') eq 'openeuler-24.03-ppc64le';
my $key = $catalog->{publisher_key};
die "Invalid publisher fingerprint\n" unless ($key->{fingerprint} // '') =~ /\A[0-9A-F]{40}\z/;
my $key_path = pinned_file($root, $key);
my (%nodes, %outputs);
for my $node (@{$catalog->{inputs}}) {
my $name = $node->{name} // '';
die "Invalid native input name '$name'\n" unless $name =~ /\A[\w+.-]+\z/;
die "Duplicate native input '$name'\n" if $nodes{$name};
my $type = $node->{type} // '';
die "Invalid native input type '$type'\n" unless $type =~ /\A(?:srpm|publisher|owner)\z/;
if ($type eq 'owner') {
die "Unsupported native build owner '$name'\n" unless grep { $_ eq $name }
qw(goconserver grub2-xcat ipmitool-xcat syslinux-xcat xnba-undi xCAT-genesis-base
perl-Crypt-Rijndael perl-Crypt-SSLeay perl-HTTP-Async perl-IO-Stty perl-Net-HTTPS-NB perl-Net-Telnet);
}
if ($type ne 'publisher') {
my $uid = $type eq 'owner' && ($name eq 'xnba-undi' || $name eq 'xCAT-genesis-base') ? 0 : 1000;
die "Invalid native build UID for $name\n" unless ($node->{build_uid} // -1) == $uid;
}
if ($type ne 'owner') {
die "Invalid pinned native URL for $name\n" unless ($node->{url} // '') =~
m{\Ahttps://repo\.openeuler\.org/openEuler-24\.03-LTS(?:-SP3)?/[A-Za-z0-9_./+-]+\.rpm\z};
die "Invalid native SHA256 for $name\n" unless ($node->{sha256} // '') =~ /\A[0-9a-f]{64}\z/;
die "Publisher binary must be exact GA: $name\n" if $type eq 'publisher'
&& $node->{url} !~ m{/openEuler-24\.03-LTS/.*\.noarch\.rpm\z};
}
die "Missing output ownership for $name\n" unless ref($node->{outputs}) eq 'ARRAY' && @{$node->{outputs}};
for my $output (@{$node->{outputs}}) {
die "Invalid native output name\n" unless $output =~ /\A[\w+.-]+\z/;
die "Conflicting output ownership: $output\n" if $outputs{$output};
$outputs{$output} = $name;
}
die "Invalid publisher outputs for $name\n" if $type eq 'publisher'
&& (@{$node->{outputs}} != 1 || $node->{outputs}[0] ne $name);
for my $patch (@{$node->{patches} // []}) {
die "Only source inputs accept patches\n" unless $type eq 'srpm';
$patch->{absolute_path} = pinned_file($root, $patch);
}
for my $define (@{$node->{defines} // []}) {
die "Invalid native spec definition for $name\n" unless $type eq 'srpm'
&& $define =~ /\A(?:llvmjit|external_libpq|runselftest|test) [01]\z/;
}
$nodes{$name} = $node;
}
for my $name (sort keys %nodes) {
my $type = $nodes{$name}{type};
for my $dependency (@{$nodes{$name}{needs} // []}) {
die "Missing native dependency '$dependency'\n" unless $nodes{$dependency};
die "Unsupported native execution edge: $name -> $dependency\n"
if $type eq 'publisher' || $nodes{$dependency}{type} eq 'owner';
}
}
my (%mark, @order);
my $visit;
$visit = sub {
my ($name) = @_;
die "Missing native dependency '$name'\n" unless $nodes{$name};
die "Cyclic native dependency at '$name'\n" if ($mark{$name} // '') eq 'visiting';
return if $mark{$name};
$mark{$name} = 'visiting';
$visit->($_) for @{$nodes{$name}{needs} // []};
$mark{$name} = 'done';
push @order, $name;
};
$visit->($_) for sort keys %nodes;
my %selected;
my $select;
$select = sub {
my ($name) = @_;
die "Missing native dependency '$name'\n" unless $nodes{$name};
return if $selected{$name}++;
$select->($_) for @{$nodes{$name}{needs} // []};
};
for my $output (sort keys %$required) {
my $owner = $outputs{$output} // ($nodes{$output} ? $output : undef);
die "No native output owner for '$output'\n" unless $owner;
$select->($owner);
}
$select->($_) for @{$catalog->{build_inputs} // []};
return {catalog => $catalog, nodes => \%nodes, outputs => \%outputs,
order => [grep { $selected{$_} } @order], selected => \%selected,
publisher_key => $key_path, publisher_fingerprint => $key->{fingerprint},
catalog_sha256 => sha256($catalog_path)};
}
sub rpm_identity {
my ($path) = @_;
my @fields = split /\n/, capture('rpm', '-qp', '--qf',
'%{NAME}\n%{ARCH}\n%{SOURCEPACKAGE}\n%{RELEASE}\n', $path);
die "Invalid RPM header: $path\n" unless @fields == 4;
return {name => $fields[0], arch => $fields[1], source => $fields[2] eq '1', release => $fields[3]};
}
sub read_exact {
my ($fh, $size) = @_;
my $data = '';
while (length($data) < $size) {
my $got = read($fh, $data, $size - length($data), length($data));
die "Truncated RPM payload\n" unless defined($got) && $got > 0;
}
return $data;
}
sub reject_elf_payload {
my ($path) = @_;
open my $fh, '-|', 'rpm2cpio', $path or die "Cannot read RPM payload: $!\n";
binmode $fh;
my $error;
eval {
while (1) {
my $header = read_exact($fh, 110);
die "Invalid RPM cpio header\n" unless $header =~ /\A07070[12][0-9A-Fa-f]{104}\z/;
my @fields = map { hex($_) } $header =~ /\A.{6}(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})\z/s;
my ($size, $namesize) = @fields[6, 11];
die "Invalid RPM cpio filename\n" unless $namesize > 0 && $namesize <= 1048576;
my $name = read_exact($fh, $namesize);
die "Invalid RPM cpio filename terminator\n" unless $name =~ s/\0\z//;
read_exact($fh, (4 - (110 + $namesize) % 4) % 4);
last if $name eq 'TRAILER!!!' && $size == 0;
my $prefix = read_exact($fh, $size < 4 ? $size : 4);
die "ELF payload in publisher noarch RPM: $name\n" if $prefix eq "\x7fELF";
$size -= length($prefix);
while ($size) { my $count = $size < 65536 ? $size : 65536; read_exact($fh, $count); $size -= $count; }
read_exact($fh, (4 - $fields[6] % 4) % 4);
}
my $tail;
while (read($fh, $tail, 65536)) { die "Unexpected data after RPM cpio trailer\n" if $tail =~ /[^\0]/; }
1;
} or $error = $@;
my $closed = close($fh);
die $error if $error;
die "rpm2cpio failed: $path\n" unless $closed;
}
sub verify_input {
my ($plan, $node, $path, $db) = @_;
die "Native input SHA256 mismatch: $path\n" unless sha256($path) eq $node->{sha256};
my $out = capture('rpmkeys', '--dbpath', $db, '--checksig', '--verbose', $path);
die "Publisher signature missing or invalid: $path\n" unless $out =~ /Signature.*: OK/i
&& $out !~ /NOKEY|NOT OK|BAD|UNSIGNED/i;
my $id = rpm_identity($path);
die "Native input NAME mismatch: $path\n" unless $id->{name} eq $node->{name};
if ($node->{type} eq 'publisher') {
die "Publisher input is not a noarch binary: $path\n" if $id->{source} || $id->{arch} ne 'noarch';
die "Publisher input is not exact GA: $path\n" unless $id->{release} =~ /\.oe2403\z/;
reject_elf_payload($path);
} else {
die "Native input is not a source RPM: $path\n" unless $id->{source};
}
die "Native input changed during verification: $path\n" unless sha256($path) eq $node->{sha256};
return $id;
}
sub publisher_trust {
my ($plan, $work) = @_;
make_path("$work/trust", "$work/gnupg");
chmod 0700, "$work/gnupg";
my $listing = capture('gpg', '--homedir', "$work/gnupg", '--batch', '--with-colons', '--show-keys', $plan->{publisher_key});
my @primary;
my $pub;
for my $line (split /\n/, $listing) {
my @fields = split /:/, $line;
$pub = 1 if $fields[0] eq 'pub';
if ($pub && $fields[0] eq 'fpr') { push @primary, $fields[9]; $pub = 0; }
}
die "Publisher public key fingerprint mismatch\n" unless @primary == 1 && $primary[0] eq $plan->{publisher_fingerprint};
run('rpmkeys', '--dbpath', "$work/trust", '--import', $plan->{publisher_key});
return "$work/trust";
}
sub stage_inputs {
my ($plan, $work) = @_;
die "Native input staging already exists: $work\n" if -e $work;
make_path($work);
my $db = publisher_trust($plan, $work);
my @ledger;
for my $name (@{$plan->{order}}) {
my $node = $plan->{nodes}{$name};
next if $node->{type} eq 'owner';
make_path("$work/$name");
for my $patch (@{$node->{patches} // []}) {
make_path("$work/$name/patches");
my $staged = "$work/$name/patches/" . basename($patch->{path});
die "Conflicting staged patch: $staged\n" if -e $staged;
copy($patch->{absolute_path}, $staged) or die "Cannot stage native patch: $!\n";
die "Staged patch SHA256 mismatch: $staged\n" unless sha256($staged) eq $patch->{sha256};
chmod 0444, $staged or die "Cannot protect native patch: $!\n";
$patch->{staged} = $staged;
}
my $dest = "$work/$name/" . basename($node->{url});
run('wget', '--https-only', '--tries=3', '--timeout=60', '-O', "$dest.part", $node->{url});
verify_input($plan, $node, "$dest.part", $db);
rename("$dest.part", $dest) or die "Cannot preserve native input: $!\n";
chmod 0444, $dest or die "Cannot protect native input: $!\n";
$node->{staged} = $dest;
push @ledger, {name => $name, type => $node->{type}, url => $node->{url},
sha256 => $node->{sha256}, path => $dest, publisher => $plan->{publisher_fingerprint},
defines => $node->{defines} // [],
patches => [map { {path => $_->{path}, sha256 => $_->{sha256}, staged => $_->{staged}} } @{$node->{patches} // []}]};
}
open my $fh, '>', "$work/inputs.json" or die "Cannot record native input ledger: $!\n";
print {$fh} JSON::PP->new->canonical->pretty->encode({catalog_sha256 => $plan->{catalog_sha256}, inputs => \@ledger});
close $fh or die "Cannot close native input ledger: $!\n";
$plan->{trust_db} = $db;
}
sub validate_outputs {
my ($node, $paths, $require_all) = @_;
my %allowed = map { $_ => 1 } @{$node->{outputs}};
my %seen;
for my $path (@$paths) {
my $id = rpm_identity($path);
next if $id->{source};
die "Unexpected output from $node->{name}: $id->{name}\n" unless $allowed{$id->{name}};
die "Duplicate output from $node->{name}: $id->{name}\n" if $seen{$id->{name}}++;
die "Foreign output architecture: $id->{arch}\n" unless $id->{arch} eq 'ppc64le' || $id->{arch} eq 'noarch';
}
if ($require_all) {
die "Missing output from $node->{name}: $_\n" for grep { !$seen{$_} } sort keys %allowed;
}
return \%seen;
}
1;
@@ -0,0 +1,7 @@
include('templates/openeuler-20.03-sp4.tpl')
config_opts['root'] = 'openeuler-20.03sp4-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['openeuler_repository_release'] = '20.03-LTS-SP4'
config_opts['openeuler_repositories'] = ('OS', 'everything', 'update')
include('templates/openeuler-lts-xcat.tpl')
@@ -0,0 +1,7 @@
include('templates/openeuler-22.03-sp4.tpl')
config_opts['root'] = 'openeuler-22.03sp4-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['openeuler_repository_release'] = '22.03-LTS-SP4'
config_opts['openeuler_repositories'] = ('OS', 'everything', 'update')
include('templates/openeuler-lts-xcat.tpl')
+15
View File
@@ -0,0 +1,15 @@
config_opts['root'] = 'openeuler-24.03-ppc64le'
config_opts['target_arch'] = 'ppc64le'
config_opts['legal_host_arches'] = ('ppc64le',)
config_opts['releasever'] = '24.03LTS'
config_opts['package_manager'] = 'dnf'
config_opts['isolation'] = 'simple'
config_opts['chrootuid'] = 1000
config_opts['chrootgid'] = 1000
config_opts['useradd'] = '/usr/sbin/useradd -o -m -u {{chrootuid}} -g {{chrootgid}} -d {{chroothome}} -N {{chrootuser}}'
config_opts['use_bootstrap'] = False
config_opts['use_bootstrap_container'] = False
config_opts['chroot_setup_cmd'] = 'install openEuler-rpm-config openEuler-release shadow rpm-build dnf-plugins-core gcc make perl-interpreter'
config_opts['openeuler_repository_release'] = '24.03-LTS'
config_opts['openeuler_repositories'] = ('OS',)
include('templates/openeuler-lts-xcat.tpl')
@@ -0,0 +1,8 @@
include('templates/openeuler-24.03.tpl')
config_opts['root'] = 'openeuler-24.03sp1-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['releasever'] = '24.03LTS_SP1'
config_opts['openeuler_repository_release'] = '24.03-LTS-SP1'
config_opts['openeuler_repositories'] = ('OS', 'everything', 'update')
include('templates/openeuler-lts-xcat.tpl')
@@ -0,0 +1,8 @@
include('templates/openeuler-24.03.tpl')
config_opts['root'] = 'openeuler-24.03sp3-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['releasever'] = '24.03LTS_SP3'
config_opts['openeuler_repository_release'] = '24.03-LTS-SP3'
config_opts['openeuler_repositories'] = ('OS', 'everything', 'update')
include('templates/openeuler-lts-xcat.tpl')
@@ -0,0 +1,8 @@
include('templates/openeuler-24.03.tpl')
config_opts['root'] = 'openeuler-24.03sp4-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['releasever'] = '24.03LTS_SP4'
config_opts['openeuler_repository_release'] = '24.03-LTS-SP4'
config_opts['openeuler_repositories'] = ('OS', 'everything', 'update')
include('templates/openeuler-lts-xcat.tpl')
@@ -0,0 +1,31 @@
config_opts['dist'] = ''
config_opts['use_bootstrap_image'] = False
config_opts['description'] = 'openEuler ' + config_opts['openeuler_repository_release']
config_opts['dnf.conf'] = """
[main]
keepcache=1
reposdir=/dev/null
logfile=/var/log/dnf.log
retries=20
obsoletes=1
gpgcheck=1
assumeyes=1
metadata_expire=0
best=1
install_weak_deps=0
skip_if_unavailable=0
protected_packages=
"""
_openeuler_root = 'https://repo.openeuler.org/openEuler-' + config_opts['openeuler_repository_release']
_openeuler_arch = config_opts['target_arch']
for _openeuler_repo in config_opts['openeuler_repositories']:
config_opts['dnf.conf'] += """
[{repo}]
name=openEuler {release} {repo}
baseurl={root}/{repo}/{arch}/
enabled=1
gpgcheck=1
gpgkey={root}/OS/{arch}/RPM-GPG-KEY-openEuler
skip_if_unavailable=0
""".format(repo=_openeuler_repo, release=config_opts['openeuler_repository_release'],
root=_openeuler_root, arch=_openeuler_arch)
+330 -62
View File
@@ -9,24 +9,29 @@ use File::Copy qw(copy);
use File::Find qw(find);
use File::Glob qw(bsd_glob);
use File::Path qw(make_path remove_tree);
use File::Spec;
use File::Temp qw(tempdir tempfile);
use Getopt::Long qw(GetOptions);
use Parallel::ForkManager;
use POSIX qw(strftime);
use JSON::PP;
use FindBin qw($RealBin);
use lib $RealBin, "$RealBin/lib";
use XCAT::NFSLock ();
use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs rpm_in_cell resolve_mock_cfg
carry_over_rpms rpm_name rpm_arch rpm_source_rpm rpm_digests_ok
install_deps_packages install_deps_command missing_perl_modules
read_manifest verify_repo_packages verify_repo_signature verify_rpm_signatures
read_manifest derive_target_from_repo_path
verify_repo_packages verify_repo_signature verify_rpm_signatures
rpm_version rpm_release rpm_sigmd5 restamp_release_line
cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix
build_mock_uniqueext rpmkeys_checksig_problem);
build_mock_uniqueext rpmkeys_checksig_problem
openeuler_build_target openeuler_repo_subdir);
# print_step and sh_quote come from MockBuildUtils above; XCAT::BuildUtils carries the same
# print_step, so it is deliberately NOT imported here (one definition, no redefinition warning).
use XCAT::BuildUtils qw(
capture_command
digest_file
every_step_failed
forward_signals_to_workers
block_handled_signals
@@ -47,6 +52,7 @@ use XCAT::GenesisRelease qw(
validated_release_checksums
verify_release_file
);
use XCAT::NativeInputs qw(load_inputs stage_inputs verify_input rpm_identity validate_outputs publisher_trust);
# --- Mount-namespace isolation: guard the host cgroup against mock teardown propagation ----------
# mock mounts /sys/fs/cgroup into every build chroot. On these systemd build hosts every mount is
@@ -149,6 +155,7 @@ my $no_verify_repo = 0;
my @HELD_LOCKS;
my $LOCK_OWNER_PID;
my ($COMMON_STAGE, $COMMON_DESTINATION, $COMMON_BACKUP);
my %NATIVE_PLANS;
for my $sig (qw(INT TERM HUP)) {
$SIG{$sig} = sub { exit 1; };
}
@@ -264,7 +271,10 @@ if ($finalize_xcat_dep) {
@finalize_arch = qw(x86_64 ppc64le) unless @finalize_arch;
my %cell;
for my $root ($x86, $ppc) {
$cell{ abs_path($_) } = 1 for grep { -d } map { glob("$root/*/$_") } @finalize_arch;
my @os = grep { -d && basename($_) !~ /^openeuler/ } glob("$root/*");
for my $os (@os) {
$cell{ abs_path($_) } = 1 for grep { -d } map { "$os/$_" } @finalize_arch;
}
}
take_lock(cell_lock_path($_), 'repository cell lock') for sort keys %cell;
# Inject the per-rpm gpg re-sign and the repo re-index as callbacks so the finalize logic in
@@ -375,6 +385,9 @@ if ($install_deps) {
die "FATAL: still missing after install: " . join(', ', @missing) . "\n" if @missing;
print " perl modules present: " . join(', ', @modules) . "\n";
print " host is ready\n";
if (lc($os_id) eq 'openeuler') {
install_mock_cfg(basename($_, '.cfg')) for glob("$repo_root/mock-configs/openeuler-*.cfg");
}
exit 0;
}
@@ -410,10 +423,15 @@ if ($genesis_release ne '') {
# ONLY the host arch (uname -m) -- the other arch is produced on its own build host --
# except for the forcearch targets (%forcearch_targets, --target only), which are
# cross-built here through qemu-user-static.
my $native_target = openeuler_build_target(\%os, $host_arch);
my @build_targets = $target
? ($target)
: defined($native_target) ? ($native_target)
: map { resolve_mock_cfg($os_id, $_, $host_arch) } (8, 9, 10);
die "openEuler repository publication requires --gpg-sign\n"
if !$dry_run && !$gpg_sign && grep { defined(openeuler_repo_subdir($_)) } @build_targets;
# What a target builds. The mock-core-configs targets (<os>+epel-<rel>-<arch>) build every
# dep natively on the host arch. The forcearch targets shipped in mock-configs/ cross-build
# another arch that has no EPEL: the EPEL-only perl deps of xCAT are built for it
@@ -432,8 +450,7 @@ my %forcearch_targets = (
},
);
# Each target deploys one cell, <repo-dep>/rh<rel>/<arch>, and locks only that cell: the per-arch
# runs of one build share --repo-dep and never wait on each other.
# Lock each target's repository cell so architecture builds can share --repo-dep.
my @cell_locks = map {
my $cell = target_cell($_);
make_path(dirname($cell));
@@ -539,6 +556,14 @@ sub build_one_target {
my %req = %{ $MANIFEST{$target} // {} };
die "FATAL: no manifest section for target '$target' in packages-manifest.conf\n"
if !%req;
my $native = $target eq 'openeuler-24.03-ppc64le' ? load_inputs($repo_root, \%req) : undef;
$NATIVE_PLANS{$target} = $native if $native;
if ($native) {
die "Native POWER collection requires signing and verification\n"
if !$gpg_sign || $no_verify_repo;
die "Native POWER inputs require a complete owner run\n"
if $skip_build || $skip_xcat_dep || $skip_perl || @extra_collect_dirs;
}
my $run_root = "$output_root/$run_id";
my $build_root = "$run_root/build-results";
@@ -573,6 +598,8 @@ my @dep_builders = (
{ name => 'goconserver', script => "$repo_root/goconserver/mockbuild.pl" },
{ name => 'conserver-xcat', script => "$repo_root/conserver/mockbuild.pl" },
{ name => 'xnba-undi', script => "$repo_root/xnba/mockbuild.pl", noarch => 1 },
{ name => 'python3-scp', srpm => "$repo_root/python-scp/python-scp-0.14.5-1.oe2403.src.rpm",
sha256 => '3461d2a3fe0122cac2893d8465ad1271ae21e5570a31d4402e3f887ef545a0e8' },
{ name => 'ipxe-xcat', script => "$repo_root/ipxe-xcat/mockbuild.pl", noarch => 1 },
);
my %profile_builds = map { $_ => 1 } @{ $profile->{dep_builders} };
@@ -587,11 +614,16 @@ die "Missing xCAT build script: $xcat_src/buildrpms.pl\n"
my @active_dep_builders;
for my $b (@dep_builders) {
next if !$profile_builds{$b->{name}};
if ($b->{srpm}) {
push @active_dep_builders, $b if $req{$b->{name}};
next;
}
if (-f $b->{script}) {
push @active_dep_builders, $b;
next;
}
print "WARN: missing dep builder script, skipping: $b->{script}\n";
die "Missing native owner script: $b->{script}\n" if $native && $req{$b->{name}};
}
die "Missing perl builder script: $perl_builder\n"
if !$skip_perl && !$perl_builder;
@@ -645,9 +677,35 @@ print "srpm_repo_dir: $srpm_repo_dir\n";
print "srpm_tarball: $srpm_tarball\n";
my @collect_roots;
my @native_source_roots;
if ($native && !$dry_run) {
stage_inputs($native, "$run_root/native-inputs");
}
if (!$skip_build && !$skip_xcat_dep) {
for my $builder (grep { $_->{srpm} } @active_dep_builders) {
my $source = $builder->{srpm};
die "Missing source RPM: $source\n" unless -f $source;
die "Source RPM SHA256 mismatch: $source\n" unless digest_file($source) eq $builder->{sha256};
next if $dry_run;
my $stage_dir = "$run_root/source-rpms/$builder->{name}";
make_path($stage_dir);
my $staged = "$stage_dir/" . basename($source);
copy($source, $staged) or die "Cannot stage source RPM $source: $!\n";
die "Staged source RPM SHA256 mismatch: $staged\n" unless digest_file($staged) eq $builder->{sha256};
$builder->{srpm} = $staged;
}
}
install_mock_cfg($target);
if ($native) {
my ($runtime, $sources) = build_native_inputs($native, $target, \%req, $run_root, $log_root);
push @collect_roots, $runtime;
push @native_source_roots, @$sources;
}
if ($scrub_all_chroots) {
run_step(
step => "Scrub all chroots for target $target",
@@ -683,7 +741,10 @@ if (!$skip_build) {
my $step_log = "$log_root/$name";
my $step_uniqueext = build_mock_uniqueext($run_id, ++$build_step_seq, $name);
my $mock_cfg = $builder->{noarch} ? $profile->{noarch_cfg} : $target;
my $cmd = join(' ',
my $cmd = $builder->{srpm}
? source_rpm_build_command($builder, $target, $step_uniqueext, $step_result, $step_log,
"$run_root/source-rpms/$name")
: join(' ',
'perl', shell_quote($script),
'--mock-cfg', shell_quote($mock_cfg),
($profile->{forcearch} && !$builder->{noarch} ? ('--target-arch', shell_quote($arch)) : ()),
@@ -693,13 +754,10 @@ if (!$skip_build) {
# host-local, run-scoped work dir so /tmp doesn't collide between runs
'--work-dir', shell_quote("/tmp/mockbuild-all-$run_id/$name"),
'--build-timestamp', $SOURCE_DATE_EPOCH,
# goconserver generates its spec at build time (from an upstream clone), so the
# in-tree spec Release bump above cannot reach it. Hand the CD suffix down so its
# NVR advances per run too, and pin the clone to an immutable commit (not the moving
# 'master') so the build is reproducible.
($name eq 'goconserver'
? ('--go-ref', sh_quote($GOCONSERVER_REF),
($RELEASE_BUMP ne '' ? ('--release-suffix', sh_quote($RELEASE_BUMP)) : ()))
($name eq 'goconserver' ? ('--go-ref', sh_quote($GOCONSERVER_REF)) : ()),
# Generated specs need the same release suffix as in-tree specs.
(($name eq 'goconserver' || $name eq 'xnba-undi') && $RELEASE_BUMP ne ''
? ('--release-suffix', sh_quote($RELEASE_BUMP))
: ()),
);
push @build_steps, {
@@ -710,12 +768,13 @@ if (!$skip_build) {
log => "$log_root/$name/run.log",
scrub_cfg => $mock_cfg,
scrub_uniqueext => $step_uniqueext,
($native ? (native_results => {$name => $step_result}) : ()),
};
push @collect_roots, $step_result;
}
}
my @perl_pkgs = sort grep { /^perl-/ } keys %req; # manifest: perl packages required here
my @perl_pkgs = sort grep { /^perl-/ && (!$native || $native->{nodes}{$_}{type} eq 'owner') } keys %req;
if (!$skip_perl && @perl_pkgs) {
my $perl_result = "$build_root/perl/$arch";
my $perl_log = "$log_root/perl/$arch";
@@ -751,6 +810,7 @@ if (!$skip_build) {
# the per-package budget times the number of packages it builds serially per worker.
timeout => ($step_timeout ? $step_timeout * scalar(@perl_pkgs) : 0),
log => "$log_root/perl-build.log",
($native ? (native_results => {map { $_ => "$perl_result/$_" } @perl_pkgs}) : ()),
};
push @collect_roots, $perl_result;
}
@@ -793,7 +853,12 @@ if (!$skip_build) {
'--force',
'--verbose',
'--xcat_dep_path', shell_quote($repo_root),
(defined(openeuler_repo_subdir($target)) && $gpg_sign
? ('--gpg-sign', '--gpg-key-name', shell_quote($gpg_key_name),
'--gpg-home', shell_quote(File::Spec->rel2abs($gpg_home ne '' ? $gpg_home
: $ENV{GNUPGHOME} || "$ENV{HOME}/.gnupg"))) : ()),
);
$cmd = native_owner_command($native, $target, $cmd, 0) if $native;
push @build_steps, {
id => 'genesis',
step => 'Build xCAT-genesis-base (per-target, OS-dependent)',
@@ -801,10 +866,16 @@ if (!$skip_build) {
cwd => $xcat_src,
log => "$log_root/genesis-build.log",
scrub_cfg => "xCAT-genesis-base-$target",
($native ? (native_results => {'xCAT-genesis-base' => "$xcat_src/dist/$target/rpms"}) : ()),
};
}
if (@build_steps) {
if ($native) {
for my $step (grep { $_->{id} ne 'genesis' } @build_steps) {
$step->{cmd} = native_owner_command($native, $target, $step->{cmd}, 1000);
}
}
# Prefer the caller-supplied cap (global budget / active targets). Fall back to the old
# behaviour (all steps at once) only when unset.
my $effective_parallel_builds =
@@ -850,6 +921,20 @@ if (!$skip_build) {
# -- 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;
if ($native && !$dry_run) {
for my $step (@build_steps) {
for my $name (sort keys %{$step->{native_results} // {}}) {
my $directory = $step->{native_results}{$name};
die "Missing native owner result: $directory\n" unless -d $directory;
my @rpms;
find({no_chdir => 1, wanted => sub {
push @rpms, $File::Find::name if -f $_ && /\.rpm\z/ && !/\.src\.rpm\z/
&& ($name ne 'xCAT-genesis-base' || basename($_) =~ /^xCAT-genesis-base-/);
}}, $directory);
validate_outputs($native->{nodes}{$name}, \@rpms, 1);
}
}
}
}
}
@@ -870,6 +955,7 @@ if ($skip_build) {
push @collect_roots, @extra_collect_dirs;
@collect_roots = uniq(@collect_roots);
my @srpm_collect_roots = uniq(@collect_roots);
push @srpm_collect_roots, @native_source_roots;
if ($genesis_release && !$dry_run) {
remove_genesis_packages($repo_dir, 0);
@@ -945,6 +1031,7 @@ if (!$dry_run && $RELEASE_BUMP ne '') {
my @rmiss;
for my $pkg (required_pkgs([sort keys %req], $skip_genesis, $skip_perl, $skip_xcat_dep)) {
next if $pkg eq 'xCAT-genesis-base';
next if $native && $native->{nodes}{$pkg} && $native->{nodes}{$pkg}{type} eq 'publisher';
my $rel = rpm_release($repo_dir, $pkg);
next if !defined $rel; # a missing rpm is caught by the completeness gate in deploy_target
push @rmiss, "$pkg: Release '$rel' is missing the CD bump '$RELEASE_BUMP'"
@@ -1050,6 +1137,21 @@ print "SRPM Tarball: $srpm_tarball\n" if !$skip_tarball;
# which dep builders run and which rpms the deployed repo must contain.
sub target_profile {
my ($target) = @_;
if (my $native = openeuler_repo_subdir($target)) {
my ($version, $arch) = $target =~ /\Aopeneuler-(.*)-([^-]+)\z/;
die "Native target '$target' requires a $arch build host, found $host_arch\n"
unless $arch eq $host_arch;
return {
rel => $version,
arch => $arch,
noarch_cfg => $target,
forcearch => 0,
epel => 0,
dep_builders => [qw(grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi python3-scp)],
required => [qw(ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB)],
};
}
if (my $fa = $forcearch_targets{$target}) {
return {
%{$fa},
@@ -1079,9 +1181,11 @@ sub target_profile {
# overwrite one the host already has.
sub install_mock_cfg {
my ($cfg) = @_;
my $src = "$repo_root/mock-configs/$cfg.cfg";
install_mock_cfg('templates/openeuler-lts-xcat') if $cfg =~ /^openeuler-/;
my $extension = $cfg =~ m{^templates/} ? 'tpl' : 'cfg';
my $src = "$repo_root/mock-configs/$cfg.$extension";
return if !-f $src;
my $dst = "/etc/mock/$cfg.cfg";
my $dst = "/etc/mock/$cfg.$extension";
if (-f $dst) {
die "$dst differs from $src: the build would not use the configuration shipped in this"
. " tree. Remove or update the host copy (it is never overwritten here) and rerun.\n"
@@ -1094,15 +1198,153 @@ sub install_mock_cfg {
chmod 0644, $dst;
}
# Assemble the built per-target repo into the deployable, signed per-EL layout
# <repo-dep>/rh<rel>/<arch>: copy the binary rpms, sign, createrepo, and drop the
# xcat-dep.repo / mklocalrepo.sh / buildinfo.txt (ready to push to xcat.org).
sub source_rpm_build_command {
my ($builder, $target, $uniqueext, $result, $log, $work) = @_;
my $cfg = "$work/mock-deterministic.cfg";
if (!$dry_run) {
make_path($work, $result);
open my $fh, '>', $cfg or die "Cannot write $cfg: $!\n";
print {$fh} "include('/etc/mock/$target.cfg')\n";
print {$fh} "config_opts['environment']['SOURCE_DATE_EPOCH'] = '$SOURCE_DATE_EPOCH'\n";
close $fh or die "Cannot close $cfg: $!\n";
}
my $mock = join(' ', 'mock', '-r', sh_quote($cfg), '--uniqueext', sh_quote($uniqueext),
'--define', sh_quote('use_source_date_epoch_as_buildtime 1'),
'--define', sh_quote('clamp_mtime_to_source_date_epoch 1'),
'--define', sh_quote('_buildhost xcat-build'));
$mock .= join('', map { ' --define ' . sh_quote($_) } @{$builder->{defines} // []});
my $srpm = sh_quote($builder->{srpm});
my $prefix = '';
if ($RELEASE_BUMP ne '' || @{$builder->{patches} // []} || @{$builder->{defines} // []}) {
my $top = "$work/restamp";
if (!$dry_run) {
make_path(map { "$top/$_" } qw(BUILD BUILDROOT RPMS SOURCES SPECS SRPMS));
}
run_step(step => "Unpack source RPM: $builder->{name}",
cmd => 'rpm -i --define ' . sh_quote("_topdir $top") . " $srpm",
log => "$log/srpm-unpack.log");
my @specs = $dry_run ? ("$top/SPECS/*.spec") : bsd_glob("$top/SPECS/*.spec");
die "Expected one spec in source RPM: $builder->{srpm}\n" unless @specs == 1;
bump_dep_release_suffix($top, $RELEASE_BUMP) if !$dry_run && $RELEASE_BUMP ne '';
for my $patch (@{$builder->{patches} // []}) {
my $path = $patch->{staged} // $patch->{absolute_path};
die "Source patch SHA256 mismatch: $path\n" unless digest_file($path) eq $patch->{sha256};
run_step(step => "Apply native source patch: $builder->{name}",
cmd => 'patch --batch --fuzz=0 -p1 -d ' . sh_quote("$top/SPECS")
. ' -i ' . sh_quote($path), log => "$log/spec-patch.log");
}
my $restamped = "$work/restamp-srpm";
make_path($restamped) unless $dry_run;
$prefix = "$mock --buildsrpm --spec " . sh_quote($specs[0])
. ' --sources ' . sh_quote("$top/SOURCES") . ' --resultdir ' . sh_quote($restamped)
. ' && set -- ' . sh_quote($restamped) . '/*.src.rpm'
. ' && test "$#" -eq 1 && test -f "$1" && ';
$srpm = '"$1"';
}
return $prefix . "$mock --rebuild $srpm --resultdir " . sh_quote($result);
}
sub native_owner_command {
my ($plan, $target, $command, $uid) = @_;
my $overlay = $plan->{overlays}{$uid} // die "Missing native mock overlay\n";
my $script = 'mount --bind ' . sh_quote($overlay) . ' ' . sh_quote("/etc/mock/$target.cfg")
. ' && exec sh -c ' . sh_quote($command);
return 'unshare --mount --propagation private -- sh -c ' . sh_quote($script);
}
sub native_overlay {
my ($plan, $target, $work, $prereqs) = @_;
$plan->{overlays} = {1000 => "$work/native-1000.cfg", 0 => "$work/native-genesis-0.cfg",
procenv => "$work/native-procenv-bootstrap.cfg"};
return if $dry_run;
my $base = "$work/native-base.cfg";
copy("/etc/mock/$target.cfg", $base) or die "Cannot snapshot native mock configuration: $!\n";
my $url = $prereqs;
$url =~ s{([^A-Za-z0-9_./~-])}{sprintf('%%%02X', ord($1))}ge;
my $repo = "\n[xcat-native-inputs]\nname=xCAT native build prerequisites\nbaseurl=file://$url\n"
. "gpgkey=file://$url/repodata/repomd.xml.key\ngpgcheck=1\nrepo_gpgcheck=1\n"
. "enabled=1\nskip_if_unavailable=0\n";
for my $key (1000, 0, 'procenv') {
my $uid = $key eq 'procenv' ? 1000 : $key;
open my $fh, '>', $plan->{overlays}{$key} or die "Cannot write native overlay: $!\n";
print {$fh} 'include(' . JSON::PP->new->encode($base) . ")\n";
print {$fh} "config_opts['chrootuid'] = $uid\nconfig_opts['chrootgid'] = 1000\n";
print {$fh} "config_opts['dnf.conf'] += \"\"\"$repo\"\"\"\n";
print {$fh} "config_opts['plugin_conf']['bind_mount_enable'] = True\n";
print {$fh} "config_opts['plugin_conf']['procenv_enable'] = " . ($key eq 'procenv' ? 'False' : 'True') . "\n";
print {$fh} "config_opts['plugin_conf']['bind_mount_opts']['dirs'].append("
. '(' . JSON::PP->new->encode($prereqs) . ', ' . JSON::PP->new->encode($prereqs) . "))\n";
close $fh or die "Cannot close native overlay: $!\n";
}
open my $ledger, '>', "$work/native-overlays.json" or die "Cannot record native overlays: $!\n";
print {$ledger} JSON::PP->new->canonical->pretty->encode({base => {path => $base, sha256 => digest_file($base)},
overlays => [map { {purpose => $_, path => $plan->{overlays}{$_}, sha256 => digest_file($plan->{overlays}{$_})} } (1000, 0, 'procenv')]});
close $ledger or die "Cannot close native overlay ledger: $!\n";
}
sub build_native_inputs {
my ($plan, $target, $req, $work, $logs) = @_;
my $prereqs = "$work/native-prerequisites";
my $runtime = "$work/native-runtime";
my @source_roots;
make_path($prereqs, $runtime) unless $dry_run;
for my $name (@{$plan->{order}}) {
my $node = $plan->{nodes}{$name};
next unless $node->{type} eq 'publisher';
next if $dry_run;
verify_input($plan, $node, $node->{staged}, $plan->{trust_db});
copy($node->{staged}, "$prereqs/" . basename($node->{staged})) or die "Cannot stage publisher RPM: $!\n";
if ($req->{$name}) {
copy($node->{staged}, "$runtime/" . basename($node->{staged})) or die "Cannot collect publisher RPM: $!\n";
}
}
sign_and_index_repo($prereqs, $plan) unless $dry_run;
native_overlay($plan, $target, $work, $prereqs);
my $sequence = 0;
for my $name (@{$plan->{order}}) {
my $node = $plan->{nodes}{$name};
next unless $node->{type} eq 'srpm';
my $result = "$work/native-results/$name";
my $log = "$logs/native/$name";
my $uniqueext = build_mock_uniqueext("$target-$run_id", ++$sequence, $name);
my %builder = (%$node, srpm => $node->{staged} // "$work/native-inputs/$name/" . basename($node->{url}));
my $command = source_rpm_build_command(\%builder, $target, $uniqueext, $result, $log,
"$work/native-source/$name");
run_step(step => "Build native prerequisite: $name", log => "$log/run.log",
cmd => native_owner_command($plan, $target, $command, $name eq 'procenv' ? 'procenv' : 1000));
next if $dry_run;
my @rpms = grep { !/\.src\.rpm$/ } bsd_glob("$result/*.rpm");
validate_outputs($node, \@rpms, 1);
for my $rpm (@rpms) {
my $base = basename($rpm);
die "Conflicting native prerequisite artifact: $base\n" if -e "$prereqs/$base";
copy($rpm, "$prereqs/$base") or die "Cannot stage native prerequisite: $!\n";
my $id = rpm_identity($rpm);
copy($rpm, "$runtime/$base") or die "Cannot collect native output: $!\n" if $req->{$id->{name}};
}
sign_and_index_repo($prereqs, $plan);
my @problems = verify_rpms_checksig($prereqs, $gpg_key_name, $gpg_home, $plan);
die "Native prerequisite signature failure: @problems\n" if @problems;
open my $ledger, '>>', "$work/native-results.jsonl" or die "Cannot record native result: $!\n";
print {$ledger} JSON::PP->new->canonical->encode({name => $name, source_sha256 => $node->{sha256},
defines => $node->{defines} // [], patches => [map { {path => $_->{path}, sha256 => $_->{sha256}} } @{$node->{patches} // []}],
outputs => [map { {name => rpm_identity($_)->{name}, unsigned_path => $_,
unsigned_sha256 => digest_file($_), signed_sha256 => digest_file("$prereqs/" . basename($_))} } @rpms]}) . "\n";
close $ledger or die "Cannot close native result ledger: $!\n";
push @source_roots, $result;
scrub_buildroot($target, $uniqueext, "$log/scrub.log") unless $keep_buildroots;
}
return ($runtime, \@source_roots);
}
# Publish each target in its native repository layout with signatures, metadata and local repository helpers.
sub deploy_target {
my ($tgt, $info) = @_;
my $rel = $info->{rel};
my $src = $info->{repo_dir};
my $tarch = $info->{profile}{arch};
my $dest = target_cell($tgt);
my $subdir = File::Spec->abs2rel($dest, $repo_dep);
print_step("Deploy $tgt -> $dest");
return if $dry_run;
@@ -1130,8 +1372,8 @@ sub deploy_target {
# rpm an earlier layout left in the collection. On the STAGE, so the published cell is
# already correct when it is swapped in.
remove_genesis_packages($stage, 0) if $genesis_release;
sign_and_index_repo($stage);
write_dep_repo_metadata($stage, $rel, $tarch);
sign_and_index_repo($stage, $NATIVE_PLANS{$tgt});
write_dep_repo_metadata($stage, $rel, $tarch, $subdir);
# Automatic completeness + signature gate on the freshly signed cell -- the single
# consolidated gate (verify_target_repo, the same one --verify-repo runs). Asserts every
# manifest-required package is present at its pinned version, the repomd signature verifies,
@@ -1161,7 +1403,7 @@ sub deploy_target {
remove_tree($old) if -d $old;
my $n = scalar(grep { !/\.src\.rpm$/ } bsd_glob("$dest/*.rpm"));
print "Deployed rh$rel/$tarch: $n rpms\n";
print "Deployed $subdir: $n rpms\n";
}
sub publish_genesis_common_repo {
@@ -1299,42 +1541,31 @@ sub publish_file {
# zypper read the XML.
sub createrepo_c_cmd {
my ($dir) = @_;
return 'createrepo_c --update '
. '--revision ' . shell_quote($SOURCE_DATE_EPOCH) . ' --set-timestamp-to-revision '
. shell_quote($dir);
return MockBuildUtils::createrepo_c_cmd($dir, $SOURCE_DATE_EPOCH);
}
sub sign_and_index_repo {
my ($dir) = @_;
my @rpms = grep { !/\.src\.rpm$/ } bsd_glob("$dir/*.rpm");
if ($gpg_sign && @rpms) {
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
run_simple('rpmsign --define ' . shell_quote("%_gpg_name $gpg_key_name")
. ' --define ' . shell_quote("%__gpg $gpg_program") . ' --addsign '
. join(' ', map { shell_quote($_) } @rpms));
}
run_simple(createrepo_c_cmd($dir));
if ($gpg_sign) {
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
my $repomd = "$dir/repodata/repomd.xml";
unlink "$repomd.asc" if -f "$repomd.asc";
run_simple("gpg -a --detach-sign --default-key " . sh_quote($gpg_key_name) . ' ' . sh_quote($repomd));
run_simple("gpg -a --export " . sh_quote($gpg_key_name) . " > " . sh_quote("$repomd.key"));
}
my ($dir, $native) = @_;
return MockBuildUtils::sign_and_index_repo($dir, $native,
gpg_sign => $gpg_sign, gpg_home => $gpg_home,
gpg_key_name => $gpg_key_name, gpg_program => $gpg_program,
source_date_epoch => $SOURCE_DATE_EPOCH, run => \&run_simple);
}
sub write_dep_repo_metadata {
my ($dir, $rel, $tarch) = @_;
my $baseurl = "https://xcat.org/files/xcat/repos/yum/devel/xcat-dep/rh$rel/$tarch";
my $gpgcheck = $gpg_sign ? 1 : 0;
my $gpgkey_line = $gpg_sign ? "gpgkey=$baseurl/repodata/repomd.xml.key" : "# gpgkey=";
my ($dir, $rel, $tarch, $subdir) = @_;
$subdir //= "rh$rel/$tarch";
my $baseurl = "https://xcat.org/files/xcat/repos/yum/devel/xcat-dep/$subdir";
my $gpgcheck = $gpg_sign || $subdir =~ /^openeuler/ ? 1 : 0;
my $gpgkey_line = $gpgcheck ? "gpgkey=$baseurl/repodata/repomd.xml.key" : "# gpgkey=";
my $label = $subdir =~ /^openeuler/ ? $subdir : "rh$rel $tarch";
# repo_gpgcheck=1 makes clients verify the DETACHED repomd.xml signature (repomd.xml.asc) against
# gpgkey before trusting the metadata -- sign_and_index_repo produces both, so enforce it. Mirrors
# gpgcheck: off when the repo is unsigned.
open my $r, '>', "$dir/xcat-dep.repo" or die "Cannot write $dir/xcat-dep.repo: $!\n";
print {$r} <<"EOF";
[xcat-dep]
name=xCAT 2 dependencies (rh$rel $tarch)
name=xCAT 2 dependencies ($label)
baseurl=$baseurl
enabled=1
gpgcheck=$gpgcheck
@@ -1344,7 +1575,7 @@ EOF
close $r;
write_local_repo_helper($dir);
write_buildinfo($dir, "rh$rel/$tarch");
write_buildinfo($dir, $subdir);
}
sub write_common_repo_metadata {
@@ -1402,6 +1633,10 @@ sub write_buildinfo {
my $build_time = strftime("%a %b %e %H:%M:%S %Z %Y", gmtime($SOURCE_DATE_EPOCH));
my $build_machine = `hostname`; chomp $build_machine;
my $commit = `git -C "$repo_root" rev-parse HEAD 2>/dev/null`; chomp $commit;
if (!$commit && -f "$repo_root/Gitinfo") {
($commit) = read_lines("$repo_root/Gitinfo");
$commit =~ s/\s+\z// if defined($commit);
}
$commit ||= 'unknown';
my $commit_short = substr($commit, 0, 7);
my $release = strftime('snap%Y%m%d%H%M', gmtime($SOURCE_DATE_EPOCH));
@@ -1475,17 +1710,26 @@ Options:
the target is present at a version satisfying its pin AND that the repomd
is signed by --gpg-key-name; exits 0 if complete, or lists each MISSING/
VERSION/UNSIGNED/WRONGKEY problem and fails. The target is derived from the path
(.../rh<N>/<arch> -> alma+epel-<N>-<arch>) unless --target is given; the
(.../rh<N>/<arch> -> alma+epel-<N>-<arch>, or
.../openeuler<releaseSP>/<arch> -> openeuler-<releaseSP>-<arch>)
unless --target is given; the
manifest and gpg key/home come from the usual options. Use alone.
--no-verify-repo Suppress the AUTOMATIC post-build completeness+signature gate that runs
after each target's repo is finalized (default: verification ON)
--gpg-sign Sign RPMs and repomd.xml in every published repository
Required for native openEuler publication, including --skip-build.
Standalone --verify-repo consumes existing signed output.
--gpg-key-name NAME GPG key name (default: "xCAT Signing Key")
--gpg-home PATH GNUPGHOME for signing (default: system keyring)
--target NAME Build only this target (<ID>+epel-<REL>-<ARCH>, or a forcearch
config from mock-configs/ such as rocky-10-riscv64-xcat, which
cross-builds that arch on this host); default is the host arch
across rh8, rh9 and rh10
across rh8, rh9 and rh10. On openEuler the default retains the exact
host release and service pack, e.g. openeuler-24.03sp3-x86_64.
Native targets require the matching host architecture and deploy to
<repo-dep>/openeuler<releaseSP>/<arch> with signature checks enabled.
The build host must provide mock and its Perl dependencies; openEuler
24.03 LTS-SP3 can build older releases in their exact native targets.
--nproc N Parallel jobs for buildrpms.pl (default: 1)
--build-timeout SECONDS Wall-clock bound for one build step. Default: none for a native
target, and 9000 for a forcearch (qemu-user) target, which runs at
@@ -1836,12 +2080,33 @@ sub rpm_vercmp_segment {
# check: it verifies each rpm's header/payload digests AND that the signature is by this key (NOKEY /
# NOT OK => a real failure, since the key IS imported). Returns @problems.
sub verify_rpms_checksig {
my ($dir, $keyname, $home) = @_;
my ($dir, $keyname, $home, $native) = @_;
my @rpms = grep { !/\.src\.rpm$/ } glob("$dir/*.rpm");
return () unless @rpms;
my ($dbopt, $problem) = rpmkeys_keyring($keyname, $home);
return ($problem) if $problem;
return map { rpm_checksig_problem($_, $dbopt) } @rpms;
my $publisher_db;
if ($native) {
my $trust = tempdir('native-publisher-XXXXXXXX', TMPDIR => 1, CLEANUP => 1);
my $ok = eval { $publisher_db = publisher_trust($native, $trust); 1; };
return ("SIGKEY: $@") unless $ok;
}
my @problems;
for my $rpm (@rpms) {
if ($native) {
my $id = rpm_identity($rpm);
my $owner = $native->{outputs}{$id->{name}};
if (!$owner) { push @problems, "Undeclared native output: $id->{name}"; next; }
my $node = $native->{nodes}{$owner};
if ($node->{type} eq 'publisher') {
my $ok = eval { verify_input($native, $node, $rpm, $publisher_db); 1; };
push @problems, $@ unless $ok;
next;
}
}
push @problems, rpm_checksig_problem($rpm, $dbopt);
}
return @problems;
}
# rpmkeys_keyring: an isolated rpm keyring holding only the signing key, as the --dbpath option for
@@ -1904,6 +2169,7 @@ sub verify_target_repo {
my %MAN = read_manifest($manifest);
my %req = %{ $MAN{$tgt} // {} };
die "FATAL: no manifest section for target '$tgt' in $manifest\n" if !%req;
my $native;
# The WHOLE manifest, deliberately -- the --skip-* flags are NOT applied here. They say what
# this INVOCATION built; they never say what the verified repository may be missing. Honouring
# them let a repo with no xCAT-genesis-base pass whenever the verifying run happened to carry
@@ -1917,6 +2183,10 @@ sub verify_target_repo {
my %present_evr = map { $_ => rpm_evr($dir, $_) } @names;
my %expected = map { $_ => $req{$_} } @names;
my @problems = verify_repo_packages(\%expected, \%present, \%present_evr, \&rpm_vercmp_segment);
if ($tgt eq 'openeuler-24.03-ppc64le') {
eval { $native = load_inputs($repo_root, \%req); 1 }
or push @problems, "Native input catalog: $@";
}
# Signature gate: the IO (gpg) lives here; the decision is the pure verify_repo_signature. The
# pipeline always signs, so a signed repo's repomd MUST be signed by --gpg-key-name. We resolve
@@ -1944,7 +2214,7 @@ sub verify_target_repo {
require_command('rpm');
# (a) RPM-native crypto verification: rpmkeys --checksig against an isolated keyring
# holding only this key verifies every rpm's digests AND that the signature is by the key.
push @problems, verify_rpms_checksig($dir, $gpg_key_name, $gpg_home);
push @problems, verify_rpms_checksig($dir, $gpg_key_name, $gpg_home, $native);
# (b) Explicit signer-id origin check kept alongside: assert each rpm's header signature
# key id is one of this key's ids (primary/subkey).
my $accept = gpg_key_ids($gpg_key_name, $gpg_home);
@@ -1952,7 +2222,7 @@ sub verify_target_repo {
push @problems, "SIGKEY: cannot list key ids for '$gpg_key_name' to verify per-rpm signatures";
} else {
my @rpm_sigs = map { [ basename($_), rpm_signer_keyid($_) ] }
grep { !/\.src\.rpm$/ } glob("$dir/*.rpm");
grep { !/\.src\.rpm$/ && (!$native || !native_publisher_rpm($native, $_)) } glob("$dir/*.rpm");
push @problems, verify_rpm_signatures(\@rpm_sigs, $accept);
}
}
@@ -1973,15 +2243,11 @@ sub verify_target_repo {
return 1;
}
# derive_target_from_repo_path: map a deployed per-target repo path .../rh<N>/<arch> to its manifest
# target section name alma+epel-<N>-<arch>. Returns undef when the path lacks that rh<N>/<arch> tail,
# so the standalone --verify-repo mode can require an explicit --target instead.
sub derive_target_from_repo_path {
my ($dir) = @_;
my $tgt;
return $tgt unless defined $dir;
$tgt = "alma+epel-$1-$2" if $dir =~ m{/rh(\d+)/([^/]+)/*$};
return $tgt;
sub native_publisher_rpm {
my ($plan, $rpm) = @_;
my $id = rpm_identity($rpm);
my $owner = $plan->{outputs}{$id->{name}} // return 0;
return $plan->{nodes}{$owner}{type} eq 'publisher';
}
sub reset_staging_repo {
@@ -2203,6 +2469,8 @@ sub take_lock {
# path from here, so the lock covers the directory the deploy writes.
sub target_cell {
my ($target) = @_;
my $native = openeuler_repo_subdir($target);
return "$repo_dep/$native" if defined $native;
my $profile = target_profile($target);
return "$repo_dep/rh$profile->{rel}/$profile->{arch}";
}
+24
View File
@@ -0,0 +1,24 @@
# Native packaging tests
The openEuler packaging tests build and sign RPMs or require Linux namespaces.
They run separately from `prove -r t` on a disposable host with the required
native tools:
```
prove -v native/*.t
```
Use an ordinary user for RPM builds. `openeuler-power-inputs.t` exercises the
whole build owner only on POWER with native Mock; its RPM admission checks also
run on x86_64. When running that test as root, set `XCAT_TEST_BUILD_USER` to an
unprivileged account for the fixture builds.
The fixtures are command doubles at the downloader and Mock boundaries. The
POWER Mock adapter uses the installed Python API directly to load generated
configurations. It does not execute another Python interpreter.
The Mock configuration test loads the installed Python API from a checked-in
fixture and is kept outside the unit run.
Inspect TAP skips: a successful exit does not qualify an unavailable native
tool or architecture.
+115
View File
@@ -0,0 +1,115 @@
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/../t/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 gpg gpgconf rpmsign);
my $parent_pid = $$;
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 die(read_binary("$tmp/rpm-build.log"));
my $fixture = "$top/RPMS/noarch/provenance-fixture-1-1.noarch.rpm";
my $key_home = "$tmp/gnupg";
make_path($key_home);
chmod 0700, $key_home;
my $key_name = 'provenance@example.invalid';
die read_binary("$tmp/key.log") if run_capture("$tmp/key.log", 'gpg', '--homedir', $key_home,
'--batch', '--pinentry-mode', 'loopback', '--passphrase', '', '--quick-generate-key',
$key_name, 'rsa2048', 'sign', '0');
END {
local $?;
run_capture("$tmp/key-cleanup.log", 'gpgconf', '--homedir', $key_home, '--kill', 'gpg-agent')
if defined($parent_pid) && $$ == $parent_pid && defined($key_home) && -d $key_home;
}
my $payload_hash = capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $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",
'--gpg-sign', '--gpg-home', $key_home, '--gpg-key-name', $key_name);
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(capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', "$repo/$subdir/provenance-fixture-1-1.noarch.rpm"), $payload_hash,
"$case $target signing preserves the RPM payload");
}
}
done_testing();
+23
View File
@@ -0,0 +1,23 @@
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))
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/python3
import json, os, pathlib, shutil, sys
import mockbuild
from mockbuild.util import load_config
args = sys.argv[1:]
call = {'mock': args}
def value(name): return args[args.index(name)+1]
if '--rebuild' in args or '--buildsrpm' in args:
load_config('/etc/mock', value('-r'), None, 'native-contract',
str(pathlib.Path(mockbuild.__file__).parent))
call['config_rc'] = 0
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)
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Copy qw(copy);
use JSON::PP qw(decode_json encode_json);
open(my $input, '<', $ENV{NATIVE_DOWNLOADS}) or die $!;
my $downloads = decode_json(do { local $/; <$input> });
close($input) or die $!;
my ($output) = grep { $ARGV[$_] eq '-O' } 0 .. $#ARGV - 1;
die 'wget fixture requires -O' unless defined($output);
open(my $trace, '>>', $ENV{NATIVE_CALLS}) or die $!;
print {$trace} encode_json({wget => $ARGV[-1]}) . "\n" or die $!;
close($trace) or die $!;
copy($downloads->{$ARGV[-1]}, $ARGV[$output + 1]) or die $!;
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Digest::SHA qw(sha256_hex);
use File::Basename qw(basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use JSON::PP qw(encode_json);
sub option {
my ($name) = @_;
for my $i (0 .. $#ARGV - 1) { return $ARGV[$i + 1] if $ARGV[$i] eq $name; }
return;
}
sub contents {
open(my $file, '<', $_[0]) or die $!;
binmode($file);
return do { local $/; <$file> };
}
my %entry = (argv => \@ARGV);
my $config = option('-r');
$entry{config} = contents($config) if defined($config) && -f $config;
$entry{spec} = contents(option('--spec')) if defined(option('--spec'));
if (my $source = option('--rebuild')) {
$entry{source} = $source;
$entry{sha256} = sha256_hex(contents($source));
}
open(my $trace, '>>', $ENV{SCP_CALLS}) or die $!;
print {$trace} encode_json(\%entry) . "\n" or die $!;
close($trace) or die $!;
exit 0 if grep { /^--scrub=/ } @ARGV;
if (grep { $_ eq '--buildsrpm' } @ARGV) {
my $dest = option('--resultdir');
make_path($dest);
copy($ENV{SCP_FIXTURE_SOURCE}, "$dest/python3-scp-0.14.5-1.src.rpm") or die $!;
exit 0;
}
if ($ENV{SCP_MUTATE_SOURCE}) {
open(my $source, '>>', $ENV{SCP_MUTATE_SOURCE}) or die $!;
print {$source} 'changed after staging' or die $!;
close($source) or die $!;
}
exit 43 if ($ENV{SCP_BUILD_STATUS} // '43') ne '0';
if (($ENV{SCP_EMPTY_OUTPUT} // '') ne '1') {
my $dest = option('--resultdir');
make_path($dest);
for my $key (qw(SCP_FIXTURE_BINARY SCP_FIXTURE_SOURCE)) {
copy($ENV{$key}, "$dest/" . basename($ENV{$key})) or die $!;
}
}
+291
View File
@@ -0,0 +1,291 @@
#!/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 File::Slurper qw(read_text write_text);
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;
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', 'lib/XCAT/NFSLock.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 $!;
make_path("$checkout/goconserver/toolchains");
write_text("$checkout/goconserver/toolchains/go1.25.12.sha256",
join('', map { "$hash go1.25.12.linux-$_.tar.gz\n" } qw(amd64 ppc64le)));
write_text("$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 $log = -f "$directory/commands.jsonl" ? read_text("$directory/commands.jsonl") : '';
my @commands = map { decode_json($_) } grep { length } split /\n/, $log;
return { directory => $directory, status => $status, output => read_text("$directory/output"),
spec => -f "$directory/work/goconserver.spec" ? read_text("$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_text("$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_text("$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_text("$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_text("$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_text("$case->{directory}/results/goconserver-0.3.3-4.riscv64.rpm"), "fixture binary RPM\n", 'EL cross path collects its package output');
}
done_testing();
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin qw($RealBin);
use JSON::PP qw(decode_json);
use Test::More;
plan skip_all => 'native Mock Python library and templates required'
if system('python3 -c "import mockbuild.config" >/dev/null 2>&1') != 0
|| !-f '/etc/mock/templates/openeuler-24.03.tpl';
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'],
);
open(my $pipe, '-|', 'python3', "$RealBin/fixtures/mock-configs.py", "$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();
+389
View File
@@ -0,0 +1,389 @@
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/../t/lib";
use MockBuildUtils qw(read_manifest sign_and_index_repo);
use XCAT::BuildUtils qw(capture_command command_exists digest_file digest_manifest relative_files 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 $parent_pid = $$;
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 die($@);
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 die(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 {
local $?;
for my $home (values %homes) {
run_capture("$home/cleanup.log", 'gpgconf', '--homedir', $home, '--kill', 'gpg-agent') if defined($parent_pid) && $$ == $parent_pid && -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 die(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 die(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');
copy("$RealBin/fixtures/power-wget.pl", "$tmp/bin/wget") or die $!;
copy("$RealBin/fixtures/power-mock.py", "$tmp/bin/mock") or die $!;
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 ($root) = prepare('standalone-signers');
my $dest = "$tmp/standalone-repo";
make_path($dest);
my $generated = signed_copy($rpm{'native-child'}, 'generated-build-signer', 'build');
my $publisher = "$dest/publisher-package-1-1.oe2403.noarch.rpm";
my $child = "$dest/native-child-1-1.oe2403.noarch.rpm";
copy($signed{'publisher-package'}, $publisher) or die $!;
copy($generated, $child) or die $!;
is(run_capture("$tmp/standalone-createrepo.log", 'createrepo_c', $dest), 0,
'create metadata for the mixed-signer repository');
is(run_capture("$tmp/standalone-sign.log", 'gpg', '--homedir', $homes{build}, '--batch', '--yes',
'--armor', '--detach-sign', '--default-key', $keys{build}, "$dest/repodata/repomd.xml"), 0,
'sign repository metadata with the build key');
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/standalone-valid.log", @verify), 0,
'standalone native verification accepts each declared signing authority')
or diag(read_binary("$tmp/standalone-valid.log"));
my $resigned = signed_copy($rpm{'publisher-package'}, 'publisher-build-signer', 'build');
copy($resigned, $publisher) or die $!;
isnt(run_capture("$tmp/standalone-resigned.log", @verify), 0,
'standalone verification rejects a publisher package signed by the build key');
like(read_binary("$tmp/standalone-resigned.log"), qr/SHA256 mismatch/,
'the publisher failure identifies the changed pinned bytes');
copy($signed{'publisher-package'}, $publisher) or die $!;
my $wrong_generated = signed_copy($rpm{'native-child'}, 'generated-publisher-signer', 'publisher');
copy($wrong_generated, $child) or die $!;
isnt(run_capture("$tmp/standalone-wrong-generated.log", @verify), 0,
'standalone verification rejects the publisher key for generated output');
like(read_binary("$tmp/standalone-wrong-generated.log"), qr/NOKEY|WRONGKEY|checksig/i,
'the generated output failure identifies the unexpected signer');
copy($generated, $child) or die $!;
is(run_capture("$tmp/standalone-restored.log", @verify), 0,
'restoring both original package signatures restores standalone acceptance');
ok(!-f $ENV{NATIVE_CALLS}, 'standalone verification runs no downloader or builder');
}
{
my $dest = "$tmp/signing-repo";
make_path($dest);
my $publisher = "$dest/publisher-package-1-1.oe2403.noarch.rpm";
my $child = "$dest/native-child-1-1.oe2403.noarch.rpm";
copy($signed{'publisher-package'}, $publisher) or die $!;
copy($rpm{'native-child'}, $child) or die $!;
my @commands;
my $sequence = 0;
my %options = (
gpg_sign => 1, gpg_home => $homes{build}, gpg_key_name => $keys{build},
gpg_program => '/usr/bin/gpg', source_date_epoch => $epoch,
run => sub {
my ($command) = @_;
push @commands, $command;
my $log = "$tmp/signing-command-" . ++$sequence . '.log';
die read_binary($log) if run_capture($log, '/bin/sh', '-c', $command);
},
);
my $ok = eval { sign_and_index_repo($dest, $plan, %options); 1 };
ok($ok, 'repository signing accepts unchanged publisher input') or die($@);
is(digest_file($publisher), digest_file($signed{'publisher-package'}),
'repository signing preserves the original publisher bytes');
is(run_capture("$tmp/signing-publisher.log", 'rpmkeys', '--dbpath', $plan->{trust_db},
'--checksig', '--verbose', $publisher), 0,
'publisher RPM retains its original trusted signature');
my $build_trust = "$tmp/signing-build-trust";
make_path($build_trust);
is(run_capture("$tmp/signing-build-import.log", 'rpmkeys', '--dbpath', $build_trust,
'--import', "$homes{build}/public.asc"), 0, 'trust the build key in an isolated RPM database');
is(run_capture("$tmp/signing-child.log", 'rpmkeys', '--dbpath', $build_trust,
'--checksig', '--verbose', $child), 0, 'generated RPM verifies with the build key');
like(read_binary("$tmp/signing-child.log"), qr/Signature.*\bOK\b/i,
'generated RPM has a verified signature');
is(run_capture("$tmp/signing-metadata.log", 'gpg', '--homedir', $homes{build},
'--verify', "$dest/repodata/repomd.xml.asc", "$dest/repodata/repomd.xml"), 0,
'repository metadata has a valid build signature');
my $changed = signed_copy($rpm{'publisher-package'}, 'changed-before-signing', 'build');
copy($changed, $publisher) or die $!;
copy($rpm{'native-child'}, $child) or die $!;
isnt(digest_file($publisher), $plan->{nodes}{'publisher-package'}{sha256},
'the changed publisher RPM differs from its pinned input');
my $before = digest_manifest($dest, 'sha256', relative_files($dest));
@commands = ();
dies_like(sub { sign_and_index_repo($dest, $plan, %options) },
qr/\APublisher input changed before signing: \Q$publisher\E\n\z/,
'changed publisher bytes stop repository signing');
is_deeply(\@commands, [], 'changed publisher bytes stop before signing or indexing commands');
is(digest_manifest($dest, 'sha256', relative_files($dest)), $before,
'rejection preserves generated RPMs, publisher RPMs and repository metadata');
}
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();
+396
View File
@@ -0,0 +1,396 @@
use strict;
use warnings;
use Cwd qw(abs_path cwd);
use File::Basename qw(dirname basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin qw($RealBin);
use JSON::PP qw(decode_json);
use Test::More;
use lib "$RealBin/../lib", "$RealBin/../t/lib";
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
use XCAT::GenesisReleaseTest qw(run_capture);
use XCAT::NFSLock ();
plan skip_all => 'Linux RPM tools and user namespaces required'
unless $^O eq 'linux' && !grep { !command_exists($_) } qw(rpm rpmkeys rpmbuild createrepo_c unshare gpg gpgconf);
my $parent_pid = $$;
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;
if ($ENV{XCAT_TEST_GENESIS_SIGNING_ONLY}) {
test_genesis_signing();
done_testing();
exit;
}
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 repository gate')
or die(read_binary("$tmp/key.log"));
END {
local $?;
run_capture("$tmp/key-cleanup.log", 'gpgconf', '--homedir', $key_home, '--kill', 'gpg-agent')
if defined($parent_pid) && $$ == $parent_pid && 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 die(read_binary("$tmp/fixture.log"));
copy("$RealBin/fixtures/srpm-mock.pl", "$tmp/bin/mock") or die $!;
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/buildrpms.pl", "die 'Genesis dry-run must not execute its child';\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');
for my $cell (keys %{$opt{published} // {}}) {
make_path("$repo/$cell");
copy($opt{published}{$cell}, "$repo/$cell/python3-scp-0.14.5-1.noarch.rpm") or die $!;
}
my ($held_lock, $lock_path, $lock_before);
if ($opt{hold_cell}) {
my $cell = "$repo/$opt{hold_cell}";
make_path(dirname($cell));
$lock_path = dirname($cell) . '/.' . basename($cell) . '.lock';
$held_lock = XCAT::NFSLock->acquire($lock_path, quiet => 1);
$lock_before = read_binary("$lock_path/metadata");
}
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";
local $ENV{HOME} = $root;
local $ENV{GNUPGHOME} = $opt{env_home} // '';
my @options = @{$opt{options} // []};
unshift @options, '--gpg-sign', '--gpg-key-name', $key_name,
($opt{default_home} ? () : ('--gpg-home', $opt{key_home} // $key_home))
if !$opt{unsigned} && $selected =~ /^openeuler-/;
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}) : ();
my $lock_after = $held_lock && -f "$lock_path/metadata" ? read_binary("$lock_path/metadata") : undef;
$held_lock->release if $held_lock;
return {rc => $rc, calls => \@calls, log => read_binary("$tmp/$name.log"), input => $input,
repo => $repo, out => $out, root => $root, lock_path => $lock_path,
lock_before => $lock_before, lock_after => $lock_after};
}
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', unsigned => 1, 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');
for my $skip (0, 1) {
my $unsigned = scenario("unsigned-$skip", unsigned => 1, success => 1,
options => ['--no-verify-repo', ($skip ? ('--skip-build', '--collect-dir', "$tmp/fixture/RPMS/noarch") : ())]);
isnt($unsigned->{rc}, 0, 'unsigned native publication is rejected even when verification is disabled');
like($unsigned->{log}, qr/openEuler repository publication requires --gpg-sign/, 'the owner reports the signing requirement');
is_deeply($unsigned->{calls}, [], 'unsigned publication fails before every mock action');
my $sentinel = "$unsigned->{repo}/openeuler20.03sp4/x86_64/sentinel";
is(-f $sentinel ? read_binary($sentinel) : '', 'previous repository', 'rejection preserves the previous repository');
my $metadata = "$unsigned->{repo}/openeuler20.03sp4/x86_64/xcat-dep.repo";
is(-f $metadata ? read_binary($metadata) : '', '', 'rejection emits no misleading native repository configuration');
}
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(capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $published),
capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm"),
'signing preserves the collected RPM header and payload digest') if -f $published;
ok(-s "$collected->{repo}/openeuler20.03sp4/x86_64/repodata/repomd.xml.key",
'signed native publication exports its configured repository key');
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 $unsigned_fixture = "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm";
for my $case ([$target, 'openeuler20.03sp4/x86_64', 'rh20.03sp4/x86_64'],
['alma+epel-9-x86_64', 'rh9/x86_64', 'openeuler9/x86_64']) {
my ($cell_target, $cell, $decoy) = @$case;
my $result = scenario("carry-$cell_target", missing => 1, target => $cell_target,
published => {$cell => $published, $decoy => $unsigned_fixture},
options => ['--skip-xcat-dep', '--gpg-sign', '--gpg-home', $key_home, '--gpg-key-name', $key_name]);
is($result->{rc}, 0, "$cell_target carries the signed skipped package from its own published cell")
or diag($result->{log});
my $carried = "$result->{out}/mockbuild-all/$cell_target-source-contract/repo/x86_64/python3-scp-0.14.5-1.noarch.rpm";
ok(-f $carried, "$cell_target includes the carried package in this run's repository");
is(capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $carried),
capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $published), "$cell_target preserves the carried payload") if -f $carried;
is_deeply($result->{calls}, [], "$cell_target carry-over executes no build command");
}
my $untrusted = scenario('carry-unsigned', missing => 1,
published => {'openeuler20.03sp4/x86_64' => $unsigned_fixture}, options => ['--skip-xcat-dep']);
isnt($untrusted->{rc}, 0, 'unsigned native carry-over fails before publication');
like($untrusted->{log}, qr/not signed by the configured key/, 'native carry-over reports the trust failure');
is(read_binary("$untrusted->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
'rejected native carry-over preserves the published repository');
my $wrong_cell = scenario('carry-wrong-cell', missing => 1,
published => {'rh20.03sp4/x86_64' => $published}, options => ['--skip-xcat-dep']);
isnt($wrong_cell->{rc}, 0, 'a signed package in the EL-shaped path cannot fill a native cell');
like($wrong_cell->{log}, qr/MISSING python3-scp/, 'the native gate reports the package absent from its own cell');
for my $held ('openeuler20.03sp4/x86_64', 'rh20.03sp4/x86_64') {
my $native = $held =~ /^openeuler/;
my $result = scenario($native ? 'native-lock' : 'decoy-lock', missing => 1,
hold_cell => $held, published => {'openeuler20.03sp4/x86_64' => $published},
options => ['--skip-xcat-dep', '--try-unlock-timeout', 0]);
if ($native) {
isnt($result->{rc}, 0, 'the native published cell lock excludes a second publisher');
like($result->{log}, qr/Trying to unlock \Q$result->{lock_path}\E failed/,
'the refusal names the lock beside the native published cell');
is(read_binary("$result->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
'native lock refusal preserves the published repository');
ok(!-d "$result->{out}/mockbuild-all/$target-source-contract",
'native lock refusal precedes carry-over and collection');
} else {
is($result->{rc}, 0, 'an EL-shaped decoy lock does not block native carry-over and deployment')
or diag($result->{log});
ok(-f "$result->{repo}/openeuler20.03sp4/x86_64/python3-scp-0.14.5-1.noarch.rpm",
'the unlocked native cell receives the carried package');
my $metadata = "$result->{repo}/openeuler20.03sp4/x86_64/xcat-dep.repo";
my $config = -f $metadata ? read_binary($metadata) : '';
like($config, qr{^baseurl=.*\/openeuler20\.03sp4/x86_64$}m,
'the native repository configuration names the deployed cell');
}
is_deeply($result->{calls}, [], "$held lock case runs no package builder");
is($result->{lock_after}, $result->{lock_before}, "$held remains held by its original owner");
}
for my $name ('native-only', 'mixed', 'legacy-locked') {
my $root = "$tmp/finalize-$name";
my @native = ("$root/openeuler20.03sp4/x86_64", "$root/openeuler24.03/ppc64le");
make_path(@native);
write_binary("$_/marker", 'native repository') for @native;
my @held_cells = @native;
my ($x, $p) = ("$root/rh9/x86_64", "$root/rh9/ppc64le");
my ($xrpm, $prpm) = ('xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm',
'xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm');
if ($name ne 'native-only') {
make_path($x, $p);
copy($published, "$x/$xrpm") or die $!;
copy($published, "$p/$prpm") or die $!;
push @held_cells, $p;
push @held_cells, $x if $name eq 'legacy-locked';
}
my @held;
for my $cell (@held_cells) {
my $path = dirname($cell) . '/.' . basename($cell) . '.lock';
my $lock = XCAT::NFSLock->acquire($path, quiet => 1);
push @held, [$lock, $path, read_binary("$path/metadata")];
}
my $logfile = "$tmp/finalize-$name.log";
my $rc = run_capture($logfile, $^X, $collector, '--finalize-xcat-dep',
'--x86_64-repo', $root, '--ppc64le-repo', $root, '--finalize-arch', 'x86_64',
'--build-timestamp', $epoch, '--try-unlock-timeout', 0, '--no-verify-repo');
my $log = read_binary($logfile);
if ($name eq 'legacy-locked') {
isnt($rc, 0, 'finalization refuses a held legacy destination lock');
like($log, qr{Trying to unlock \Q$root\E/rh9/\.x86_64\.lock failed},
'finalization refuses the same sibling lock as an EL publisher');
ok(!-e "$x/$prpm", 'lock refusal precedes the legacy cross-copy');
ok(!-d "$x/repodata", 'lock refusal precedes legacy reindexing');
} else {
is($rc, 0, "$name finalization ignores held native and unselected cell locks") or diag($log);
if ($name eq 'mixed') {
ok(-f "$x/$prpm", 'selected legacy destination receives its foreign Genesis');
is(-f "$x/$prpm" ? digest_file("$x/$prpm") : undef, digest_file("$p/$prpm"),
'legacy cross-copy preserves the source RPM');
ok(-f "$x/repodata/repomd.xml", 'selected legacy destination is reindexed');
ok(!-e "$p/$xrpm", 'unselected legacy source receives no foreign Genesis');
ok(!-d "$p/repodata", 'unselected legacy source is not reindexed');
} else {
like($log, qr/openEuler.*skipping/, 'native-only finalization explains the skip');
unlike($log, qr/(?:acquired|took-over) repository cell lock/,
'native-only finalization acquires no cell lock');
}
}
for my $native (@native) {
is_deeply([glob("$native/*")], ["$native/marker"], "$name adds no native artifacts");
is(read_binary("$native/marker"), 'native repository', "$name preserves native content");
}
for my $held (@held) {
my ($lock, $path, $before) = @$held;
is(-f "$path/metadata" ? read_binary("$path/metadata") : undef, $before,
"$name preserves the existing owner of $path");
$lock->release;
}
}
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');
test_genesis_signing();
done_testing();
sub test_genesis_signing {
for my $home ('explicit', 'default', 'environment', 'relative-explicit', 'relative-environment', 'relative-missing') {
my $relative = File::Spec->abs2rel($key_home, cwd());
$relative .= '/not-created' if $home eq 'relative-missing';
my $environment = $home =~ /environment/ ? ($home eq 'environment' ? $key_home : $relative) : '';
my $result = scenario("genesis-$home", packages => 'xCAT-genesis-base=2.19.0',
missing => 1, default_home => ($home eq 'default' || $home =~ /environment/ ? 1 : 0), env_home => $environment,
key_home => $home =~ /^relative-/ ? $relative : $key_home,
options => ['--dry-run', '--no-skip-genesis', '--skip-xcat-dep']);
is($result->{rc}, 0, "$home keyring native Genesis planning completes") or diag($result->{log});
my ($command) = grep { /^\+ .*buildrpms\.pl/ } split /\n/, $result->{log};
my $home_path = $home eq 'default' ? "$result->{root}/.gnupg" : $key_home;
$home_path = File::Spec->rel2abs($relative, cwd()) if $home =~ /^relative-/;
like($command // '', qr/--gpg-sign --gpg-key-name '\Q$key_name\E' --gpg-home '\Q$home_path\E'/,
"$home parent signing identity reaches the Genesis child despite its private HOME");
is_deeply($result->{calls}, [], "$home Genesis planning runs no mock command");
isnt($result->{root}, cwd(), "$home child source directory differs from the parent signing directory");
like($result->{log}, qr/\(cwd: \Q$result->{root}\E\)/, "$home child command runs in its source directory");
}
my $legacy_genesis = scenario('genesis-legacy', target => 'alma+epel-9-x86_64', packages => 'xCAT-genesis-base=2.19.0',
missing => 1,
options => ['--dry-run', '--no-skip-genesis', '--skip-xcat-dep', '--gpg-sign', '--gpg-home', $key_home, '--gpg-key-name', $key_name]);
is($legacy_genesis->{rc}, 0, 'legacy signed owner still plans Genesis');
my ($legacy_command) = grep { /^\+ .*buildrpms\.pl/ } split /\n/, $legacy_genesis->{log};
like($legacy_command // '', qr/--package xCAT-genesis-base/, 'legacy plan contains the production child command');
unlike($legacy_command // '', qr/--gpg-(?:sign|home|key-name)/, 'legacy Genesis child invocation remains unchanged');
}
+135
View File
@@ -0,0 +1,135 @@
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/../t/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", "$source/lib/XCAT");
copy($owner, "$source/xnba/mockbuild.pl") or die $!;
copy("$repo/MockBuildUtils.pm", "$source/MockBuildUtils.pm") or die $!;
copy("$repo/lib/XCAT/NFSLock.pm", "$source/lib/XCAT/NFSLock.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();
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
openEuler 24.03 LTS POWER inputs
This GA target is a functional build profile. It does not establish maintained
POWER service-pack support or physical platform qualification.
The 24.03-ppc64le.inputs.json catalog pins the native source RPMs, publisher
noarch RPMs, public key, spec patch, and spec definitions used by this target.
Its outputs are RPM package names. The packages-manifest.conf POWER section
selects the required runtime outputs; needs selects their build prerequisites.
The build_inputs group supplies the additional same-GA noarch build packages.
No binary package from another service pack or architecture is admitted.
Use an exact native ppc64le openEuler 24.03 LTS builder. The target configuration
uses the published OS repository, native vendor macros, and Mock simple
isolation. POWER GA has no published Everything, update, or EPOL architecture
repository. Individual GA noarch inputs are pinned from the publisher's other
architecture repositories. Each must have a noarch header, a GA release suffix,
a valid publisher signature, and no ELF file anywhere in its RPM payload.
They retain their original bytes and signatures in the resulting repository.
Run the existing owner through actual sudo from UID1000. The native Mock 2.2
entry at /usr/libexec/mock/mock must precede consolehelper's /usr/bin/mock in
sudo's PATH. The target fixes chrootuid and chrootgid at 1000. Do not synthesize
SUDO_UID or USERHELPER_UID. Preserve the actual Mock build logs and confirm
UID1000 for compiled packages. Genesis assembles a root filesystem as UID0;
xNBA packages existing boot artifacts as UID0 and has no compilation or check
section. These are the two packaging exceptions.
sudo perl mockbuild-all.pl --target openeuler-24.03-ppc64le \
--xcat-source /path/to/frozen/xcat-core \
--output /path/to/private/output --max-parallel 1 \
--gpg-sign --gpg-home /path/to/signing-home \
--gpg-key-name SIGNING_FINGERPRINT --build-timestamp SOURCE_EPOCH
Freeze both source trees and record their hashes before running the owner.
Use a fresh output/run identity. Do not modify input files while a build runs.
The owner validates the complete graph and output ownership before starting
Mock. It fetches all selected inputs, verifies them in a private publisher
RPM database, and preserves the signed originals under native-inputs.
The source RPM path in mockbuild-all.pl builds the prerequisites in dependency
order. Publisher inputs precede source builds; existing owners run afterward.
Dependencies on an existing owner and publisher dependency edges are rejected
before acquisition because those phases cannot honor them. Patches and
definitions use that same source path. Native results remain
unsigned under native-results; signed copies populate native-prerequisites.
Private configuration overlays bind that repository into subsequent Mock
roots. Their paths and hashes are recorded in native-overlays.json. Publisher
RPMs retain their publisher signatures; generated RPMs require the selected
build signing key. Repository metadata is signed by the build key and exports
both public keys. Neither key is imported into the builder's system RPMDB.
Native Mock's procenv plugin requires a procenv package that GA OS lacks.
The procenv source is therefore built first with only that diagnostic plugin
disabled in a recorded private overlay. Its normal vendor checks and UID1000
build remain enabled. All subsequent roots enable the plugin and obtain the
newly signed native procenv package. No package feature or test is disabled.
The final repository collects the manifest-selected native/publisher outputs
and the established script-owner outputs. PostgreSQL and its source helper
chain remain in the private prerequisite repository; the normal xCAT/xCATsn
closure does not select a PostgreSQL server. Native perl-DBD-Pg is required by
the service image profiles. Its unchanged vendor check needs PostgreSQL.
The PostgreSQL patch and vendor options are described in ../postgresql/Build-notes.
Those options preserve normal SQL, authentication, backup, and reconnect
features. Runtime backend validation is a separate gate from package builds.
Bootstrapping the build tools
The catalog's bootstrap_inputs records the native source chain needed when
Mock and the owner Perl modules are absent. It includes the unchanged official
24.03 SP3 File-Slurper source RPM because GA publishes no such source package.
Rebuild that source on GA; do not install its SP3 binary RPM.
The input verifier uses only core Perl modules. It can fetch and verify these
inputs before Mock is installed:
perl -Ilib -MXCAT::NativeInputs=load_inputs,stage_inputs -e '
my ($root, $stage) = @ARGV;
my %required = map { $_ => "*" } qw(mock perl-Params-Util
python3-psutil python3-pyroute2 perl-PerlIO-utf8_strict
perl-File-Slurper procenv);
stage_inputs(load_inputs($root, \%required), $stage);
' "$PWD" /path/to/new/private/input-stage
Use a disposable exact-GA installroot for bootstrap builds. Install rpm-build,
dnf-plugins-core, gcc, make, and each source's full BuildRequires through strict
signed native repositories. Extract the verified source with rpm -i and a
private _topdir; use dnf builddep on the extracted spec. Run normal rpmbuild -ba
as UID1000 without changing the vendor spec or disabling its check section.
Build native perl-Params-Util before users of perl-Module-Build; build
perl-PerlIO-utf8_strict before perl-File-Slurper. Build python-psutil and
python-pyroute2 before installing Mock. Build procenv for Mock diagnostics.
The pinned noarch group supplies the missing pure Perl/Python prerequisites.
Retain unsigned outputs, vendor check logs, native architecture, full RPM
Provides/Requires, and the original-source and output hashes. Sign copies and
verify them against a private RPMDB before making them available to DNF.
Install the resulting native Mock, its runtime prerequisites, and the owner
Perl modules inside the disposable builder. Use the existing mockbuild-all.pl
owner for subsequent target/package builds. The bootstrap instructions do not
replace its scheduler, collector, signature gate, or repository layout.
+50
View File
@@ -0,0 +1,50 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGOROkcBDAC2S6JpeU5YFzMDp5zqpWoTQmDaVnNh4dsbCEJp+Z6p2v7Y7NmM
iGzDYvScsa0nhM15SVJsrWYFkJB1rX+ESy7RRb1qGS5FznobzgUbhmMhpE0U/5+u
hTcvjk7wpFn04+FHugvIZ5gjP0G48gYkJoOtKKtMYA5Uvl/w0uRI6++Vme6m4W/K
Y2igg/JmRXSHhJHLQFICtQSZWw0kvWr6EUhmnFayzB6teKwJivJzJKHBTOgiSq5h
Q4BEcOJz0jmF4xOvpXIBB2mIb191DSXm9kadyRBZMDfw1Nqgmhhw40BRlt4hsV8k
yKymCFqm9M48NwY99/8Cfms4IXfD9XiF7nVj8+e5CcXeEGFWatZD2nCHTAkyah2L
Ukqe372pnQyCBvDIwkxTha/LWIVXU3eIMbSOz2dLht55yb+TNhOgK1b4xjhq6RWz
BpGjReU8RDtghVZkelt+mBZA8HPR81DoUuAm4vQuaxKecl44FdhzeUkCVDyA6ubh
kY5LQQBwIR7X+68AEQEAAbQkb3BlbmV1bGVyIDxvcGVuZXVsZXJAY29tcGFzcy1j
aS5jb20+iQHNBBMBCAA3FiEEiqFr+fLKUkQBDcqWO0d8YLZ1YAsFAmOROkcCGwMF
CwkIBwIGFQoJCAsCAxYCAQIeAQIXgAAKCRA7R3xgtnVgCzyEC/9L7TMRYC6xK2Dn
BetWLGBYag2YQmIIPUqZLFmq7RDiyAeVgFfk3TQj7AQryp3Cg63pxGH3YEOmU2B+
6s9advYUzEokd9DpiZoOKnNRK7EXb1aDw1Ujgd9xH4FgTNiUUxnkrb5Rlf3U5uSI
moqTwHuagBm9JP3xDllFFyo++w/23pQpoFMza4DiGrfVRor/oqfkmuKnimxg2naU
iAD4kO25O9Css9cgKrKNN06iuLPW0txqV9t2WfUsP28Lj+QE0yFaxlCokVbD0PSy
L1GKZszWMN+95NuEwrD8VeEzOrji7MqTjpWmzq70O4tyzyEHlCXizhQo/6HrDVPF
2npcCFYkxd53LmfW0MuRdEETf7hbIC0+ViD7mX55i3Z3x4MWb2X2zPl+r8yHiQsZ
Y/wm2sPWZb7jBm8up3c+xIoJZv5yoEX7JMFtiwpEMYJhyNKhgeQ4M3hi3v4q6rIL
QoCyujyENpr/opHL0EXFkUVvA3AUh+DR8cUiAo7X1pmJjKuRdEW5AY0EY5E6ygEM
AMj+qR7eLSdfDkcuPkSYqvzVcaYHpBwKn6ax9QTtR6UfONbg5CGQOU90RGH8xBix
bHf3VvIqt00x9dRW36mwLR/+CP/FJyqchC6Wh2k0SEJ5HR4frsWmOOHcT7wK150D
uTsyuWF4DidtvWtV1sgMZQcg66iFsPbdyTGaIolXij+4tv2TJgo9468MI0gFOY+0
2B6vluyB9k9nKNwEzH1cQCcDXa1r3P0f8iMNoojvSHZPKF9uAtUrnWULd3At+Nui
AI3H6rc7MEp/mVGnGWbNEfpHcwHqafRuJsdQgYu0AYNPyh+NT82n+clNSh0RoYGI
YLmPX+QBIIlsgcK3P8AZWjISKWtBRo5IJQWeB2BkMNrAKWpKUKn+nsWVaG4TZ8c+
2oqpuO+6ol4lFhk0G7cVqW09OOQ/UNopEiXHbJvpAqzSKbuzmK+kLB67pp4/wS+w
Os09t1o/m9qynMCCGmisNvVrWWmEiG/KaeFcQzzs9jVr9piGeGcxva70PbJew1hz
qwARAQABiQNsBBgBCAAgFiEEiqFr+fLKUkQBDcqWO0d8YLZ1YAsFAmOROsoCGwIB
wAkQO0d8YLZ1YAvA9CAEGQEIAB0WIQSBLhvcto+bdWqkjO8Af7dH+ze8bwUCY5E6
ygAKCRAAf7dH+ze8b57HC/4sHZk0yhBlwMWdu0vQGE+e8W1FTkL6uF2TTsTAVmAX
aIT3PrZJGiCfuqvdaYzArpEjWg6mk63esVs3//iGqsfQBKA6KhJgy4/daSKDnUlv
RbzJXWFi2gd2FBvGZUvRb/otdA34UvdhHr5q5A6DqPsKu++lj6rqMdDI1RFPr70T
N2Hd7xGevIWo620N/Hv884dkZ1QiJJ7d+BLavvLWwYy/l/c7NkwdMwFfqS1KMmLU
Nw5opyBi57I9lhYQTqexa6Fvs5lSvtK+C6YRI6PDn+7tRyqYYQdDANeNzUkn5rBV
ZGo5FuHlkyk0oKWX0kkYGLwaTV1BdTraeoYYywAJ59PC73pzCe4yBiQmDi6hsZ6D
DJtrngrGwrYhq87cjBAhK94FpgPSN8CK2XiLcMjmOi8KmVnjb0F6jKH6G0sadNi5
wm13Ec9XyrcggJUXmGBHQirHTyM3rkyI3C6xC2ZPbl6YxFyTbPruVJuFw2Cfivnk
b0nMdbfgyoNpOr+BiPqasGzwOgwAogZCFEHPamnOov/Wk/iodTYpR3rV4IAJWBxy
KLxZYZSf41cgTEZvOKIE2vP8jPnm/ag3T+qTEAsBSf1Y6w1ohLbifF4APq9WmJ8g
kFuexEyHJUeivojUX2j1V+qDwLJU4EjRsAaLC5dkTf5nF04nwbdnF+qiBsG0bsVK
V7sdKpbOEfFDQKe66bQ2n2t7jTVjOuS7sLRUx7bGLIEzj8mxhRNmxbXf/gb/Q0bw
r9T5WxkQnTI6ZwH8t/dYDhMvwpWPCkPqwvY/JAzY3J++AE9oGVdBOu+q9xIkWX7w
cy5VeGx2n/SLa+aNFXFi9FxyPHAozRnIM9ET8NuhEBncSgvlY1yjURmay8l0zCin
TOmyCewwVi8TVz9wdrqrHAoItamu+y5mQgU4jinbxWBytzaQ6gmZUsoKHMNOYpOQ
sg4mugUPR5Gv0xNn+1nZcVyL7nSGlxp7C0ujMVlBugKVR4091KizlHjfVrtuwRHG
RvdQJiP2pHXAQpBJduIgGAQsGDCk
=WmUf
-----END PGP PUBLIC KEY BLOCK-----
+102
View File
@@ -184,6 +184,108 @@ perl-Net-DNS=1.57
perl-Net-IP=1.26
perl-Path-Class=0.37
[openeuler-20.03sp4-x86_64]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTML-Form=6.07
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
python3-scp=0.14.5
xCAT-genesis-base=>= 2:2.18.0
[openeuler-22.03sp4-x86_64]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
xCAT-genesis-base=>= 2:2.18.0
[openeuler-24.03sp1-x86_64]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
xCAT-genesis-base=>= 2:2.18.0
[openeuler-24.03sp3-x86_64]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
xCAT-genesis-base=>= 2:2.18.0
[openeuler-24.03sp4-x86_64]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
xCAT-genesis-base=>= 2:2.18.0
[openeuler-24.03-ppc64le]
goconserver=>= 0.3.3-snap202011021058
grub2-xcat=1.0
ipmitool-xcat=>= 1.8.18-4
syslinux-xcat=>= 6.03-1
xnba-undi=>= 1.21.1-1
perl-Crypt-Rijndael=1.13
perl-Crypt-SSLeay=0.72
perl-HTTP-Async=>= 0.30-3
perl-IO-Stty=>= 0.04-5
perl-Net-HTTPS-NB=>= 0.14-3
perl-Net-Telnet=3.04
xCAT-genesis-base=>= 2:2.18.0
ksh=1.0.8
net-snmp=5.9.3
net-snmp-libs=5.9.3
net-snmp-perl=5.9.3
perl-DB_File=1.859
perl-Digest-SHA1=2.13
perl-IO-Tty=1.17
perl-Sys-Virt=4.7.0
perl-CGI=4.57
perl-Crypt-CBC=2.33
perl-Expect=1.35
perl-HTML-Form=6.11
perl-LWP-Protocol-https=6.10
perl-Mail-Sender=0.903
perl-Net-DNS=1.40
perl-DBD-Pg=3.18.0
# [common] is NOT a build target. It describes the SHARED repository the OpenEmbedded Genesis
# release is published into (<repo-dep>/common), which lives beside the per-EL cells and is
# therefore invisible to every [<target>] section above. Without it nothing asserted the published
+52
View File
@@ -0,0 +1,52 @@
openEuler 24.03 LTS native build inputs
This source supplies a build prerequisite for the native perl-DBD-Pg checks.
The dependency repository does not select a PostgreSQL server for the normal
xCAT/xCATsn closure. PostgreSQL backend runtime qualification is separate.
Source RPM:
https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/postgresql-15.6-1.oe2403.src.rpm
SHA256: 0e1dd792cccf353f7a0d7f3f9d13b1a22a7ace0a428f4b197193b99c58081960
Publisher key:
https://repo.openeuler.org/openEuler-24.03-LTS/source/RPM-GPG-KEY-openEuler
Fingerprint: 8AA16BF9F2CA5244010DCA963B477C60B675600B
Verify the source RPM checksum, signature, and payload digests before extraction.
Apply postgresql-15.6-openeuler-llvmjit-buildrequires.patch with patch -p1
from the directory containing the extracted postgresql.spec.
The patch makes the clang BuildRequires follow the existing llvmjit option.
PostgreSQL uses clang to generate LLVM bitcode. Its normal C compiler selection
uses gcc or cc, and its LLVM build paths are disabled when llvmjit is zero.
JIT-enabled BuildRequires remain unchanged.
Use the native openEuler build environment and these existing vendor options:
--define 'llvmjit 0' --define 'external_libpq 0'
--define 'runselftest 1' --define 'test 1'
Keep the other vendor feature defaults, including SSL, GSSAPI, LDAP, PAM,
SELinux, ICU, UUID, and procedural languages. Build as an unprivileged user;
the regression tests require this. Retain the regression, contrib, procedural
language, and postgresql-setup tests. Resolve all remaining BuildRequires from
signed native packages before building.
Use the vendor-default private libpq build. Its libraries have private SONAMEs.
The external_libpq=1 branch in this SRPM has a stale patch that fails during
normal preparation. Build DBD-Pg with the vendor libpq-devel package;
postgresql-private-devel declares a conflict with that package.
On ppc64le, use mock-configs/openeuler-24.03-ppc64le.cfg when invoking the
existing mock build owner. The downloaded source RPM and generated packages
are external build artifacts. This directory does not add a package builder.
After building, verify package Provides/Requires, native architecture, and
libpq linkage. Build the matching native DBD-Pg source RPM without changes:
https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-DBD-Pg-3.18.0-1.oe2403.src.rpm
SHA256: 5bb91b28459ee5754c031cf62471c86a887392587c261c8df61ab44e4c8caf09
Retain its make test check. Validate xCAT schema initialization, SQL
transactions, authentication, backup/restore, and reconnect behavior before
using the resulting packages for management or service nodes.
@@ -0,0 +1,14 @@
--- a/postgresql.spec
+++ b/postgresql.spec
@@ -78,7 +78,10 @@
Patch11: postgresql-datalayout-mismatch-on-s390.patch
Patch16: postgresql-pgcrypto-openssl3-tests.patch
-BuildRequires: gcc clang
+BuildRequires: gcc
+%if %llvmjit
+BuildRequires: clang
+%endif
BuildRequires: perl(ExtUtils::MakeMaker) glibc-devel bison flex gawk
BuildRequires: perl(ExtUtils::Embed), perl-devel
BuildRequires: perl-generators
+11
View File
@@ -0,0 +1,11 @@
`python-scp-0.14.5-1.oe2403.src.rpm` is the unchanged openEuler 24.03 LTS
source package, rebuilt for openEuler 20.03 LTS SP4, whose native repositories
do not provide `python3-scp`.
- Source: https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/python-scp-0.14.5-1.oe2403.src.rpm
- SHA256: `3461d2a3fe0122cac2893d8465ad1271ae21e5570a31d4402e3f887ef545a0e8`
- Upstream signing key: `8AA16BF9F2CA5244010DCA963B477C60B675600B`
- License: LGPL-2.1-or-later
The upstream spec disables its SSH-dependent `%check`. The build uses the
target's native Python and Paramiko packages.
Binary file not shown.
+96 -4
View File
@@ -10,13 +10,13 @@ use lib "$RealBin/..";
use File::Temp qw(tempdir);
use File::Path qw(make_path);
use File::Basename qw(basename);
use File::Slurper qw(write_text);
use File::Slurper qw(read_text write_text);
use MockBuildUtils qw(install_deps_packages install_deps_command missing_perl_modules
required_pkgs version_matches rpm_sigmd5 rpm_version rpm_release rpm_is_signed
rpm_arch rpm_in_cell resolve_mock_cfg
skipped_builder carry_over_rpms source_package
restamp_release_line cross_copy_genesis finalize_xcat_dep read_manifest
verify_repo_packages verify_repo_signature verify_rpm_signatures
derive_target_from_repo_path verify_repo_packages verify_repo_signature verify_rpm_signatures
parse_evr evr_constraint_ok parse_pin rpmkeys_checksig_problem
bump_dep_release_suffix build_mock_uniqueext);
@@ -403,6 +403,81 @@ SPEC
'the refusal names the arch');
}
{
my $tmp = tempdir(CLEANUP => 1);
my ($x, $p) = ("$tmp/x/rh9/x86_64", "$tmp/p/rh9/ppc64le");
my @native = ("$tmp/x/openeuler20.03sp4/x86_64", "$tmp/p/openeuler24.03/ppc64le");
make_path($x, $p, @native);
write_text("$x/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm", "x86 genesis\n");
write_text("$p/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm", "ppc genesis\n");
write_text("$_/marker", "native repository\n") for @native;
my (@signed, @reindexed);
my $ok = eval {
quiet { finalize_xcat_dep("$tmp/x", "$tmp/p",
sign => sub { push @signed, $_[0] },
reindex => sub { push @reindexed, $_[0] }) };
1;
};
ok($ok, 'mixed roots finalize their legacy cells without native peer requirements') or diag($@);
is(-f "$x/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm"
? read_text("$x/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm") : undef, "ppc genesis\n",
'the legacy x86 cell receives its foreign Genesis');
is(-f "$p/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm"
? read_text("$p/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm") : undef, "x86 genesis\n",
'the legacy ppc cell receives its foreign Genesis');
is_deeply([sort @signed], [sort { $a cmp $b } ("$x/xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm",
"$p/xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm")], 'only legacy copies are signed');
is_deeply([sort @reindexed], [sort { $a cmp $b } ($x, $p)], 'only legacy cells are indexed');
for my $native (@native) {
is_deeply([glob("$native/*")], ["$native/marker"], 'finalize adds no native artifacts');
is(read_text("$native/marker"), "native repository\n", 'finalize preserves native content');
}
}
{
my $tmp = tempdir(CLEANUP => 1);
my @native = ("$tmp/x/openeuler20.03sp4/x86_64", "$tmp/p/openeuler24.03/ppc64le");
make_path(@native);
write_text("$_/marker", "native repository\n") for @native;
my (@signed, @reindexed);
my $ok = eval {
quiet { finalize_xcat_dep("$tmp/x", "$tmp/p", only => ['x86_64'],
sign => sub { push @signed, $_[0] },
reindex => sub { push @reindexed, $_[0] }) };
1;
};
ok($ok, 'native-only roots require no legacy Genesis finalization') or diag($@);
is_deeply(\@signed, [], 'native-only finalization signs nothing');
is_deeply(\@reindexed, [], 'native-only finalization indexes nothing');
for my $native (@native) {
is_deeply([glob("$native/*")], ["$native/marker"], 'native-only finalization adds no artifacts');
is(read_text("$native/marker"), "native repository\n", 'native-only finalization preserves content');
}
my $bad = eval { quiet { finalize_xcat_dep("$tmp/x", "$tmp/p", only => ['riscv64']) }; 1 };
ok(!$bad, 'native-only roots still reject an unsupported finalization architecture');
like($@, qr/no cross-arch genesis for arch 'riscv64'/, 'native-only validation names the bad architecture');
}
for my $legacy ('x86-only', 'ppc-only', 'missing-genesis') {
my $tmp = tempdir(CLEANUP => 1);
make_path("$tmp/x/openeuler20.03sp4/x86_64", "$tmp/p/openeuler24.03/ppc64le");
make_path("$tmp/x/rh9/x86_64") unless $legacy eq 'ppc-only';
make_path("$tmp/p/rh9/ppc64le") unless $legacy eq 'x86-only';
my (@signed, @reindexed);
my $ok = eval {
quiet { finalize_xcat_dep("$tmp/x", "$tmp/p",
sign => sub { push @signed, $_[0] },
reindex => sub { push @reindexed, $_[0] }) };
1;
};
ok(!$ok, "$legacy legacy input remains fatal in mixed roots");
my $expected = $legacy eq 'x86-only' ? qr/no ppc64le peer repo/
: $legacy eq 'ppc-only' ? qr/no x86_64 peer repo/ : qr/no x86_64 xCAT-genesis-base/;
like($@, $expected, "$legacy reports the missing legacy input");
is_deeply(\@signed, [], "$legacy signs nothing");
is_deeply(\@reindexed, [], "$legacy indexes nothing");
}
# ---- restamp_release_line: CD --build-number Release stamping (PR #62 review point 1) ----------
# A fresh stamp is appended after the Release token, preserving any %{?dist} macro.
{
@@ -451,7 +526,7 @@ is(rpm_release(tempdir(CLEANUP => 1), 'nonexistent-pkg'), undef, 'rpm_release is
# Not every section is a build target: [common] describes the SHARED repository the
# OpenEmbedded Genesis release is published into, which no builder produces. Target sections are
# the ones named after a mock config (<id>+epel-<rel>-<arch>, opensuse-leap-<ver>-<arch>).
my @targets = grep { /^[a-z0-9.+-]+-\d+(?:\.\d+)?-[a-z0-9_]+$/ } sort keys %m;
my @targets = grep { !/^openeuler-/ && /^[a-z0-9.+-]+-\d+(?:\.\d+)?-[a-z0-9_]+$/ } sort keys %m;
cmp_ok(scalar(@targets), '>=', 1, 'packages-manifest.conf has at least one target section');
ok(!grep({ $_ eq 'common' } @targets), 'the shared-repo section is not treated as a build target');
my @missing = grep { !exists $m{$_}{'conserver-xcat'} } @targets;
@@ -459,8 +534,10 @@ is(rpm_release(tempdir(CLEANUP => 1), 'nonexistent-pkg'), undef, 'rpm_release is
my @no_ipxe_xcat = grep { exists $m{$_}{'xnba-undi'} && !exists $m{$_}{'ipxe-xcat'} } @targets;
is_deeply(\@no_ipxe_xcat, [], 'every target that lists xnba-undi also lists ipxe-xcat')
or diag("missing ipxe-xcat in: @no_ipxe_xcat");
is_deeply(\@missing, [], 'conserver-xcat is present in every manifest target section')
is_deeply(\@missing, [], 'conserver-xcat is present in every legacy manifest target section')
or diag("missing conserver-xcat in: @missing");
my @native_missing = grep { /^openeuler-/ && !exists $m{$_}{goconserver} } sort keys %m;
is_deeply(\@native_missing, [], 'native manifest targets retain goconserver');
# The forcearch riscv64 target carries the noarch boot components the ppc64le EL10 target
# carries, at the same pins: a riscv64 MN serves the x86 nodes of a mixed cluster too.
@@ -554,6 +631,21 @@ is(rpm_release(tempdir(CLEANUP => 1), 'nonexistent-pkg'), undef, 'rpm_release is
is($a_again, $a_after, 'a.spec content unchanged on the idempotent second call');
}
for my $case (
['/repo/rh8/x86_64', 'alma+epel-8-x86_64'],
['/repo/rh9/s390x/', 'alma+epel-9-s390x'],
['/repo/rh10/ppc64le//', 'alma+epel-10-ppc64le'],
['/repo/rh10/riscv64', 'alma+epel-10-riscv64'],
) {
my ($path, $target) = @$case;
is(derive_target_from_repo_path($path), $target, "$path selects $target");
}
is(derive_target_from_repo_path(undef), undef, 'missing path has no target');
is(derive_target_from_repo_path(''), undef, 'empty path has no target');
for my $path ('/repo', '/repo/rh10', '/repo/rh10/x86_64/repodata', '/repo/notrh10/x86_64') {
is(derive_target_from_repo_path($path), undef, "$path has no target");
}
# ---- verify_repo_packages: pure repo-completeness decision (MISSING + VERSION + wildcard) ---------
# The gate's completeness layer: given manifest pins and the versions actually present in a repo,
# return the list of problems (empty = complete). No I/O -- exercised directly with plain hashes.
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin qw($RealBin);
use lib "$RealBin/..";
use Test::More;
use MockBuildUtils qw(openeuler_build_target openeuler_repo_subdir install_deps_command
install_deps_packages derive_target_from_repo_path read_manifest);
my %manifest = read_manifest("$RealBin/../packages-manifest.conf");
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";
ok(-f "$RealBin/../mock-configs/$target.cfg", "$target has a native mock config");
ok(exists $manifest{$target}{goconserver}, "$target has a native package manifest");
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");
for my $suffix ('', '/', '//') {
my $path = "/repo/openeuler$version/$arch$suffix";
is(derive_target_from_repo_path($path), $target, "$path selects $target");
}
}
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');
for my $path (
'/repo/openeuler24.09/x86_64',
'/repo/openeuler24.03sp0/x86_64',
'/repo/openeuler24.03/ppc64',
'/repo/openeuler24.03',
'/repo/openeuler24.03/x86_64/repodata',
'/repo/notopeneuler24.03/x86_64',
) {
is(derive_target_from_repo_path($path), undef, "$path has no native target");
}
done_testing();
+9
View File
@@ -7,6 +7,9 @@ use File::Basename qw(dirname);
use File::Copy qw(copy);
use File::Path qw(make_path remove_tree);
use Getopt::Long qw(GetOptions);
use FindBin qw($RealBin);
use lib "$RealBin/..";
use MockBuildUtils qw(restamp_release_line);
my $script_dir = abs_path(dirname(__FILE__));
my $repo_root = abs_path("$script_dir/..");
@@ -20,6 +23,7 @@ my $mock_uniqueext = '';
my $result_dir = "$repo_root/build-output/list3/xnba-undi";
my $log_dir = "$repo_root/build-logs/list3/xnba-undi";
my $build_timestamp;
my $release_suffix = '';
GetOptions(
'work-dir=s' => \$work_dir,
@@ -28,6 +32,7 @@ GetOptions(
'result-dir=s' => \$result_dir,
'log-dir=s' => \$log_dir,
'build-timestamp=i' => \$build_timestamp,
'release-suffix=s' => \$release_suffix,
) or die usage();
die "Run as root (current uid=$>)\n" if $> != 0;
@@ -133,6 +138,9 @@ install -m 644 binary/xnba.efi %{buildroot}/tftpboot/xcat/xnba.efi
- Packaged pre-built xnba binaries for EL10
SPEC
$simple_spec = join('', map { (restamp_release_line($_, $release_suffix))[0] }
split(/(?<=\n)/, $simple_spec)) if $release_suffix ne '';
open my $fh, '>', "$rpmbuild_top/SPECS/xnba-undi.spec"
or die "Cannot write spec: $!\n";
print $fh $simple_spec;
@@ -174,6 +182,7 @@ Options:
--result-dir PATH Output directory for RPMs
--log-dir PATH Output directory for logs
--build-timestamp EPOCH Unix timestamp for reproducible builds
--release-suffix STR Append a suffix to the generated RPM Release
USAGE
}