From f6f1395c1bab766d86d8cd604131887dd6979fd2 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:03:29 -0300 Subject: [PATCH] feat(build): add a source-only mode to buildrpms.pl buildrpms.pl already produces a source rpm for every package on every run -- buildall() is createmockconfig -> buildsources -> buildspkgs (mock --buildsrpm) -> buildpkgs (mock --rebuild). Source-only is that sequence without the last step, so it belongs here rather than in a parallel implementation: the spec, the staged sources and the mock root are identical either way, and anything built beside them can drift from what a real build does. --source-only stops after buildspkgs. Two things downstream had to learn about it, and both are about not publishing something untrue: - index_repo no longer re-indexes the binary directory. Running createrepo_c over a directory with no binaries in it would replace working metadata with metadata for an empty repository -- a repo that resolves nothing. The srpm index is still regenerated. - write_repo_metadata_dir emits nothing. The .repo file and buildinfo describe an installable binary repository, which this mode does not produce. --source-only with --merge-core-repos is refused: one builds packages, the other assembles per-arch trees that are already built. buildrpms.pl cannot be loaded by a test -- it runs mkdir, git and read_text at file scope -- so the two routines whose behaviour changed are lifted out with a regex and eval'd into a scratch package with their collaborators stubbed, per the code standard, with BAIL_OUT if the extraction stops matching. The CLI contract is exercised by running the real program. Verified by mutation: removing the index_repo guard reddens 2 of the 7 assertions. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- buildrpms.pl | 40 ++++++++- xCAT-test/unit/buildrpms_source_only.t | 116 +++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 xCAT-test/unit/buildrpms_source_only.t diff --git a/buildrpms.pl b/buildrpms.pl index 72f95ba97..840787552 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -195,12 +195,18 @@ GetOptions( "output-dir=s" => \$opts{output_dir}, "input-core-repos=s{1,}" => \@cli_input_core_repos, "repo-baseurl=s" => \$opts{repo_baseurl}, + "source-only" => \$opts{source_only}, ) or usage(); # --package REPLACES the default set (build exactly what was asked), so # `--package xCAT-genesis-base` builds only genesis-base for the dep pipeline. # The full default set is built on every arch (x86_64 and ppc64le alike), so each # arch produces a complete, self-contained xcat-core repo. +# --source-only produces source rpms and nothing else. It is a build mode, so it has +# nothing to assemble and must not be confused with the deploy-time merge. +usage(message => "--source-only and --merge-core-repos are different modes; pass one") + if $opts{source_only} && $opts{merge_core_repos}; + $opts{packages} = \@cli_packages if @cli_packages; # --native-only: build just the arch-native packages (@NATIVE_PACKAGES). Used on a @@ -560,6 +566,11 @@ sub buildall { createmockconfig($pkg, $target); buildsources($pkg, $target); buildspkgs($pkg, $target); + # --source-only stops here: buildspkgs has produced the src.rpm, and the binary + # rebuild is the only thing buildpkgs does. Everything upstream of this point -- + # the spec, the staged sources, the mock root -- is identical either way, which + # is why source-only belongs here rather than in a parallel script. + return if $opts{source_only}; buildpkgs($pkg, $target); } @@ -700,7 +711,12 @@ sub index_repo { # ships neither). The canonical src.rpm lives in SRPMS/. unlink($_) for glob("$repodir/*.src.rpm"), glob("$repodir/*.log"), glob("$repodir/SRPMS/*.log"); - createrepo_dir($repodir, "--excludes 'SRPMS/*' --excludes '*.src.rpm'"); + # In source-only mode no binaries were built, so re-indexing the binary dir would + # replace good metadata with metadata for an empty repo -- publishing a repo that + # resolves nothing. Leave it exactly as the last binary build left it and index + # only the srpms. + createrepo_dir($repodir, "--excludes 'SRPMS/*' --excludes '*.src.rpm'") + unless $opts{source_only}; createrepo_dir("$repodir/SRPMS") if -d "$repodir/SRPMS"; } @@ -777,6 +793,10 @@ sub write_repo_metadata { sub write_repo_metadata_dir { my ($repodir) = @_; return unless -d $repodir; + # The .repo file and buildinfo describe an installable binary repository. A + # source-only run produced none, so emitting them would advertise packages that + # are not there. + return if $opts{source_only}; # Shipped baseurl points at xcat.org (--repo-baseurl overrides it per family, e.g. the # sles/apt layout); mklocalrepo.sh rewrites baseurl/gpgkey to file:// at deploy time. @@ -1128,6 +1148,24 @@ Default: all host CPUs. Rebuild artifacts even if output files already exist. +=item B<--source-only> + +Build source RPMs and stop. Every step up to and including C runs +normally, so the srpms are the same ones a full build would produce; only the binary +C<--rebuild> is skipped. + + ./buildrpms.pl --target alma+epel-9-x86_64 --source-only + +The srpms land in CtargetE/rpms/SRPMS/> and that index is regenerated. +The binary metadata under CtargetE/rpms/> is deliberately left as the +last binary build wrote it: re-indexing a directory with no binaries in it would +replace working metadata with metadata for an empty repository. For the same reason +no C<.repo> file or buildinfo is emitted, since both describe an installable binary +repo that this mode does not produce. With C<--gpg-sign> the srpms are signed. + +Cannot be combined with C<--merge-core-repos>, which assembles already-built +per-arch binary trees. + =item B<--release>=I Override the auto-generated C release string. xCAT packages diff --git a/xCAT-test/unit/buildrpms_source_only.t b/xCAT-test/unit/buildrpms_source_only.t new file mode 100644 index 000000000..e7a7f42e8 --- /dev/null +++ b/xCAT-test/unit/buildrpms_source_only.t @@ -0,0 +1,116 @@ +#!/usr/bin/env perl +# buildrpms.pl --source-only: build the source rpms and stop. +# +# buildrpms.pl cannot be loaded -- it runs mkdir/git/read_text at file scope and +# expects a working tree -- so the routines whose behaviour changed are lifted out +# with a regex and eval'd into a scratch package with their collaborators stubbed, +# per the repo's code standard. The CLI contract is exercised by running the real +# program. +use strict; +use warnings; + +use Cwd qw(getcwd); +use File::Slurper qw(read_text); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use lib "$FindBin::Bin/../lib"; +use Test::More; + +use XCAT::Test::File qw(repo_path); + +my $builder = repo_path('buildrpms.pl'); +plan skip_all => 'buildrpms.pl not found' unless -r $builder; + +my $source = read_text($builder); + +# ---------------------------------------------------------------- extraction -- +# Lift the two routines that decide what a source-only run publishes. BAIL_OUT +# rather than skip: if the extraction stops matching, this file would silently +# cover nothing. +my %routine; +for my $name (qw(index_repo write_repo_metadata_dir)) { + my ($body) = $source =~ /\n(sub \Q$name\E \{.*?\n\})\n/s; + BAIL_OUT("could not extract $name from buildrpms.pl") unless $body; + $routine{$name} = $body; +} + +our @CREATEREPO; +our @METADATA_WRITTEN; + +{ + package Scratch; + no warnings 'redefine'; + # Collaborators the lifted code calls. Each records instead of acting. + sub createrepo_dir { push @main::CREATEREPO, $_[0]; } +} + +# %opts lives in the scratch package and is set directly. Aliasing it to a hash in +# main and localising that does NOT work: local swaps the container, so the lifted +# code keeps reading the hash the glob pointed at before. +my $harness = join "\n", + 'package Scratch;', + 'use strict; use warnings;', + 'our %opts;', + 'sub say { }', + # write_repo_metadata_dir does real work past the guard; stop it there so the + # test observes the guard and nothing else. + $routine{index_repo}, + ($routine{write_repo_metadata_dir} =~ s/(return if \$opts\{source_only\};).*\n\}\z/$1\n push \@main::METADATA_WRITTEN, \$repodir;\n return 1;\n}/sr), + '1;'; + +eval $harness or BAIL_OUT("could not evaluate the extracted routines: $@"); + +sub run_index { + my (%args) = @_; + %Scratch::opts = (source_only => $args{source_only}); + local @CREATEREPO = (); + my $dir = tempdir(CLEANUP => 1); + mkdir File::Spec->catdir($dir, 'SRPMS'); + Scratch::index_repo($dir); + return [map { $_ eq $dir ? 'BINARY' : 'SRPMS' } @CREATEREPO]; +} + +# ------------------------------------------------------------------ behaviour -- +is_deeply( run_index(source_only => 0), ['BINARY', 'SRPMS'], + 'a normal run indexes the binary repository and the srpms' ); + +is_deeply( run_index(source_only => 1), ['SRPMS'], + 'a source-only run indexes only the srpms' ); + +# The reason for the guard: re-indexing a directory with no binaries in it would +# replace working metadata with metadata for an empty repository. +ok( !grep({ $_ eq 'BINARY' } @{ run_index(source_only => 1) }), + 'a source-only run leaves the binary metadata alone' ); + +{ + %Scratch::opts = (source_only => 1); + local @METADATA_WRITTEN = (); + my $dir = tempdir(CLEANUP => 1); + Scratch::write_repo_metadata_dir($dir); + is_deeply( \@METADATA_WRITTEN, [], + 'a source-only run emits no .repo file for packages it did not build' ); +} +{ + %Scratch::opts = (source_only => 0); + local @METADATA_WRITTEN = (); + my $dir = tempdir(CLEANUP => 1); + Scratch::write_repo_metadata_dir($dir); + is_deeply( \@METADATA_WRITTEN, [$dir], + 'a normal run still emits the repository metadata' ); +} + +# ------------------------------------------------------------------- the CLI -- +# Run the real program. --source-only and --merge-core-repos are different modes: +# one builds, the other assembles trees that are already built. +my $cwd = getcwd(); +chdir repo_path('.') or BAIL_OUT("cannot chdir to the repository root: $!"); +my $out = qx($^X buildrpms.pl --source-only --merge-core-repos 2>&1); +my $rc = $? >> 8; +chdir $cwd; + +isnt( $rc, 0, 'combining --source-only with --merge-core-repos is refused' ); +like( $out, qr/--source-only and --merge-core-repos/, + 'and the refusal names both options' ); + +done_testing();