From 1160214ce3efdcca6bbe14be019e9e7082634810 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:14:06 -0300 Subject: [PATCH] refactor(build): merge the two BuildUtils modules into one The rebase onto master left the repository with two modules named BuildUtils.pm: the shared build helpers at the root, package BuildUtils, and the target architecture parser at build-utils/lib/XCAT/BuildUtils.pm, package XCAT::BuildUtils. buildrpms.pl loaded both, one through `@INC` and one through a path require. A reader cannot tell which module a BuildUtils reference names, and the test sandbox staged the wrong one. Move the shared helpers into build-utils/lib/XCAT/BuildUtils.pm as XCAT::BuildUtils, and export targetarch_from_target beside them. Both builders and the four tests now put build-utils/lib on `@INC` and import from the one module. targetarch_from_target keeps its behaviour: it returns the same architecture as before for suffixed targets, empty and undefined input, mixed case and every architecture token. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- BuildUtils.pm | 466 ------------------ build-utils/lib/XCAT/BuildUtils.pm | 466 +++++++++++++++++- builddebs.pl | 12 +- buildrpms.pl | 13 +- docs/source/developers/guides/code/builds.rst | 2 +- xCAT-test/unit/build_utils.t | 48 +- xCAT-test/unit/builddebs_lock.t | 4 +- xCAT-test/unit/buildrpms_source_only.t | 9 +- xCAT-test/unit/ubuntu_2604_pkglist.t | 10 +- 9 files changed, 511 insertions(+), 519 deletions(-) delete mode 100644 BuildUtils.pm diff --git a/BuildUtils.pm b/BuildUtils.pm deleted file mode 100644 index dcefcebf3..000000000 --- a/BuildUtils.pm +++ /dev/null @@ -1,466 +0,0 @@ -package BuildUtils; -# Reusable, unit-testable helpers shared by the xcat-core build tooling: buildrpms.pl -# (rpm/mock) and builddebs.pl (deb/reprepro). Both derive the same Version-Release from -# the same git state, stage the same xCAT-probe helpers, and shell out the same way, so -# that logic lives here once instead of twice. -# -# Everything here is a pure function of its arguments, or a thin wrapper whose side -# effect is the argument. Nothing reaches for an orchestrator global, so -# xCAT-test/unit/build_utils.t drives every function directly rather than grepping the -# builders for evidence that they call it. -# -# It mirrors xcat-dep's BuildUtils.pm in shape and intent; the two repos ship separate -# copies because neither installs the other's tooling. -use strict; -use warnings; -use Exporter 'import'; -use File::Copy qw(copy move); -use File::Basename qw(basename); -use File::Path qw(make_path remove_tree); -use File::Slurper qw(read_text write_text); -use POSIX qw(strftime); -use Pod::Usage qw(pod2usage); -use feature 'say'; - -our @EXPORT_OK = qw( - source_date_epoch snap_release deb_version - stage_probe_helpers XCAT_PROBE_HELPERS - deb_package_arches dist_arches default_dists - orig_tarball_name upstream_version resolve_dest - pin_control_version rewrite_changelog_header - reprepro_distributions reprepro_options - lock_id_for take_build_lock - sh_quote clean_debian_residue git_revision - backup_file restore_file - sh sh_or_die usage - rewrite_file write_script read_line - buildinfo_text -); - -# Both builders echo the commands they run under --verbose. Set once, after -# option parsing, rather than threaded through every sh() call site. -our $VERBOSE = 0; - -# The xCAT-probe helpers. xcat-probe reuses functions shipped by xCAT; they are COPIED -# rather than symlinked because a symlink does not survive packaging, and rather than -# maintained twice because they would drift. Both builders stage them the same way. -# The stamp both builders write beside a published repository. deploy.sh copies -# the file verbatim and cluster-test.pl parses it, so the field names and their -# order are a contract; each builder passes its own time format and writes to -# its own filename, which are part of that contract too. -sub buildinfo_text { - my (%args) = @_; - my $commit = $args{commit} // 'unknown'; - my $host = $args{host}; - unless (defined $host) { - $host = `hostname 2>/dev/null` || 'unknown'; - chomp $host; - } - return join('', map { "$_\n" } - "VERSION=$args{version}", - "RELEASE=$args{release}", - "BUILD_TIME=" . strftime($args{time_format}, gmtime($args{epoch})), - "BUILD_MACHINE=$host", - "COMMIT_ID=" . substr($commit, 0, 7), - "COMMIT_ID_LONG=$commit"); -} - -# Write a helper script and make it executable. Both builders ship a -# mklocalrepo.sh beside the packages they publish, and builddebs.pl installs the -# genesis postscripts the same way; a script written without the executable bit -# is shipped broken, so the mode is not left to the caller to remember. It -# still varies -- the published repo helper is group-writable, the postscripts -# are not -- so the caller may say, and 0775 is only the default. -sub write_script { - my ($path, $content, $mode) = @_; - $mode = 0775 unless defined $mode; - write_text($path, $content); - chmod $mode, $path or die "Cannot chmod $path: $!\n"; - return; -} - -# The first line of a file, without its newline. Version and Release are -# one-line stamps that both builders read, and each spelled the open, the read -# and the chomp differently -- buildrpms.pl chomped ten lines away from its -# read, which is how a stamp keeps a trailing newline nobody notices until it -# lands in a package name. Returns undef when the file is absent, which is -# what a caller with a fallback wants. -sub read_line { - my ($path) = @_; - return undef unless -f $path; - my ($line) = split /\n/, read_text($path), 2; - return undef unless defined $line && length $line; - return $line; -} - -# Read a file, pass its contents through $transform, write the result back. -# A file that is not there is left alone, which is what every caller wanted. -sub rewrite_file { - my ($path, $transform) = @_; - return 0 unless -f $path; - write_text($path, $transform->(read_text($path))); - return 1; -} - -# Run a shell command, returning its EXIT STATUS. system() yields the raw wait -# status, which is the exit code times 256, so it is shifted here: a caller -# comparing the result against a specific code gets the code it expects, not a -# multiple of it. -sub sh { - my ($cmd) = @_; - say "Running: $cmd" if $VERBOSE; - system($cmd); - return $? >> 8; -} - -# pod2usage reads the POD of the running program, so each builder keeps its own -# help text while sharing the way it is printed and the status it exits with. -# Run a command and stop the build when it fails. The same operation was -# spelled in opposite polarities -- `sh(...) == 0 or die` in builddebs.pl, -# `sh(...) and die` in buildrpms.pl, which also used both -- and the `and die` -# form reads as though the die is what happens next rather than what happens on -# failure. One name, one direction, and the exit code lands in the message. -sub sh_or_die { - my ($cmd, $message) = @_; - my $rc = sh($cmd); - return 0 if $rc == 0; - $message = "FATAL: command failed: $cmd" unless defined $message; - $message =~ s/\n\z//; - die "$message (exit $rc)\n"; -} - -sub usage { - my (%args) = @_; - pod2usage( - -verbose => $args{verbose} // 1, - -exitval => $args{exitval} // 2, - (defined($args{message}) && length($args{message}) - ? (-message => "$args{message}\n") : ()), - ); -} - -use constant XCAT_PROBE_HELPERS => qw( - GlobalDef.pm - NetworkUtils.pm - ServiceNodeUtils.pm -); - -# Packages whose .deb carries a real architecture. Everything else in xcat-core is -# Perl and ships as Architecture: all -- one binary serving every Ubuntu release and -# every arch, which is why this build never needs a per-codename chroot. -my %ARCH_PACKAGES = map { $_ => 1 } qw(xCAT xCATsn xCAT-genesis-scripts); - -# Ubuntu releases predating ppc64el. Kept as data rather than an `if` in the caller so -# the repo-assembly and the package-selection paths cannot disagree about it. -my %NO_PPC64EL = map { $_ => 1 } qw(saucy); - -my @DEB_ARCHES = qw(amd64 ppc64el); - -# The Ubuntu releases the apt repository serves by default. Single source of truth: -# the builder, the repo assembly and the tests all read it here, so they cannot drift. -my @DEFAULT_DISTS = qw(focal jammy noble resolute); - -sub default_dists { return @DEFAULT_DISTS; } - -# sh_quote: single-quote a string for safe use in a shell command. -# clean_debian_residue: remove what dpkg-buildpackage leaves inside a package's -# debian/ directory. -# -# debian/files accumulates one line per artifact and is never truncated by -# `dh_clean -d`, which only removes directories. dpkg-genchanges then reads the -# stale entries on the next build and fstats artifacts that are no longer there: -# dpkg-genchanges: error: cannot fstat file ../perl-xcat__amd64.buildinfo -# so a second build in the same checkout dies as soon as the release string moves. -# The staging directories go for the same reason the old shell builder removed -# them -- they are the previous build's payload, not source. -# -# Call this only after a package's LAST architecture: debian/files carries the -# amd64 artifacts that the ppc64el run's dpkg-genchanges still needs. -# backup_file / restore_file: put a file back exactly as it was. -# -# File::Copy::copy does NOT carry permissions, so a naive backup-and-restore returns -# an executable with its exec bit stripped -- the content compares equal and only -# `git diff` notices the mode change. xCAT/postscripts/{bmcsetup,getipmi} are shipped -# executable and are rewritten during the xCAT build, so this is not hypothetical. -# git_revision: the commit the packages are built from. -# -# This is not cosmetic. perl-xCAT/debian/rules and perl-xCAT.spec both pass it to -# modifyUtils, which substitutes it and the version into xCAT::Version. Hand -# modifyUtils an empty string and it does nothing, and the built package reports no -# version at all -- `lsxcatd -v` prints a bare "Version". So a revision is always -# produced: the git checkout when there is one, an existing Gitinfo when there is -# not (a source export carries the real revision that way, and clobbering it with -# a placeholder would throw away the only provenance the tree has), and only then -# the "unknown" placeholder. -sub git_revision { - my (%args) = @_; - my $run = $args{git} || sub { `git rev-parse HEAD 2>/dev/null` }; - my $read_file = $args{read_file} || sub { - return unless -f 'Gitinfo'; - return scalar read_text('Gitinfo'); - }; - - for my $source ($run, $read_file) { - my $rev = $source->(); - next unless defined $rev; - $rev =~ s/\s+\z//; - return $rev if length $rev; - } - return 'unknown'; -} - -sub backup_file { - my ($path) = @_; - return unless defined $path && -f $path; - my $backup = "$path.build.save"; - my $mode = ( stat $path )[2] & 07777; - copy( $path, $backup ) or die "Cannot back up $path: $!\n"; - return [ $backup, $path, $mode ]; -} - -sub restore_file { - my ($entry) = @_; - return 0 unless $entry; - my ( $backup, $path, $mode ) = @{$entry}; - move( $backup, $path ) or do { warn "Could not restore $path: $!\n"; return 0; }; - chmod $mode, $path if defined $mode; - return 1; -} - -sub clean_debian_residue { - my ($package_root) = @_; - return () unless defined $package_root && -d "$package_root/debian"; - - my @removed; - my $files = "$package_root/debian/files"; - if (-e $files) { - unlink $files or die "Cannot remove $files: $!\n"; - push @removed, $files; - } - - my $stem = lc(basename($package_root)); - foreach my $dir (glob("$package_root/debian/$stem*")) { - next unless -d $dir; - remove_tree($dir); - push @removed, $dir; - } - - # debhelper's own bookkeeping. Never tracked, and it accumulates per build. - # glob returns a wildcard-free pattern verbatim whether or not it exists, so - # the -e guard is what makes a second call a no-op rather than a fatal unlink. - foreach my $residue (glob("$package_root/debian/*.debhelper.log"), - "$package_root/debian/.debhelper") { - next unless -e $residue; - if (-d $residue) { remove_tree($residue); } - else { unlink $residue or die "Cannot remove $residue: $!\n"; } - push @removed, $residue; - } - - return @removed; -} - -sub sh_quote { - my ($s) = @_; - $s = '' if !defined $s; - $s =~ s/'/'"'"'/g; - return "'$s'"; -} - -# source_date_epoch: the commit time the build is reproducible against. -# -# Gitepoch wins when present -- CI writes it so every arch of one release stamps an -# identical epoch even when the arches build minutes apart. Falling back to the local -# clock is last-resort: it makes the build non-reproducible, so the caller is told. -sub source_date_epoch { - my (%args) = @_; - my $read = $args{read_file} || sub { - my ($p) = @_; - return unless -f $p; - return scalar read_text($p); - }; - my $git = $args{git_epoch} || sub { return scalar `git log -1 --format=%ct HEAD 2>/dev/null`; }; - - for my $candidate ($read->('Gitepoch'), $git->()) { - next unless defined $candidate; - chomp $candidate; - return $candidate if $candidate =~ /\A\d+\z/; - } - return $args{now} || time(); -} - -# snap_release: the Release string, derived from the commit time so identical sources -# give identical NVRs. UTC, because a build host's timezone must not change the name. -sub snap_release { - my ($epoch) = @_; - return strftime("snap%Y%m%d%H%M", gmtime($epoch)); -} - -# deb_version: the Debian version. Same Version-Release pair the rpms carry, so an -# apt repo and a yum repo built from one commit report the same thing. -sub deb_version { - my ($version, $release) = @_; - return "$version-$release"; -} - -# stage_probe_helpers: copy the shared helpers into xCAT-probe's tree. -# Returns the list of destination paths, so a caller can remove exactly what it added. -sub stage_probe_helpers { - my ($source_dir, $dest_dir) = @_; - make_path($dest_dir) unless -d $dest_dir; - my @staged; - for my $helper (XCAT_PROBE_HELPERS) { - my $from = "$source_dir/$helper"; - my $to = "$dest_dir/$helper"; - copy($from, $to) or die "Unable to stage $from into $dest_dir: $!\n"; - push @staged, $to; - } - return @staged; -} - -# deb_package_arches: the architectures to build a package for. -# 'all' is a single arch-independent build; the three arch packages get one per arch. -sub deb_package_arches { - my ($package) = @_; - return @DEB_ARCHES if $ARCH_PACKAGES{$package // ''}; - return ('all'); -} - -# dist_arches: the architectures a release's apt repo declares. -sub dist_arches { - my ($dist) = @_; - return ('amd64') if $NO_PPC64EL{$dist // ''}; - return @DEB_ARCHES; -} - -# orig_tarball_name: the .orig.tar.gz dpkg-source expects for a 3.0 (quilt) package. -# -# The name carries the UPSTREAM version only -- dpkg looks for -# _.orig.tar.gz, with no Debian revision, because one upstream -# tarball is shared by every revision built from it. The revision is stripped here -# rather than at the call site so passing the full Version-Release cannot produce a -# tarball dpkg will not find. Lower-cased because dpkg requires a lower-case source -# package name. -sub upstream_version { - my ($version) = @_; - return '' unless defined $version; - $version =~ s/-[^-]*\z//; # drop the Debian revision, if any - return $version; -} - -sub orig_tarball_name { - my ($package, $version) = @_; - return lc($package) . '_' . upstream_version($version) . '.orig.tar.gz'; -} - -# resolve_dest: turn a --dest argument into an absolute path. -# -# NOT Cwd::abs_path: that returns undef when a PARENT component is missing, and the -# caller then interpolates undef, so `--dest /no/such/parent/out` silently becomes -# `/debs` and `/xcat-core` at the filesystem root. rel2abs is purely lexical and -# works for a path that does not exist yet, which is the normal case for an output -# directory. -sub resolve_dest { - my ($dest, $default) = @_; - return $default unless defined $dest && length $dest; - require File::Spec; - return File::Spec->rel2abs($dest); -} - -# pin_control_version: pin xCAT's inter-package dependencies to this exact build. -# -# debian/control carries the sentinel ">= 2.13-snap000000000000" on every intra-xCAT -# dependency. Left alone, apt would satisfy them with any older xCAT already installed, -# so a partial upgrade could mix versions. Replacing it with "= " makes the set -# install or fail as a unit. -sub pin_control_version { - my ($control, $version) = @_; - return $control unless defined $control; - $control =~ s/>= \Q2.13-snap000000000000\E/= $version/g; - return $control; -} - -# rewrite_changelog_header: set the version and the trailer date of the top stanza. -# -# The date comes from SOURCE_DATE_EPOCH rather than "now" so two builds of one commit -# produce byte-identical packages. Only the first stanza is touched -- the history below -# it is not ours to rewrite. -sub rewrite_changelog_header { - my ($changelog, $version, $date, $maintainer) = @_; - return $changelog unless defined $changelog; - $changelog =~ s/\A(\S+) \([^)]*\)/$1 ($version)/; - $changelog =~ s/^ -- .*$/ -- $maintainer $date/m; - return $changelog; -} - -# reprepro_distributions: the conf/distributions body for the whole repo. -# -# One stanza per release, all listing the same packages: xcat-core debs are Perl and are -# byte-identical across releases, so the build produces them once and every codename -# serves the same files. keyid is undef for an unsigned repo. -sub reprepro_distributions { - my ($dists, $keyid) = @_; - my $out = ''; - for my $dist (@$dists) { - my $arches = join ' ', dist_arches($dist); - $out .= <<"STANZA"; -Origin: xCAT internal repository -Label: xcat-core bazaar repository -Codename: $dist -Architectures: $arches -Components: main -Description: Repository automatically genereted conf -STANZA - $out .= "SignWith: $keyid\n" if defined $keyid && length $keyid; - $out .= "\n"; - } - return $out; -} - -# reprepro_options: the conf/options body. -# -# ask-passphrase is omitted when a GNUPGHOME is supplied, because that key is -# passphrase-less and an unattended build must never stop to prompt. -sub reprepro_options { - my ($gpg_home) = @_; - my $out = "verbose\n"; - $out .= "ask-passphrase\n" unless defined $gpg_home && length $gpg_home; - $out .= "basedir .\n"; - return $out; -} - - -# lock_id_for: a short, stable id for a checkout path. -# -# The build rewrites debian/changelog and debian/control and runs dpkg-buildpackage -# inside the package directories, so what two builds contend for is the CHECKOUT, not -# the host. A host-global lock made the devel and stable CD lanes collide even though -# they share nothing. Keying on the path lets distinct checkouts build in parallel while -# two builds of one checkout still fail fast. -sub lock_id_for { - my ($path) = @_; - require Digest::MD5; - return substr(Digest::MD5::md5_hex(defined $path ? $path : ''), 0, 12); -} - -# lock_path_for: where that checkout's lock lives. -# Local /var/lock deliberately: the checkout itself may be on NFS, where flock is not -# reliable. -sub lock_path_for { - my ($path, $dir) = @_; - $dir = '/var/lock' unless defined $dir; - return "$dir/xcatbld-" . lock_id_for($path) . ".lock"; -} - -# take_build_lock: take the checkout's lock, or die. -# Returns the open handle -- the lock is held for as long as the caller keeps it. -sub take_build_lock { - my ($path, $dir) = @_; - require Fcntl; - my $lockfile = lock_path_for($path, $dir); - open my $fh, '>', $lockfile or die "FATAL: cannot open $lockfile: $!\n"; - flock($fh, Fcntl::LOCK_EX() | Fcntl::LOCK_NB()) - or die "FATAL: another build of $path already holds $lockfile\n"; - return $fh; -} - -1; diff --git a/build-utils/lib/XCAT/BuildUtils.pm b/build-utils/lib/XCAT/BuildUtils.pm index 60ecf0877..86bedd846 100644 --- a/build-utils/lib/XCAT/BuildUtils.pm +++ b/build-utils/lib/XCAT/BuildUtils.pm @@ -1,12 +1,472 @@ package XCAT::BuildUtils; - +# Reusable, unit-testable helpers shared by the xcat-core build tooling: buildrpms.pl +# (rpm/mock) and builddebs.pl (deb/reprepro). Both derive the same Version-Release from +# the same git state, stage the same xCAT-probe helpers, and shell out the same way, so +# that logic lives here once instead of twice. +# +# Everything here is a pure function of its arguments, or a thin wrapper whose side +# effect is the argument. Nothing reaches for an orchestrator global, so +# xCAT-test/unit/build_utils.t drives every function directly rather than grepping the +# builders for evidence that they call it. +# +# It mirrors xcat-dep's BuildUtils.pm in shape and intent; the two repos ship separate +# copies because neither installs the other's tooling. use strict; use warnings; +use Exporter 'import'; +use File::Copy qw(copy move); +use File::Basename qw(basename); +use File::Path qw(make_path remove_tree); +use File::Slurper qw(read_text write_text); +use POSIX qw(strftime); +use Pod::Usage qw(pod2usage); +use feature 'say'; -use Exporter qw(import); +our @EXPORT_OK = qw( + source_date_epoch snap_release deb_version + stage_probe_helpers XCAT_PROBE_HELPERS + deb_package_arches dist_arches default_dists + orig_tarball_name upstream_version resolve_dest + pin_control_version rewrite_changelog_header + reprepro_distributions reprepro_options + lock_id_for take_build_lock + sh_quote clean_debian_residue git_revision + backup_file restore_file + sh sh_or_die usage + rewrite_file write_script read_line + buildinfo_text + targetarch_from_target +); -our @EXPORT_OK = qw(targetarch_from_target); +# Both builders echo the commands they run under --verbose. Set once, after +# option parsing, rather than threaded through every sh() call site. +our $VERBOSE = 0; +# The xCAT-probe helpers. xcat-probe reuses functions shipped by xCAT; they are COPIED +# rather than symlinked because a symlink does not survive packaging, and rather than +# maintained twice because they would drift. Both builders stage them the same way. +# The stamp both builders write beside a published repository. deploy.sh copies +# the file verbatim and cluster-test.pl parses it, so the field names and their +# order are a contract; each builder passes its own time format and writes to +# its own filename, which are part of that contract too. +sub buildinfo_text { + my (%args) = @_; + my $commit = $args{commit} // 'unknown'; + my $host = $args{host}; + unless (defined $host) { + $host = `hostname 2>/dev/null` || 'unknown'; + chomp $host; + } + return join('', map { "$_\n" } + "VERSION=$args{version}", + "RELEASE=$args{release}", + "BUILD_TIME=" . strftime($args{time_format}, gmtime($args{epoch})), + "BUILD_MACHINE=$host", + "COMMIT_ID=" . substr($commit, 0, 7), + "COMMIT_ID_LONG=$commit"); +} + +# Write a helper script and make it executable. Both builders ship a +# mklocalrepo.sh beside the packages they publish, and builddebs.pl installs the +# genesis postscripts the same way; a script written without the executable bit +# is shipped broken, so the mode is not left to the caller to remember. It +# still varies -- the published repo helper is group-writable, the postscripts +# are not -- so the caller may say, and 0775 is only the default. +sub write_script { + my ($path, $content, $mode) = @_; + $mode = 0775 unless defined $mode; + write_text($path, $content); + chmod $mode, $path or die "Cannot chmod $path: $!\n"; + return; +} + +# The first line of a file, without its newline. Version and Release are +# one-line stamps that both builders read, and each spelled the open, the read +# and the chomp differently -- buildrpms.pl chomped ten lines away from its +# read, which is how a stamp keeps a trailing newline nobody notices until it +# lands in a package name. Returns undef when the file is absent, which is +# what a caller with a fallback wants. +sub read_line { + my ($path) = @_; + return undef unless -f $path; + my ($line) = split /\n/, read_text($path), 2; + return undef unless defined $line && length $line; + return $line; +} + +# Read a file, pass its contents through $transform, write the result back. +# A file that is not there is left alone, which is what every caller wanted. +sub rewrite_file { + my ($path, $transform) = @_; + return 0 unless -f $path; + write_text($path, $transform->(read_text($path))); + return 1; +} + +# Run a shell command, returning its EXIT STATUS. system() yields the raw wait +# status, which is the exit code times 256, so it is shifted here: a caller +# comparing the result against a specific code gets the code it expects, not a +# multiple of it. +sub sh { + my ($cmd) = @_; + say "Running: $cmd" if $VERBOSE; + system($cmd); + return $? >> 8; +} + +# pod2usage reads the POD of the running program, so each builder keeps its own +# help text while sharing the way it is printed and the status it exits with. +# Run a command and stop the build when it fails. The same operation was +# spelled in opposite polarities -- `sh(...) == 0 or die` in builddebs.pl, +# `sh(...) and die` in buildrpms.pl, which also used both -- and the `and die` +# form reads as though the die is what happens next rather than what happens on +# failure. One name, one direction, and the exit code lands in the message. +sub sh_or_die { + my ($cmd, $message) = @_; + my $rc = sh($cmd); + return 0 if $rc == 0; + $message = "FATAL: command failed: $cmd" unless defined $message; + $message =~ s/\n\z//; + die "$message (exit $rc)\n"; +} + +sub usage { + my (%args) = @_; + pod2usage( + -verbose => $args{verbose} // 1, + -exitval => $args{exitval} // 2, + (defined($args{message}) && length($args{message}) + ? (-message => "$args{message}\n") : ()), + ); +} + +use constant XCAT_PROBE_HELPERS => qw( + GlobalDef.pm + NetworkUtils.pm + ServiceNodeUtils.pm +); + +# Packages whose .deb carries a real architecture. Everything else in xcat-core is +# Perl and ships as Architecture: all -- one binary serving every Ubuntu release and +# every arch, which is why this build never needs a per-codename chroot. +my %ARCH_PACKAGES = map { $_ => 1 } qw(xCAT xCATsn xCAT-genesis-scripts); + +# Ubuntu releases predating ppc64el. Kept as data rather than an `if` in the caller so +# the repo-assembly and the package-selection paths cannot disagree about it. +my %NO_PPC64EL = map { $_ => 1 } qw(saucy); + +my @DEB_ARCHES = qw(amd64 ppc64el); + +# The Ubuntu releases the apt repository serves by default. Single source of truth: +# the builder, the repo assembly and the tests all read it here, so they cannot drift. +my @DEFAULT_DISTS = qw(focal jammy noble resolute); + +sub default_dists { return @DEFAULT_DISTS; } + +# sh_quote: single-quote a string for safe use in a shell command. +# clean_debian_residue: remove what dpkg-buildpackage leaves inside a package's +# debian/ directory. +# +# debian/files accumulates one line per artifact and is never truncated by +# `dh_clean -d`, which only removes directories. dpkg-genchanges then reads the +# stale entries on the next build and fstats artifacts that are no longer there: +# dpkg-genchanges: error: cannot fstat file ../perl-xcat__amd64.buildinfo +# so a second build in the same checkout dies as soon as the release string moves. +# The staging directories go for the same reason the old shell builder removed +# them -- they are the previous build's payload, not source. +# +# Call this only after a package's LAST architecture: debian/files carries the +# amd64 artifacts that the ppc64el run's dpkg-genchanges still needs. +# backup_file / restore_file: put a file back exactly as it was. +# +# File::Copy::copy does NOT carry permissions, so a naive backup-and-restore returns +# an executable with its exec bit stripped -- the content compares equal and only +# `git diff` notices the mode change. xCAT/postscripts/{bmcsetup,getipmi} are shipped +# executable and are rewritten during the xCAT build, so this is not hypothetical. +# git_revision: the commit the packages are built from. +# +# This is not cosmetic. perl-xCAT/debian/rules and perl-xCAT.spec both pass it to +# modifyUtils, which substitutes it and the version into xCAT::Version. Hand +# modifyUtils an empty string and it does nothing, and the built package reports no +# version at all -- `lsxcatd -v` prints a bare "Version". So a revision is always +# produced: the git checkout when there is one, an existing Gitinfo when there is +# not (a source export carries the real revision that way, and clobbering it with +# a placeholder would throw away the only provenance the tree has), and only then +# the "unknown" placeholder. +sub git_revision { + my (%args) = @_; + my $run = $args{git} || sub { `git rev-parse HEAD 2>/dev/null` }; + my $read_file = $args{read_file} || sub { + return unless -f 'Gitinfo'; + return scalar read_text('Gitinfo'); + }; + + for my $source ($run, $read_file) { + my $rev = $source->(); + next unless defined $rev; + $rev =~ s/\s+\z//; + return $rev if length $rev; + } + return 'unknown'; +} + +sub backup_file { + my ($path) = @_; + return unless defined $path && -f $path; + my $backup = "$path.build.save"; + my $mode = ( stat $path )[2] & 07777; + copy( $path, $backup ) or die "Cannot back up $path: $!\n"; + return [ $backup, $path, $mode ]; +} + +sub restore_file { + my ($entry) = @_; + return 0 unless $entry; + my ( $backup, $path, $mode ) = @{$entry}; + move( $backup, $path ) or do { warn "Could not restore $path: $!\n"; return 0; }; + chmod $mode, $path if defined $mode; + return 1; +} + +sub clean_debian_residue { + my ($package_root) = @_; + return () unless defined $package_root && -d "$package_root/debian"; + + my @removed; + my $files = "$package_root/debian/files"; + if (-e $files) { + unlink $files or die "Cannot remove $files: $!\n"; + push @removed, $files; + } + + my $stem = lc(basename($package_root)); + foreach my $dir (glob("$package_root/debian/$stem*")) { + next unless -d $dir; + remove_tree($dir); + push @removed, $dir; + } + + # debhelper's own bookkeeping. Never tracked, and it accumulates per build. + # glob returns a wildcard-free pattern verbatim whether or not it exists, so + # the -e guard is what makes a second call a no-op rather than a fatal unlink. + foreach my $residue (glob("$package_root/debian/*.debhelper.log"), + "$package_root/debian/.debhelper") { + next unless -e $residue; + if (-d $residue) { remove_tree($residue); } + else { unlink $residue or die "Cannot remove $residue: $!\n"; } + push @removed, $residue; + } + + return @removed; +} + +sub sh_quote { + my ($s) = @_; + $s = '' if !defined $s; + $s =~ s/'/'"'"'/g; + return "'$s'"; +} + +# source_date_epoch: the commit time the build is reproducible against. +# +# Gitepoch wins when present -- CI writes it so every arch of one release stamps an +# identical epoch even when the arches build minutes apart. Falling back to the local +# clock is last-resort: it makes the build non-reproducible, so the caller is told. +sub source_date_epoch { + my (%args) = @_; + my $read = $args{read_file} || sub { + my ($p) = @_; + return unless -f $p; + return scalar read_text($p); + }; + my $git = $args{git_epoch} || sub { return scalar `git log -1 --format=%ct HEAD 2>/dev/null`; }; + + for my $candidate ($read->('Gitepoch'), $git->()) { + next unless defined $candidate; + chomp $candidate; + return $candidate if $candidate =~ /\A\d+\z/; + } + return $args{now} || time(); +} + +# snap_release: the Release string, derived from the commit time so identical sources +# give identical NVRs. UTC, because a build host's timezone must not change the name. +sub snap_release { + my ($epoch) = @_; + return strftime("snap%Y%m%d%H%M", gmtime($epoch)); +} + +# deb_version: the Debian version. Same Version-Release pair the rpms carry, so an +# apt repo and a yum repo built from one commit report the same thing. +sub deb_version { + my ($version, $release) = @_; + return "$version-$release"; +} + +# stage_probe_helpers: copy the shared helpers into xCAT-probe's tree. +# Returns the list of destination paths, so a caller can remove exactly what it added. +sub stage_probe_helpers { + my ($source_dir, $dest_dir) = @_; + make_path($dest_dir) unless -d $dest_dir; + my @staged; + for my $helper (XCAT_PROBE_HELPERS) { + my $from = "$source_dir/$helper"; + my $to = "$dest_dir/$helper"; + copy($from, $to) or die "Unable to stage $from into $dest_dir: $!\n"; + push @staged, $to; + } + return @staged; +} + +# deb_package_arches: the architectures to build a package for. +# 'all' is a single arch-independent build; the three arch packages get one per arch. +sub deb_package_arches { + my ($package) = @_; + return @DEB_ARCHES if $ARCH_PACKAGES{$package // ''}; + return ('all'); +} + +# dist_arches: the architectures a release's apt repo declares. +sub dist_arches { + my ($dist) = @_; + return ('amd64') if $NO_PPC64EL{$dist // ''}; + return @DEB_ARCHES; +} + +# orig_tarball_name: the .orig.tar.gz dpkg-source expects for a 3.0 (quilt) package. +# +# The name carries the UPSTREAM version only -- dpkg looks for +# _.orig.tar.gz, with no Debian revision, because one upstream +# tarball is shared by every revision built from it. The revision is stripped here +# rather than at the call site so passing the full Version-Release cannot produce a +# tarball dpkg will not find. Lower-cased because dpkg requires a lower-case source +# package name. +sub upstream_version { + my ($version) = @_; + return '' unless defined $version; + $version =~ s/-[^-]*\z//; # drop the Debian revision, if any + return $version; +} + +sub orig_tarball_name { + my ($package, $version) = @_; + return lc($package) . '_' . upstream_version($version) . '.orig.tar.gz'; +} + +# resolve_dest: turn a --dest argument into an absolute path. +# +# NOT Cwd::abs_path: that returns undef when a PARENT component is missing, and the +# caller then interpolates undef, so `--dest /no/such/parent/out` silently becomes +# `/debs` and `/xcat-core` at the filesystem root. rel2abs is purely lexical and +# works for a path that does not exist yet, which is the normal case for an output +# directory. +sub resolve_dest { + my ($dest, $default) = @_; + return $default unless defined $dest && length $dest; + require File::Spec; + return File::Spec->rel2abs($dest); +} + +# pin_control_version: pin xCAT's inter-package dependencies to this exact build. +# +# debian/control carries the sentinel ">= 2.13-snap000000000000" on every intra-xCAT +# dependency. Left alone, apt would satisfy them with any older xCAT already installed, +# so a partial upgrade could mix versions. Replacing it with "= " makes the set +# install or fail as a unit. +sub pin_control_version { + my ($control, $version) = @_; + return $control unless defined $control; + $control =~ s/>= \Q2.13-snap000000000000\E/= $version/g; + return $control; +} + +# rewrite_changelog_header: set the version and the trailer date of the top stanza. +# +# The date comes from SOURCE_DATE_EPOCH rather than "now" so two builds of one commit +# produce byte-identical packages. Only the first stanza is touched -- the history below +# it is not ours to rewrite. +sub rewrite_changelog_header { + my ($changelog, $version, $date, $maintainer) = @_; + return $changelog unless defined $changelog; + $changelog =~ s/\A(\S+) \([^)]*\)/$1 ($version)/; + $changelog =~ s/^ -- .*$/ -- $maintainer $date/m; + return $changelog; +} + +# reprepro_distributions: the conf/distributions body for the whole repo. +# +# One stanza per release, all listing the same packages: xcat-core debs are Perl and are +# byte-identical across releases, so the build produces them once and every codename +# serves the same files. keyid is undef for an unsigned repo. +sub reprepro_distributions { + my ($dists, $keyid) = @_; + my $out = ''; + for my $dist (@$dists) { + my $arches = join ' ', dist_arches($dist); + $out .= <<"STANZA"; +Origin: xCAT internal repository +Label: xcat-core bazaar repository +Codename: $dist +Architectures: $arches +Components: main +Description: Repository automatically genereted conf +STANZA + $out .= "SignWith: $keyid\n" if defined $keyid && length $keyid; + $out .= "\n"; + } + return $out; +} + +# reprepro_options: the conf/options body. +# +# ask-passphrase is omitted when a GNUPGHOME is supplied, because that key is +# passphrase-less and an unattended build must never stop to prompt. +sub reprepro_options { + my ($gpg_home) = @_; + my $out = "verbose\n"; + $out .= "ask-passphrase\n" unless defined $gpg_home && length $gpg_home; + $out .= "basedir .\n"; + return $out; +} + + +# lock_id_for: a short, stable id for a checkout path. +# +# The build rewrites debian/changelog and debian/control and runs dpkg-buildpackage +# inside the package directories, so what two builds contend for is the CHECKOUT, not +# the host. A host-global lock made the devel and stable CD lanes collide even though +# they share nothing. Keying on the path lets distinct checkouts build in parallel while +# two builds of one checkout still fail fast. +sub lock_id_for { + my ($path) = @_; + require Digest::MD5; + return substr(Digest::MD5::md5_hex(defined $path ? $path : ''), 0, 12); +} + +# lock_path_for: where that checkout's lock lives. +# Local /var/lock deliberately: the checkout itself may be on NFS, where flock is not +# reliable. +sub lock_path_for { + my ($path, $dir) = @_; + $dir = '/var/lock' unless defined $dir; + return "$dir/xcatbld-" . lock_id_for($path) . ".lock"; +} + +# take_build_lock: take the checkout's lock, or die. +# Returns the open handle -- the lock is held for as long as the caller keeps it. +sub take_build_lock { + my ($path, $dir) = @_; + require Fcntl; + my $lockfile = lock_path_for($path, $dir); + open my $fh, '>', $lockfile or die "FATAL: cannot open $lockfile: $!\n"; + flock($fh, Fcntl::LOCK_EX() | Fcntl::LOCK_NB()) + or die "FATAL: another build of $path already holds $lockfile\n"; + return $fh; +} + +# The rpm architecture a mock target builds for. A target carries the arch as its +# last meaningful token (alma+epel-10-ppc64le), and a suffixed target keeps it in +# the middle (rocky-10-riscv64-xcat), so the token is found from the right. sub targetarch_from_target { my ( $target, $default_arch ) = @_; return $default_arch unless defined($target) && length($target); diff --git a/builddebs.pl b/builddebs.pl index 60de1dafd..7e148223e 100755 --- a/builddebs.pl +++ b/builddebs.pl @@ -3,7 +3,7 @@ # # Replaces build-ubunturepo. The shape mirrors buildrpms.pl -- Getopt::Long options, # one package list, build then index then sign -- so the two builders read the same way -# and share BuildUtils.pm. +# and share XCAT::BuildUtils. # # The central fact this design rests on: xcat-core debs are Perl. They are byte-identical # for every Ubuntu release, so they are built ONCE and the same files are published into @@ -27,8 +27,8 @@ use POSIX qw(strftime); use Pod::Usage qw(pod2usage); use FindBin; -use lib $FindBin::Bin; -use BuildUtils qw( +use lib "$FindBin::Bin/build-utils/lib"; +use XCAT::BuildUtils qw( source_date_epoch snap_release deb_version stage_probe_helpers XCAT_PROBE_HELPERS deb_package_arches dist_arches default_dists @@ -57,7 +57,7 @@ my @PACKAGES = qw( ); # Releases the repo serves. The same debs are published into each; the list itself -# lives in BuildUtils so the builder and the tests cannot disagree about it. +# lives in XCAT::BuildUtils so the builder and the tests cannot disagree about it. my @DISTS = default_dists(); my %opts; @@ -77,7 +77,7 @@ GetOptions( ) or usage(); usage(exitval => 0, verbose => 2) if $opts{help}; -$BuildUtils::VERBOSE = $opts{verbose}; +$XCAT::BuildUtils::VERBOSE = $opts{verbose}; $opts{packages} = @cli_packages ? \@cli_packages : \@PACKAGES; $opts{dists} = @cli_dists ? \@cli_dists : \@DISTS; @@ -126,7 +126,7 @@ $ENV{DEBEMAIL} = 'xcat-build@xcat.org'; my $MAINTAINER = "$ENV{DEBFULLNAME} <$ENV{DEBEMAIL}>"; my $DEB_DATE = strftime('%a, %d %b %Y %H:%M:%S +0000', gmtime($EPOCH)); -# The build lock is scoped to this checkout, not the host -- see BuildUtils::lock_id_for. +# The build lock is scoped to this checkout, not the host -- see XCAT::BuildUtils::lock_id_for. # ------------------------------------------------------------------ staging -- # diff --git a/buildrpms.pl b/buildrpms.pl index f5fe76993..70c1c3e8b 100755 --- a/buildrpms.pl +++ b/buildrpms.pl @@ -42,9 +42,9 @@ use File::Path qw(make_path remove_tree); use File::Slurper qw(read_text write_text); use File::Temp qw(tempdir tempfile); use FindBin qw($Bin); -use lib $Bin; -use BuildUtils qw(git_revision source_date_epoch sh sh_or_die usage buildinfo_text - write_script read_line); +use lib "$Bin/build-utils/lib"; +use XCAT::BuildUtils qw(git_revision source_date_epoch sh sh_or_die usage buildinfo_text + write_script read_line targetarch_from_target); use Fcntl qw(:flock); # per-target build lock (concurrency guard; see main()) use Getopt::Long qw(GetOptions); use POSIX qw(strftime); @@ -54,7 +54,6 @@ use Pod::Usage qw(pod2usage); use autodie; use autodie qw(cp); -require "$Bin/build-utils/lib/XCAT/BuildUtils.pm"; my $SOURCES = "$ENV{HOME}/rpmbuild/SOURCES"; # Ensure the rpmbuild tree exists. buildrpms stages source tarballs into $SOURCES, but it only @@ -189,7 +188,7 @@ GetOptions( "source-only" => \$opts{source_only}, ) or usage(); -$BuildUtils::VERBOSE = $opts{verbose}; +$XCAT::BuildUtils::VERBOSE = $opts{verbose}; # --package REPLACES the default set (build exactly what was asked), so # `--package xCAT-genesis-base` builds only genesis-base for the dep pipeline. @@ -435,7 +434,7 @@ sub buildspkgs { my $ext = $opts{mock_uniqueext} ? "-$opts{mock_uniqueext}" : ""; my $chroot = "$pkg-$target$ext"; my $targetarch = - XCAT::BuildUtils::targetarch_from_target( $target, $ARCH ); + targetarch_from_target( $target, $ARCH ); my $genesis_tarch = genesis_tarch_from_targetarch($targetarch); my $diskcache = ( @@ -492,7 +491,7 @@ sub buildpkgs { # get x86_64 from alma+epel-9-x86_64 my $targetarch = - XCAT::BuildUtils::targetarch_from_target( $target, $ARCH ); + targetarch_from_target( $target, $ARCH ); # xCAT genesis packages include the translated target arch in their file names. my $arch = is_in($pkg, @NATIVE_PACKAGES) ? $targetarch : "noarch"; diff --git a/docs/source/developers/guides/code/builds.rst b/docs/source/developers/guides/code/builds.rst index 07ecfd75b..607075ce5 100644 --- a/docs/source/developers/guides/code/builds.rst +++ b/docs/source/developers/guides/code/builds.rst @@ -62,7 +62,7 @@ compiled output. That is why this build needs no ``sbuild`` and no per-codename chroot -- unlike xcat-deps, whose packages are compiled and genuinely differ per release. -Helpers shared by both builders live in ``BuildUtils.pm``. +Helpers shared by both builders live in ``build-utils/lib/XCAT/BuildUtils.pm``. ``buildcore.sh`` builds the architecture specific packages (``xCAT``, ``xCATsn``, ``xCAT-genesis-scripts``) for every supported architecture, riscv64 included, with diff --git a/xCAT-test/unit/build_utils.t b/xCAT-test/unit/build_utils.t index 1971b39ad..8a44fbe39 100644 --- a/xCAT-test/unit/build_utils.t +++ b/xCAT-test/unit/build_utils.t @@ -1,5 +1,5 @@ #!/usr/bin/env perl -# BuildUtils.pm: the helpers buildrpms.pl and builddebs.pl share. +# XCAT::BuildUtils: the helpers buildrpms.pl and builddebs.pl share. # # Every function here is pure, so every assertion below RUNS it. Nothing in this file # reads the builders' source to check that they call it -- that would pass with the @@ -13,12 +13,12 @@ use File::Spec; use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; -use lib "$FindBin::Bin/../.."; +use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; -BEGIN { use_ok('BuildUtils') or BAIL_OUT('BuildUtils.pm does not load'); } +BEGIN { use_ok('XCAT::BuildUtils') or BAIL_OUT('XCAT::BuildUtils does not load'); } -use BuildUtils qw( +use XCAT::BuildUtils qw( source_date_epoch snap_release deb_version stage_probe_helpers XCAT_PROBE_HELPERS deb_package_arches dist_arches @@ -312,7 +312,7 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', # deploy.sh copies this file verbatim and cluster-test.pl parses it, so the # field names and their order are a contract, not a presentation choice. { - my $text = BuildUtils::buildinfo_text( + my $text = XCAT::BuildUtils::buildinfo_text( version => '2.18.1', release => 'snap1', epoch => 0, commit => 'abcdef1234567890', host => 'builder', time_format => '%Y-%m-%d', @@ -328,8 +328,8 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', # The two builders stamp different formats, and both are consumed. my %common = (version => '1', release => '2', epoch => 0, commit => 'c', host => 'h'); - isnt( BuildUtils::buildinfo_text(%common, time_format => '%a %b %d %H:%M:%S %Y'), - BuildUtils::buildinfo_text(%common, time_format => '%a %b %e %H:%M:%S %Z %Y'), + isnt( XCAT::BuildUtils::buildinfo_text(%common, time_format => '%a %b %d %H:%M:%S %Y'), + XCAT::BuildUtils::buildinfo_text(%common, time_format => '%a %b %e %H:%M:%S %Z %Y'), 'each builder keeps the format its own consumers parse' ); } @@ -341,12 +341,12 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', my $path = File::Spec->catfile($dir, 'thing.txt'); write_text($path, "one\ntwo\n"); - my $changed = BuildUtils::rewrite_file($path, sub { uc $_[0] }); + my $changed = XCAT::BuildUtils::rewrite_file($path, sub { uc $_[0] }); is( $changed, 1, 'rewriting a file that exists reports that it did' ); is( read_text($path), "ONE\nTWO\n", 'and applies the transform' ); my $absent = File::Spec->catfile($dir, 'not-there.txt'); - is( BuildUtils::rewrite_file($absent, sub { die 'must not run' }), 0, + is( XCAT::BuildUtils::rewrite_file($absent, sub { die 'must not run' }), 0, 'a file that is not there is left alone, not created' ); ok( !-e $absent, 'and really is not created' ); } @@ -360,24 +360,24 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', my $path = File::Spec->catfile($dir, 'Version'); write_text($path, "2.18.1\n"); - is( BuildUtils::read_line($path), '2.18.1', + is( XCAT::BuildUtils::read_line($path), '2.18.1', 'a one-line stamp comes back without its newline' ); write_text($path, "2.18.1\nignored\n"); - is( BuildUtils::read_line($path), '2.18.1', + is( XCAT::BuildUtils::read_line($path), '2.18.1', 'and only the first line is taken' ); write_text($path, "2.18.1"); - is( BuildUtils::read_line($path), '2.18.1', + is( XCAT::BuildUtils::read_line($path), '2.18.1', 'a file with no trailing newline reads the same' ); # builddebs.pl falls back to snap_release() when there is no Release file, # so absence has to be reported rather than raised. - is( BuildUtils::read_line(File::Spec->catfile($dir, 'nope')), undef, + is( XCAT::BuildUtils::read_line(File::Spec->catfile($dir, 'nope')), undef, 'a file that is not there reads as undef, not an error' ); write_text($path, ""); - is( BuildUtils::read_line($path), undef, 'and so does an empty file' ); + is( XCAT::BuildUtils::read_line($path), undef, 'and so does an empty file' ); } # ------------------------------------------------- the published helper script -- @@ -389,7 +389,7 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', my $dir = tempdir(CLEANUP => 1); my $path = File::Spec->catfile($dir, 'mklocalrepo.sh'); - BuildUtils::write_script($path, "#!/bin/sh\necho hello\n"); + XCAT::BuildUtils::write_script($path, "#!/bin/sh\necho hello\n"); is( read_text($path), "#!/bin/sh\necho hello\n", 'a helper script keeps the exact text it was given' ); ok( -x $path, 'and is executable, which is the point of writing it this way' ); @@ -399,7 +399,7 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', # The genesis postscripts builddebs.pl installs are 0755, not 0775, so the # mode has to stay the caller's to choose. my $ps = File::Spec->catfile($dir, 'bmcsetup'); - BuildUtils::write_script($ps, "#!/bin/sh\n", 0755); + XCAT::BuildUtils::write_script($ps, "#!/bin/sh\n", 0755); is( (stat $ps)[2] & 07777, 0755, 'a caller may ask for a different mode' ); } @@ -408,20 +408,20 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', # two builders disagreed about shifting it, so a caller comparing sh() against # a specific code got the code from one and a multiple of it from the other. { - is( BuildUtils::sh('true'), 0, 'a command that succeeds reports 0' ); - is( BuildUtils::sh('sh -c "exit 3"'), 3, + is( XCAT::BuildUtils::sh('true'), 0, 'a command that succeeds reports 0' ); + is( XCAT::BuildUtils::sh('sh -c "exit 3"'), 3, 'the exit code is returned, not the wait status it is packed into' ); - isnt( BuildUtils::sh('sh -c "exit 3"'), 768, + isnt( XCAT::BuildUtils::sh('sh -c "exit 3"'), 768, 'and specifically not the exit code times 256' ); } { # --verbose echoes the command; the default does not. - local $BuildUtils::VERBOSE = 1; + local $XCAT::BuildUtils::VERBOSE = 1; my $out = ''; open my $fh, '>', \$out or die; my $old = select $fh; - BuildUtils::sh('true'); + XCAT::BuildUtils::sh('true'); select $old; close $fh; like( $out, qr/\ARunning: true/, 'a verbose run echoes the command' ); @@ -433,16 +433,16 @@ isnt( git_revision( git => sub { '' }, read_file => sub { '' } ), '', # Reversed polarities for one operation are easy to misread, so there is now a # single name with a single direction. { - is( BuildUtils::sh_or_die('true'), 0, + is( XCAT::BuildUtils::sh_or_die('true'), 0, 'a command that succeeds returns 0 and does not die' ); - my $err = eval { BuildUtils::sh_or_die('sh -c "exit 4"', 'FATAL: it failed'); 1 } + my $err = eval { XCAT::BuildUtils::sh_or_die('sh -c "exit 4"', 'FATAL: it failed'); 1 } ? '' : $@; like( $err, qr/FATAL: it failed/, 'a failure dies with the caller\'s message' ); like( $err, qr/exit 4/, 'and names the exit code, which the old spellings threw away' ); - my $bare = eval { BuildUtils::sh_or_die('sh -c "exit 5"'); 1 } ? '' : $@; + my $bare = eval { XCAT::BuildUtils::sh_or_die('sh -c "exit 5"'); 1 } ? '' : $@; like( $bare, qr/\Qsh -c "exit 5"\E/, 'a caller with no message still gets the command that failed' ); } diff --git a/xCAT-test/unit/builddebs_lock.t b/xCAT-test/unit/builddebs_lock.t index 68ae09875..5166dcba9 100644 --- a/xCAT-test/unit/builddebs_lock.t +++ b/xCAT-test/unit/builddebs_lock.t @@ -16,10 +16,10 @@ use warnings; use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; -use lib "$FindBin::Bin/../.."; +use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; -use BuildUtils qw(lock_id_for take_build_lock); +use XCAT::BuildUtils qw(lock_id_for take_build_lock); my $lockdir = tempdir(CLEANUP => 1); diff --git a/xCAT-test/unit/buildrpms_source_only.t b/xCAT-test/unit/buildrpms_source_only.t index 5779d34b0..8b5de8098 100644 --- a/xCAT-test/unit/buildrpms_source_only.t +++ b/xCAT-test/unit/buildrpms_source_only.t @@ -142,12 +142,11 @@ ok( !grep( { $_ eq 'buildpkgs' } @{ stages_for(1) } ), # buildrpms.pl rewrites the tracked Gitinfo in its working directory and creates # $HOME/rpmbuild. Running it in place left the developer's tree dirty and reached # into their home for a test that only exercises argument parsing. Version is -# staged because the same file-scope code reads it and dies without it. Both -# modules named BuildUtils.pm are staged at their own relative paths, because -# buildrpms.pl loads each from a different directory: BuildUtils.pm from its own, -# and XCAT::BuildUtils from build-utils/lib/XCAT. +# staged because the same file-scope code reads it and dies without it, and +# XCAT::BuildUtils at its own relative path because buildrpms.pl puts +# build-utils/lib on @INC relative to its own directory. my $sandbox = tempdir(CLEANUP => 1); -for my $needed (qw(buildrpms.pl Version BuildUtils.pm +for my $needed (qw(buildrpms.pl Version build-utils/lib/XCAT/BuildUtils.pm)) { my $from = repo_path($needed); BAIL_OUT("$needed is missing from the repository") unless -r $from; diff --git a/xCAT-test/unit/ubuntu_2604_pkglist.t b/xCAT-test/unit/ubuntu_2604_pkglist.t index 4fcd51188..8ad55973e 100644 --- a/xCAT-test/unit/ubuntu_2604_pkglist.t +++ b/xCAT-test/unit/ubuntu_2604_pkglist.t @@ -9,8 +9,8 @@ use Test::More; use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; use lib "$FindBin::Bin/../../perl-xCAT"; -use lib "$FindBin::Bin/../.."; -use BuildUtils (); +use lib "$FindBin::Bin/../../build-utils/lib"; +use XCAT::BuildUtils (); my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' ); @@ -155,13 +155,13 @@ close($rel_fh); ok( xCAT::Template::ubuntu_subiquity_local_apt_repo($repo_dir), 'an indexed directory is' ); -# The releases the deb builder serves by default. Read from BuildUtils, which is where +# The releases the deb builder serves by default. Read from XCAT::BuildUtils, which is where # the builder itself reads them, rather than matched against the source that sets them: # the old assertion passed on any file containing that shell fragment, and broke on a # reflow that changed nothing. -ok( scalar( grep { $_ eq 'resolute' } BuildUtils::default_dists() ), +ok( scalar( grep { $_ eq 'resolute' } XCAT::BuildUtils::default_dists() ), 'the Ubuntu repository serves resolute by default' ); -like( BuildUtils::reprepro_distributions( [ BuildUtils::default_dists() ], undef ), +like( XCAT::BuildUtils::reprepro_distributions( [ XCAT::BuildUtils::default_dists() ], undef ), qr/^Codename: resolute$/m, 'and a resolute stanza reaches conf/distributions' );