diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e520fca --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch whitespace=-blank-at-eol diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index 326c776..49c60f0 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -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 diff --git a/MockBuildUtils.pm b/MockBuildUtils.pm index 3f2bf85..2dca2e3 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -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--*.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 -# /x86_64 and /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 { # 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 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; diff --git a/goconserver/mockbuild.pl b/goconserver/mockbuild.pl index 15195c1..6acc658 100755 --- a/goconserver/mockbuild.pl +++ b/goconserver/mockbuild.pl @@ -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 diff --git a/goconserver/toolchains/README.md b/goconserver/toolchains/README.md new file mode 100644 index 0000000..73c7e48 --- /dev/null +++ b/goconserver/toolchains/README.md @@ -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. diff --git a/goconserver/toolchains/go1.25.12.sha256 b/goconserver/toolchains/go1.25.12.sha256 new file mode 100644 index 0000000..e698cb0 --- /dev/null +++ b/goconserver/toolchains/go1.25.12.sha256 @@ -0,0 +1,2 @@ +234828b7a89e0e303d2556310ee549fbcf253d28de937bac3da13d6294262ac1 go1.25.12.linux-amd64.tar.gz +64adb4ddefef4f0a6f11af550547f39bf510350da69ab308438a21eacfde97ad go1.25.12.linux-ppc64le.tar.gz diff --git a/grub2-xcat/grub2-xcat.spec b/grub2-xcat/grub2-xcat.spec index 54bd047..de43168 100644 --- a/grub2-xcat/grub2-xcat.spec +++ b/grub2-xcat/grub2-xcat.spec @@ -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/ diff --git a/lib/XCAT/NativeInputs.pm b/lib/XCAT/NativeInputs.pm new file mode 100644 index 0000000..df78564 --- /dev/null +++ b/lib/XCAT/NativeInputs.pm @@ -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; diff --git a/mock-configs/openeuler-20.03sp4-x86_64.cfg b/mock-configs/openeuler-20.03sp4-x86_64.cfg new file mode 100644 index 0000000..f6ada6a --- /dev/null +++ b/mock-configs/openeuler-20.03sp4-x86_64.cfg @@ -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') diff --git a/mock-configs/openeuler-22.03sp4-x86_64.cfg b/mock-configs/openeuler-22.03sp4-x86_64.cfg new file mode 100644 index 0000000..4261f65 --- /dev/null +++ b/mock-configs/openeuler-22.03sp4-x86_64.cfg @@ -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') diff --git a/mock-configs/openeuler-24.03-ppc64le.cfg b/mock-configs/openeuler-24.03-ppc64le.cfg new file mode 100644 index 0000000..5b3ce7c --- /dev/null +++ b/mock-configs/openeuler-24.03-ppc64le.cfg @@ -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') diff --git a/mock-configs/openeuler-24.03sp1-x86_64.cfg b/mock-configs/openeuler-24.03sp1-x86_64.cfg new file mode 100644 index 0000000..2ca84c4 --- /dev/null +++ b/mock-configs/openeuler-24.03sp1-x86_64.cfg @@ -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') diff --git a/mock-configs/openeuler-24.03sp3-x86_64.cfg b/mock-configs/openeuler-24.03sp3-x86_64.cfg new file mode 100644 index 0000000..82241c1 --- /dev/null +++ b/mock-configs/openeuler-24.03sp3-x86_64.cfg @@ -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') diff --git a/mock-configs/openeuler-24.03sp4-x86_64.cfg b/mock-configs/openeuler-24.03sp4-x86_64.cfg new file mode 100644 index 0000000..4d42489 --- /dev/null +++ b/mock-configs/openeuler-24.03sp4-x86_64.cfg @@ -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') diff --git a/mock-configs/templates/openeuler-lts-xcat.tpl b/mock-configs/templates/openeuler-lts-xcat.tpl new file mode 100644 index 0000000..5f80d47 --- /dev/null +++ b/mock-configs/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) diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 01374ec..176999a 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -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 (+epel--) 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, /rh/, 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 -# /rh/: 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/ -> alma+epel--) unless --target is given; the + (.../rh/ -> alma+epel--, or + .../openeuler/ -> openeuler--) + 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 (+epel--, 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 + /openeuler/ 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/ to its manifest -# target section name alma+epel--. Returns undef when the path lacks that rh/ 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}"; } diff --git a/native/README.md b/native/README.md new file mode 100644 index 0000000..e999353 --- /dev/null +++ b/native/README.md @@ -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. diff --git a/native/buildinfo_provenance.t b/native/buildinfo_provenance.t new file mode 100644 index 0000000..225c6cc --- /dev/null +++ b/native/buildinfo_provenance.t @@ -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(); diff --git a/native/fixtures/mock-configs.py b/native/fixtures/mock-configs.py new file mode 100644 index 0000000..a2150f5 --- /dev/null +++ b/native/fixtures/mock-configs.py @@ -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)) diff --git a/native/fixtures/power-mock.py b/native/fixtures/power-mock.py new file mode 100644 index 0000000..9abe53e --- /dev/null +++ b/native/fixtures/power-mock.py @@ -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) diff --git a/native/fixtures/power-wget.pl b/native/fixtures/power-wget.pl new file mode 100644 index 0000000..a84b69b --- /dev/null +++ b/native/fixtures/power-wget.pl @@ -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 $!; diff --git a/native/fixtures/srpm-mock.pl b/native/fixtures/srpm-mock.pl new file mode 100644 index 0000000..d14d218 --- /dev/null +++ b/native/fixtures/srpm-mock.pl @@ -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 $!; + } +} diff --git a/native/goconserver-openeuler.t b/native/goconserver-openeuler.t new file mode 100644 index 0000000..2383972 --- /dev/null +++ b/native/goconserver-openeuler.t @@ -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(); diff --git a/native/mock-configs.t b/native/mock-configs.t new file mode 100644 index 0000000..26985c0 --- /dev/null +++ b/native/mock-configs.t @@ -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(); diff --git a/native/openeuler-power-inputs.t b/native/openeuler-power-inputs.t new file mode 100644 index 0000000..b13f3c4 --- /dev/null +++ b/native/openeuler-power-inputs.t @@ -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(); diff --git a/native/openeuler-srpm.t b/native/openeuler-srpm.t new file mode 100644 index 0000000..fd9e899 --- /dev/null +++ b/native/openeuler-srpm.t @@ -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'); +} diff --git a/native/xnba-release-suffix.t b/native/xnba-release-suffix.t new file mode 100644 index 0000000..8821e96 --- /dev/null +++ b/native/xnba-release-suffix.t @@ -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(); diff --git a/openeuler/24.03-ppc64le.inputs.json b/openeuler/24.03-ppc64le.inputs.json new file mode 100644 index 0000000..8c0af3c --- /dev/null +++ b/openeuler/24.03-ppc64le.inputs.json @@ -0,0 +1,1123 @@ +{ + "version": 1, + "target": "openeuler-24.03-ppc64le", + "publisher_key": { + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/RPM-GPG-KEY-openEuler", + "sha256": "006e79d37c10e74c24df6d07c4efc4176515cec009daa5ed493b06f5b6ef39c1", + "fingerprint": "8AA16BF9F2CA5244010DCA963B477C60B675600B", + "path": "openeuler/RPM-GPG-KEY-openEuler-24.03-LTS" + }, + "build_inputs": [ + "perl-CPAN-Changes", + "perl-Class-Method-Modifiers", + "perl-Data-OptList", + "perl-Data-Section", + "perl-Devel-GlobalDestruction", + "perl-Devel-Symdump", + "perl-ExtUtils-CBuilder", + "perl-Import-Into", + "perl-MRO-Compat", + "perl-Module-Build", + "perl-Module-Runtime", + "perl-Moo", + "perl-Package-Generator", + "perl-Parallel-ForkManager", + "perl-Path-Class", + "perl-Pod-Coverage", + "perl-Role-Tiny", + "perl-Software-License", + "perl-Sub-Exporter", + "perl-Sub-Exporter-Progressive", + "perl-Sub-Install", + "perl-Sub-Quote", + "perl-Sub-Uplevel", + "perl-Test-Exception", + "perl-Test-Pod", + "perl-Test-Pod-Coverage", + "perl-Test-Warnings", + "perl-Text-Template", + "perl-XML-XPath", + "perl-inc-latest", + "python3-mock", + "python3-pbr", + "python3-wheel", + "procenv" + ], + "bootstrap_inputs": [ + "mock", + "perl-Params-Util", + "python-psutil", + "python-pyroute2", + "perl-PerlIO-utf8_strict", + "perl-File-Slurper", + "procenv" + ], + "inputs": [ + { + "name": "ksh", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/ksh-1.0.8-1.oe2403.src.rpm", + "sha256": "06d31f1ed0213f58d7009d5830bec3b3cb26878c4ae4deaeb8f8f4d74acb33fb", + "outputs": [ + "ksh", + "ksh-debuginfo", + "ksh-debugsource" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "net-snmp", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/net-snmp-5.9.3-2.oe2403.src.rpm", + "sha256": "c6edbda654e5f4416e19c3f56004457daf72e4ae7058f118030c3442c486930b", + "outputs": [ + "net-snmp", + "net-snmp-debuginfo", + "net-snmp-debugsource", + "net-snmp-devel", + "net-snmp-gui", + "net-snmp-help", + "net-snmp-libs", + "net-snmp-perl", + "python3-net-snmp" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-DB_File", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-DB_File-1.859-1.oe2403.src.rpm", + "sha256": "cce82dd316ed906fde94ec69e5d0298322dc4b14dba89dd3151c2823f404e8b3", + "outputs": [ + "perl-DB_File", + "perl-DB_File-debuginfo", + "perl-DB_File-debugsource", + "perl-DB_File-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Digest-SHA1", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-Digest-SHA1-2.13-27.oe2403.src.rpm", + "sha256": "2761ba4d7f43727908af232cd03b2d872f245380acfecc9cf7b65d0a2da83060", + "outputs": [ + "perl-Digest-SHA1", + "perl-Digest-SHA1-debuginfo", + "perl-Digest-SHA1-debugsource", + "perl-Digest-SHA1-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-IO-Tty", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-IO-Tty-1.17-1.oe2403.src.rpm", + "sha256": "28a795eb3d834e10760dfa7e016e697c6e5c3acbdb35ee5c3971b44146c3e903", + "outputs": [ + "perl-IO-Tty", + "perl-IO-Tty-debuginfo", + "perl-IO-Tty-debugsource", + "perl-IO-Tty-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Sys-Virt", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-Sys-Virt-4.7.0-3.oe2403.src.rpm", + "sha256": "a7603e7e27afb1aa60c69852c583fc0837a895fd3975aabf7470013d4f798482", + "outputs": [ + "perl-Sys-Virt", + "perl-Sys-Virt-debuginfo", + "perl-Sys-Virt-debugsource", + "perl-Sys-Virt-help" + ], + "needs": [ + "perl-Params-Util", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "nasm", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/nasm-2.16.01-1.oe2403.src.rpm", + "sha256": "0dac600e4da291eefb1986daabea54f9bf60aeeb5b8c8940faeb86dbcd20ea4a", + "outputs": [ + "nasm", + "nasm-debuginfo", + "nasm-debugsource", + "nasm-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "docbook-style-dsssl", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/docbook-style-dsssl-1.79-28.oe2403.src.rpm", + "sha256": "9bc538924ce5c76e0006cd25cb1af34a3ea9b754bbd25c6fc1a65416274e5b4a", + "outputs": [ + "docbook-style-dsssl", + "docbook-style-dsssl-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "docbook-utils", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/docbook-utils-0.6.14-47.oe2403.src.rpm", + "sha256": "2337c62d716bafcb90910f8a530d7a037ca6d93905d0b26cfb27a2d2ecf39d05", + "outputs": [ + "docbook-utils", + "docbook-utils-help", + "docbook-utils-pdf" + ], + "needs": [ + "docbook-style-dsssl", + "openjade", + "perl-SGMLSpm", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "elinks", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/elinks-0.12-4.oe2403.src.rpm", + "sha256": "bd7e4695311b3eb259af15bb2a5c6154675de9cf07bf3b260591bb50ee19c40f", + "outputs": [ + "elinks", + "elinks-debuginfo", + "elinks-debugsource", + "elinks-help" + ], + "needs": [ + "lua", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "help2man", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/help2man-1.49.3-1.oe2403.src.rpm", + "sha256": "438f7a27f86e4906d03cb1451e93b6ae189948a4287a2cbd3ce3aa75b83f7eec", + "outputs": [ + "help2man", + "help2man-debuginfo", + "help2man-debugsource", + "help2man-help" + ], + "needs": [ + "perl-gettext", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "lua", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/lua-5.4.6-1.oe2403.src.rpm", + "sha256": "6e327bf54d114f3c6fb849a92fd8530e7d093d14e566cc9eca69e8ccd980f7ab", + "outputs": [ + "lua", + "lua-debuginfo", + "lua-debugsource", + "lua-devel", + "lua-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "multilib-rpm-config", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/multilib-rpm-config-1-16.oe2403.src.rpm", + "sha256": "807e501bf7a52e868a69fdb5c412a996858c835596e90786279257a80b63c84c", + "outputs": [ + "multilib-rpm-config" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "openjade", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/openjade-1.3.2-61.oe2403.src.rpm", + "sha256": "418003efee45c62a3c1576d960cfec704d328bf4c992362444c49ad9266955f2", + "outputs": [ + "openjade", + "openjade-debuginfo", + "openjade-debugsource", + "openjade-help" + ], + "needs": [ + "opensp", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "opensp", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/opensp-1.5.2-31.oe2403.src.rpm", + "sha256": "12dcec9276c9234fb05424ee668f046e04c42e48300bf4c2f5c97d3182f48785", + "outputs": [ + "opensp", + "opensp-debuginfo", + "opensp-debugsource", + "opensp-devel" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Data-UUID", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-Data-UUID-1.226-1.oe2403.src.rpm", + "sha256": "bff5621c60eb5d5e883e222c4e4a6654f6b6babde1ecfbd350a9a82d4f986143", + "outputs": [ + "perl-Data-UUID", + "perl-Data-UUID-debuginfo", + "perl-Data-UUID-debugsource", + "perl-Data-UUID-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-SGMLSpm", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-SGMLSpm-1.03ii-46.oe2403.src.rpm", + "sha256": "d64347233d55e1fed96b3f0956a6b6654875eed1bb691d19b381a824be5e48b6", + "outputs": [ + "perl-SGMLSpm", + "perl-SGMLSpm-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-gettext", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-gettext-1.07-15.oe2403.src.rpm", + "sha256": "4c957422db0487ad914ce71dea89c81eb17d9ffd5fc4388cba79e4f449eb1146", + "outputs": [ + "perl-Locale-gettext", + "perl-gettext-debuginfo", + "perl-gettext-debugsource", + "perl-gettext-help" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "uuid", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/uuid-1.6.2-47.oe2403.src.rpm", + "sha256": "208ec1a90ac611a11527eb813fe94bd8349085b9e10ac4d784c551933633a7f5", + "outputs": [ + "uuid", + "uuid-c++", + "uuid-c++-devel", + "uuid-dce", + "uuid-dce-devel", + "uuid-debuginfo", + "uuid-debugsource", + "uuid-devel", + "uuid-help", + "uuid-perl" + ], + "needs": [ + "perl-Data-UUID", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "postgresql", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/postgresql-15.6-1.oe2403.src.rpm", + "sha256": "0e1dd792cccf353f7a0d7f3f9d13b1a22a7ace0a428f4b197193b99c58081960", + "outputs": [ + "postgresql", + "postgresql-contrib", + "postgresql-debuginfo", + "postgresql-debugsource", + "postgresql-docs", + "postgresql-plperl", + "postgresql-plpython3", + "postgresql-pltcl", + "postgresql-private-devel", + "postgresql-private-libs", + "postgresql-server", + "postgresql-server-devel", + "postgresql-static", + "postgresql-test", + "postgresql-test-rpm-macros" + ], + "needs": [ + "docbook-utils", + "elinks", + "help2man", + "multilib-rpm-config", + "procenv", + "uuid" + ], + "patches": [ + { + "path": "postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch", + "sha256": "799a90eb82321722d11ffc421d4b09684f2939c44bfdc71a6c9b039a4f4d8592" + } + ], + "defines": [ + "llvmjit 0", + "external_libpq 0", + "runselftest 1", + "test 1" + ], + "build_uid": 1000 + }, + { + "name": "perl-DBD-Pg", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-DBD-Pg-3.18.0-1.oe2403.src.rpm", + "sha256": "5bb91b28459ee5754c031cf62471c86a887392587c261c8df61ab44e4c8caf09", + "outputs": [ + "perl-DBD-Pg", + "perl-DBD-Pg-debuginfo", + "perl-DBD-Pg-debugsource", + "perl-DBD-Pg-help" + ], + "needs": [ + "postgresql", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-CGI", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-CGI-4.57-1.oe2403.noarch.rpm", + "sha256": "98609a3a6c210a676a14960531cd5c82a86667da7aacea0c6e693ccbee612ee3", + "outputs": [ + "perl-CGI" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-CPAN-Changes", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-CPAN-Changes-0.400002-10.oe2403.noarch.rpm", + "sha256": "95c2a1939b26e201a07d128851b53a4fff7522b84f7a9ddb0947edf1f1ed2505", + "outputs": [ + "perl-CPAN-Changes" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Class-Method-Modifiers", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Class-Method-Modifiers-2.15-1.oe2403.noarch.rpm", + "sha256": "e92f880e1a3eef95025938da643e07a05db0d7f2eda140a1fdaeef64d6c0a8a7", + "outputs": [ + "perl-Class-Method-Modifiers" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Crypt-CBC", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Crypt-CBC-2.33-22.oe2403.noarch.rpm", + "sha256": "60f2dd1de7c4143301bcc1a35d52f3b69b3c40effe377a5740114d58a1205fc0", + "outputs": [ + "perl-Crypt-CBC" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-Data-OptList", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Data-OptList-0.114-1.oe2403.noarch.rpm", + "sha256": "68176d8117d5bbab5761e9b658465e96e01ecf5d6daa0e3cb400b45d77b3ecaf", + "outputs": [ + "perl-Data-OptList" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Data-Section", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Data-Section-0.200007-6.oe2403.noarch.rpm", + "sha256": "fb307d87653b15130f0bb0480761a7e86eed8052399977f9a811977b10792f62", + "outputs": [ + "perl-Data-Section" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Devel-GlobalDestruction", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Devel-GlobalDestruction-0.14-8.oe2403.noarch.rpm", + "sha256": "f141968f5359ed78945f08b042782889f3ff30b0d41a8ed8e9a284f1ecb80c54", + "outputs": [ + "perl-Devel-GlobalDestruction" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Devel-Symdump", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Devel-Symdump-2.18-9.oe2403.noarch.rpm", + "sha256": "a304653f87360e565bb40f91569ccf168535f8b9e801536da9f5993cc5ca15c7", + "outputs": [ + "perl-Devel-Symdump" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Expect", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Expect-1.35-7.oe2403.noarch.rpm", + "sha256": "a99336a76e9c9245026e7e4dc8cbddd5386dce30de91de0013905cbc9cbe9a41", + "outputs": [ + "perl-Expect" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-ExtUtils-CBuilder", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-ExtUtils-CBuilder-0.280236-1.oe2403.noarch.rpm", + "sha256": "fa99812f5cfff7d24fcbc98b40741db286e1dcfb4412ad9cdcf47fcf7722a474", + "outputs": [ + "perl-ExtUtils-CBuilder" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-HTML-Form", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-HTML-Form-6.11-1.oe2403.noarch.rpm", + "sha256": "163a17e21393bb97e8c2e16b9ac163aea99983c294ded47c00f98998bcb1caff", + "outputs": [ + "perl-HTML-Form" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-Import-Into", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Import-Into-1.002005-2.oe2403.noarch.rpm", + "sha256": "a5bad0bbf3e5ae2789857799114f84867b7c93044b233754d238dd97b2ad563d", + "outputs": [ + "perl-Import-Into" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-LWP-Protocol-https", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-LWP-Protocol-https-6.10-1.oe2403.noarch.rpm", + "sha256": "ee20297e3736754b5751b4160ac9a236d2d876a5bc85a7ba7bada51928bf9f74", + "outputs": [ + "perl-LWP-Protocol-https" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-MRO-Compat", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-MRO-Compat-0.15-1.oe2403.noarch.rpm", + "sha256": "3c0bb3b8a227e1682c262fb3456179da9bd2f50f47665c421104944cdb0bb0da", + "outputs": [ + "perl-MRO-Compat" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Mail-Sender", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Mail-Sender-0.903-9.oe2403.noarch.rpm", + "sha256": "dcf401ee82064f8c06e6e22b8e28f24be97600b197ca0bc3fc3fb8883ca6f5f7", + "outputs": [ + "perl-Mail-Sender" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-Module-Build", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Module-Build-0.42.34-1.oe2403.noarch.rpm", + "sha256": "db8163ae929ca61af611d2f13e8f9b946011ecad34811a36a5bdbe4ba74dae93", + "outputs": [ + "perl-Module-Build" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Module-Runtime", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Module-Runtime-0.016-5.oe2403.noarch.rpm", + "sha256": "87a0baf3236ccc778749c60992a79f1fdc5f4ebdb28f6f55927d0360ad37468c", + "outputs": [ + "perl-Module-Runtime" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Moo", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Moo-2.005005-1.oe2403.noarch.rpm", + "sha256": "277a6477d7fe6320d3b487f38f245fc468abf94e9a5b75c134c8a941488ae529", + "outputs": [ + "perl-Moo" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Net-DNS", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Net-DNS-1.40-1.oe2403.noarch.rpm", + "sha256": "c6d9932ccfaae2e515635b00204e86b5121516f5693d4fb345ae0d979bfbde23", + "outputs": [ + "perl-Net-DNS" + ], + "needs": [], + "runtime": true + }, + { + "name": "perl-Package-Generator", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Package-Generator-1.106-14.oe2403.noarch.rpm", + "sha256": "b9a424e6e083920b5000482637b62f617eb31808285ad86c09727e4f6d2c3fa8", + "outputs": [ + "perl-Package-Generator" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Parallel-ForkManager", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Parallel-ForkManager-2.02-2.oe2403.noarch.rpm", + "sha256": "f406916c07bc1c59cb83080c91b308aa0a3e560825b948f44d02d6f93b5f9050", + "outputs": [ + "perl-Parallel-ForkManager" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Path-Class", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Path-Class-0.37-14.oe2403.noarch.rpm", + "sha256": "aa2a7bc7789bfa34becdc8d98ad2263091cce2065776ab670802b20b72e497ce", + "outputs": [ + "perl-Path-Class" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Pod-Coverage", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Pod-Coverage-0.23-16.oe2403.noarch.rpm", + "sha256": "d55a851604e72bd8eae598883b6d07f7795bde516b6a82e9af2654294412e3d0", + "outputs": [ + "perl-Pod-Coverage" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Role-Tiny", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Role-Tiny-2.002004-1.oe2403.noarch.rpm", + "sha256": "058122267ea240407d97677450d50e251708d88dd9edf11a0e9cc3dfe813389d", + "outputs": [ + "perl-Role-Tiny" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Software-License", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Software-License-0.104001-2.oe2403.noarch.rpm", + "sha256": "511ad1cd11c7acd42798f05662d8732c67bb5780bde034912102b9b9628e96f4", + "outputs": [ + "perl-Software-License" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Sub-Exporter", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Sub-Exporter-0.990-1.oe2403.noarch.rpm", + "sha256": "1bad67d715ab6ff15e4add9f0c41b2e0f505ccc25ef0ca75ed75bcb1332d040b", + "outputs": [ + "perl-Sub-Exporter" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Sub-Exporter-Progressive", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Sub-Exporter-Progressive-0.001013-8.oe2403.noarch.rpm", + "sha256": "dbb9b6a648ea82eaef58d6c60136e0e6101b54c40a1b6e346d85df46fd5947f4", + "outputs": [ + "perl-Sub-Exporter-Progressive" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Sub-Install", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Sub-Install-0.929-1.oe2403.noarch.rpm", + "sha256": "5a828b848f5b730aaf30ffd9b2f7608cde97f69737bfb3abed4c7d6dd44a8e7b", + "outputs": [ + "perl-Sub-Install" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Sub-Quote", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Sub-Quote-2.005001-4.oe2403.noarch.rpm", + "sha256": "f11b0e95c4ec3df9ed03ee923a127b2cc84794a740dbe521f3adbfc52c7ca401", + "outputs": [ + "perl-Sub-Quote" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Sub-Uplevel", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Sub-Uplevel-0.2800-5.oe2403.noarch.rpm", + "sha256": "faa82aeb4f083ff0b4dac06a1219deacaf35ad6fc682ecbd91dfea8efbaaba74", + "outputs": [ + "perl-Sub-Uplevel" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Test-Exception", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Test-Exception-0.43-10.oe2403.noarch.rpm", + "sha256": "ba94cb3100826ef941055017174d3b613416e785b04d20792725c17007a82361", + "outputs": [ + "perl-Test-Exception" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Test-Pod", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Test-Pod-1.52-4.oe2403.noarch.rpm", + "sha256": "edcfbc73f8d1b848b5ff4e40699e74820f1f6c60f031f264381c29c6f01ba43a", + "outputs": [ + "perl-Test-Pod" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Test-Pod-Coverage", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Test-Pod-Coverage-1.10-14.oe2403.noarch.rpm", + "sha256": "953f3ca4c78abc2ac4c63e2debece478409766c54e9a2ec08c2f8b1247505e4b", + "outputs": [ + "perl-Test-Pod-Coverage" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Test-Warnings", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Test-Warnings-0.031-1.oe2403.noarch.rpm", + "sha256": "b152ba3fddab972447eb341a2cfd845d3851146d554605a02da2101d56387b8f", + "outputs": [ + "perl-Test-Warnings" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-Text-Template", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-Text-Template-1.60-1.oe2403.noarch.rpm", + "sha256": "04012c8e50e8fca42b22feadc51ac84a03cb1811067cd88eadd97ed76c6138b0", + "outputs": [ + "perl-Text-Template" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-XML-XPath", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-XML-XPath-1.47-1.oe2403.noarch.rpm", + "sha256": "4b4c440628ca82c3eaf997b2b52b38e243cb52eb3e6cc8296a858a0905008ea6", + "outputs": [ + "perl-XML-XPath" + ], + "needs": [], + "runtime": false + }, + { + "name": "perl-inc-latest", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/perl-inc-latest-0.500-12.oe2403.noarch.rpm", + "sha256": "46df92ce44f1dcb654642b42650dc790455dbb82d6a8594d70227c406874d82b", + "outputs": [ + "perl-inc-latest" + ], + "needs": [], + "runtime": false + }, + { + "name": "python3-mock", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/python3-mock-5.1.0-1.oe2403.noarch.rpm", + "sha256": "2cf15ae56a3c01b85be156c939cc4c26d3b0a48f67327e4089484d11c8d719a5", + "outputs": [ + "python3-mock" + ], + "needs": [], + "runtime": false + }, + { + "name": "python3-pbr", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/python3-pbr-6.0.0-1.oe2403.noarch.rpm", + "sha256": "b841a0ee0421804080a2a9dd1b58991b59098b61eaf50ea5cee08e74b245ee1d", + "outputs": [ + "python3-pbr" + ], + "needs": [], + "runtime": false + }, + { + "name": "python3-wheel", + "type": "publisher", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/everything/x86_64/Packages/python3-wheel-0.40.0-1.oe2403.noarch.rpm", + "sha256": "a5f12a955d77f97f571d9d2253b845976ec52b14cb0ed2e1f22504941e2093c6", + "outputs": [ + "python3-wheel" + ], + "needs": [], + "runtime": false + }, + { + "name": "mock", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/mock-2.2-2.oe2403.src.rpm", + "sha256": "15aee65d6d6284522c3c777c2355a7b4310a706bbff4a99d227c9917e32873db", + "outputs": [ + "mock", + "mock-plugins" + ], + "build_uid": 1000, + "needs": [ + "procenv", + "python-psutil", + "python-pyroute2" + ] + }, + { + "name": "perl-Params-Util", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-Params-Util-1.07-26.oe2403.src.rpm", + "sha256": "66a4363f542e00063dbc37e0b5abe42c9537c2f54c3e1d1d8290772d4d3f1824", + "outputs": [ + "perl-Params-Util", + "perl-Params-Util-debuginfo", + "perl-Params-Util-debugsource", + "perl-Params-Util-help" + ], + "build_uid": 1000, + "needs": [ + "procenv" + ] + }, + { + "name": "python-psutil", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/python-psutil-5.9.5-1.oe2403.src.rpm", + "sha256": "f0dc3859323b5e769cc8d3910d1d19645f6c868f152e9053fedd59a279fdc27f", + "outputs": [ + "python-psutil-debuginfo", + "python-psutil-debugsource", + "python3-psutil" + ], + "build_uid": 1000, + "needs": [ + "procenv" + ] + }, + { + "name": "python-pyroute2", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/python-pyroute2-0.7.9-2.oe2403.src.rpm", + "sha256": "4f42494d851c91bb2416254fdbfa04eb33acf23bd3507b5fc03ad62089db8bed", + "outputs": [ + "python-pyroute2-help", + "python3-pyroute2" + ], + "build_uid": 1000, + "needs": [ + "procenv" + ] + }, + { + "name": "perl-PerlIO-utf8_strict", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/perl-PerlIO-utf8_strict-0.010-1.oe2403.src.rpm", + "sha256": "5f0749a94e7e42b68a4b5a8ad13b06696cc02cceb302924cdacbecaaa43209d8", + "outputs": [ + "perl-PerlIO-utf8_strict", + "perl-PerlIO-utf8_strict-debuginfo", + "perl-PerlIO-utf8_strict-debugsource", + "perl-PerlIO-utf8_strict-help" + ], + "build_uid": 1000, + "needs": [ + "procenv" + ] + }, + { + "name": "perl-File-Slurper", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS-SP3/source/Packages/perl-File-Slurper-0.014-1.oe2403sp3.src.rpm", + "sha256": "7f0438344d8c191e74da13c26254f71374a32128ab9efc1d79705e9b00637c1e", + "outputs": [ + "perl-File-Slurper", + "perl-File-Slurper-help" + ], + "needs": [ + "perl-PerlIO-utf8_strict", + "perl-Test-Warnings", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "procenv", + "type": "srpm", + "url": "https://repo.openeuler.org/openEuler-24.03-LTS/source/Packages/procenv-0.60-1.oe2403.src.rpm", + "sha256": "4175affb56e6a39f6aac4ada813dee8802b3086807b17a1f9cd80e91717e7819", + "outputs": [ + "procenv", + "procenv-debuginfo", + "procenv-debugsource" + ], + "build_uid": 1000, + "needs": [] + }, + { + "name": "perl-Crypt-Rijndael", + "type": "owner", + "outputs": [ + "perl-Crypt-Rijndael", + "perl-Crypt-Rijndael-debuginfo", + "perl-Crypt-Rijndael-debugsource" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Crypt-SSLeay", + "type": "owner", + "outputs": [ + "perl-Crypt-SSLeay", + "perl-Crypt-SSLeay-debuginfo", + "perl-Crypt-SSLeay-debugsource" + ], + "needs": [ + "perl-Path-Class", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-HTTP-Async", + "type": "owner", + "outputs": [ + "perl-HTTP-Async" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Net-HTTPS-NB", + "type": "owner", + "outputs": [ + "perl-Net-HTTPS-NB" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "perl-Net-Telnet", + "type": "owner", + "outputs": [ + "perl-Net-Telnet" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "ipmitool-xcat", + "type": "owner", + "outputs": [ + "ipmitool-xcat", + "ipmitool-xcat-debuginfo", + "ipmitool-xcat-debugsource" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "syslinux-xcat", + "type": "owner", + "outputs": [ + "syslinux", + "syslinux-debuginfo", + "syslinux-debugsource", + "syslinux-extlinux", + "syslinux-xcat" + ], + "needs": [ + "nasm", + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "grub2-xcat", + "type": "owner", + "outputs": [ + "grub2-xcat" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "xnba-undi", + "type": "owner", + "outputs": [ + "xnba-undi" + ], + "needs": [ + "procenv" + ], + "build_uid": 0 + }, + { + "name": "goconserver", + "type": "owner", + "outputs": [ + "goconserver" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + }, + { + "name": "xCAT-genesis-base", + "type": "owner", + "outputs": [ + "xCAT-genesis-base-ppc64" + ], + "needs": [ + "procenv" + ], + "build_uid": 0 + }, + { + "name": "perl-IO-Stty", + "type": "owner", + "outputs": [ + "perl-IO-Stty" + ], + "needs": [ + "procenv" + ], + "build_uid": 1000 + } + ] +} diff --git a/openeuler/Build-notes b/openeuler/Build-notes new file mode 100644 index 0000000..553c6b9 --- /dev/null +++ b/openeuler/Build-notes @@ -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. diff --git a/openeuler/RPM-GPG-KEY-openEuler-24.03-LTS b/openeuler/RPM-GPG-KEY-openEuler-24.03-LTS new file mode 100644 index 0000000..efabcea --- /dev/null +++ b/openeuler/RPM-GPG-KEY-openEuler-24.03-LTS @@ -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----- diff --git a/packages-manifest.conf b/packages-manifest.conf index cc38cbb..93348b8 100644 --- a/packages-manifest.conf +++ b/packages-manifest.conf @@ -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 (/common), which lives beside the per-EL cells and is # therefore invisible to every [] section above. Without it nothing asserted the published diff --git a/postgresql/Build-notes b/postgresql/Build-notes new file mode 100644 index 0000000..37cf0c1 --- /dev/null +++ b/postgresql/Build-notes @@ -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. diff --git a/postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch b/postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch new file mode 100644 index 0000000..e47062e --- /dev/null +++ b/postgresql/postgresql-15.6-openeuler-llvmjit-buildrequires.patch @@ -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 diff --git a/python-scp/SOURCE.md b/python-scp/SOURCE.md new file mode 100644 index 0000000..92bb48b --- /dev/null +++ b/python-scp/SOURCE.md @@ -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. diff --git a/python-scp/python-scp-0.14.5-1.oe2403.src.rpm b/python-scp/python-scp-0.14.5-1.oe2403.src.rpm new file mode 100644 index 0000000..4876bd1 Binary files /dev/null and b/python-scp/python-scp-0.14.5-1.oe2403.src.rpm differ diff --git a/t/mockbuild-all.t b/t/mockbuild-all.t index 731fb32..c5ea55c 100644 --- a/t/mockbuild-all.t +++ b/t/mockbuild-all.t @@ -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 (+epel--, opensuse-leap--). - 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. diff --git a/t/openeuler.t b/t/openeuler.t new file mode 100644 index 0000000..a2ea7a8 --- /dev/null +++ b/t/openeuler.t @@ -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(); diff --git a/xnba/mockbuild.pl b/xnba/mockbuild.pl index 7c1395a..da99bcd 100755 --- a/xnba/mockbuild.pl +++ b/xnba/mockbuild.pl @@ -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 }