2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-25 01:04:05 +00:00

fix(xcat-core): the Genesis build-root and payload tests assert on source text and script output, not behaviour

genesis_ubuntu_build_root.t cut REQUIRED_PACKAGES out of
builddeb-genesis-base with a regex and evaluated it through bash -c. It
then matched the apt-cache fallback calls as text. Reformatting the
script broke it, and a wrong choice between renamed packages passed it.
genesis_payload_verification.t ran the verifier script and parsed its
stderr.

genesis_ubuntu_build_root.t now calls required_packages() with a chosen
set of carried packages and asserts the exact result: the amd64 extras,
the name picked for each renamed package, the order apt is asked,
tzdata-legacy, and the error for a release that carries neither name.
It reads the mandatory commands from XCAT::GenesisPayload, the code the
build uses. genesis_payload_verification.t calls the XCAT::GenesisPayload
functions with chosen payload trees and asserts the exact missing paths
and results.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-24 16:22:25 -03:00
parent eb30d2e92a
commit 888698e384
2 changed files with 201 additions and 216 deletions
+145 -152
View File
@@ -1,192 +1,185 @@
#!/usr/bin/env perl
# Drive verify-genesis-payload against payload trees that each leave out one thing the image
# needs.
# XCAT::GenesisPayload decides whether an extracted Genesis payload is complete.
# verify-genesis-payload calls it in the EL spec and in the Ubuntu builder. Each payload
# below leaves out one thing the image needs.
use strict;
use warnings;
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Slurper qw(read_text write_text);
use File::Slurper qw(write_text);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../xCAT-genesis-builder/lib";
use Test::More;
use XCAT::Test::File qw(repo_path);
use XCAT::GenesisPayload qw(module_commands missing_paths check_payload);
my $verifier = repo_path('xCAT-genesis-builder/verify-genesis-payload');
if (!-f $verifier) {
# Skipping here would cover nothing: the Ubuntu Genesis build has no payload gate at
# all until this file exists.
fail('xCAT-genesis-builder/verify-genesis-payload is missing');
done_testing();
exit;
}
plan tests => 22;
my $tmpdir = tempdir(CLEANUP => 1);
my $module_seq = 0;
my $OPENSSH_99 = "OpenSSH_9.9p1\n/usr/libexec/openssh/sshd-session\n";
my $OPENSSH_80 = "OpenSSH_8.0p1\n";
# A complete payload: OpenSSH 9.9 sshd plus its session helper, tmux plus a UTF-8 locale.
my $good = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 1);
my ($rc, $err) = run($good, 'usr/sbin/dhclient');
is($rc, 0, 'a complete payload passes') or diag($err);
my @COMPLETE = qw(
usr/sbin/sshd usr/libexec/openssh/sshd-session usr/bin/tmux
usr/lib/locale/C.utf8/LC_CTYPE usr/sbin/dhclient usr/bin/mktemp
usr/bin/awk etc/services usr/bin/openssl usr/bin/wget usr/bin/tar
);
# The payload carries exactly the paths it is given.
sub carries {
my %present = map { $_ => 1 } @_;
return sub { $present{ $_[0] } };
}
sub all_but { my %gone = map { $_ => 1 } @_; return carries(grep { !$gone{$_} } @COMPLETE) }
is_deeply([ missing_paths(carries(@COMPLETE), sshd => $OPENSSH_99, required => ['usr/sbin/dhclient']) ],
[], 'a complete payload passes');
# doxcat calls dhclient with ISC flags. dhclient.conf and dhclient-script are not enough.
my $nodhcp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 0, mktemp => 1);
($rc, $err) = run($nodhcp, 'usr/sbin/dhclient');
isnt($rc, 0, 'a payload without dhclient fails');
like($err, qr{usr/sbin/dhclient}, 'the missing dhclient is named');
is_deeply([ missing_paths(all_but('usr/sbin/dhclient'), sshd => $OPENSSH_99, required => ['usr/sbin/dhclient']) ],
['usr/sbin/dhclient (required by the build)'],
'a payload without dhclient fails and names it');
# sshd 9.9 execs /usr/libexec/openssh/sshd-session for every connection.
my $nohelper = build_payload(sshd_execs_session => 1, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1);
($rc, $err) = run($nohelper, 'usr/sbin/dhclient');
isnt($rc, 0, 'a payload whose sshd execs sshd-session but does not ship it fails');
like($err, qr{sshd-session}, 'the missing sshd-session is named');
# OpenSSH 8 does not use the helper, so el8 must still pass without it.
my $openssh8 = build_payload(sshd_execs_session => 0, session_helper => 0, tmux => 1, locale => 1, dhclient => 1, mktemp => 1);
($rc, $err) = run($openssh8, 'usr/sbin/dhclient');
is($rc, 0, 'an OpenSSH 8 payload passes without sshd-session') or diag($err);
is_deeply([ missing_paths(all_but('usr/libexec/openssh/sshd-session'), sshd => $OPENSSH_99) ],
['usr/libexec/openssh/sshd-session (this sshd execs it for every connection)'],
'a payload whose sshd execs sshd-session but does not ship it fails');
# tmux without a UTF-8 locale is what stopped doxcat from ever running.
my $nolocale = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 0, dhclient => 1, mktemp => 1);
($rc, $err) = run($nolocale, 'usr/sbin/dhclient');
isnt($rc, 0, 'a payload with tmux and no UTF-8 locale fails');
like($err, qr{C\.utf8}, 'the missing locale is named');
is_deeply([ missing_paths(all_but('usr/lib/locale/C.utf8/LC_CTYPE'), sshd => $OPENSSH_99) ],
['usr/lib/locale/C.utf8/LC_CTYPE (tmux refuses to start without a UTF-8 locale)'],
'a payload with tmux and no UTF-8 locale fails');
# getdestiny makes its request file with mktemp.
my $nomktemp = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1, dhclient => 1, mktemp => 0);
($rc, $err) = run($nomktemp, 'usr/sbin/dhclient');
isnt($rc, 0, 'a payload without mktemp fails');
like($err, qr{usr/bin/mktemp}, 'the missing mktemp is named');
is_deeply([ missing_paths(all_but('usr/bin/mktemp'), sshd => $OPENSSH_99) ],
['usr/bin/mktemp (getdestiny makes its request file with it)'],
'a payload without mktemp fails');
# Genesis is reached over ssh.
is_deeply([ missing_paths(all_but('usr/sbin/sshd')) ],
['usr/sbin/sshd (Genesis is reached over ssh)'],
'a payload without sshd fails');
is_deeply([ missing_paths(carries(grep({ $_ ne 'usr/libexec/openssh/sshd-session' } @COMPLETE),
'usr/lib/openssh/sshd-session'), sshd => $OPENSSH_99) ],
[], 'the Debian path of sshd-session counts');
# OpenSSH 8 does not use the helper, so el8 must still pass without it.
is_deeply([ missing_paths(all_but('usr/libexec/openssh/sshd-session'), sshd => $OPENSSH_80) ],
[], 'an OpenSSH 8 payload passes without sshd-session');
is_deeply([ missing_paths(all_but('usr/lib/locale/C.utf8/LC_CTYPE', 'usr/bin/tmux'), sshd => $OPENSSH_99) ],
[], 'a payload without tmux needs no locale');
# dracut_install reports a missing binary and returns, so every name the dracut module
# installs has to be checked against the payload.
my $module = write_module_setup([qw(openssl wget tar)]);
my $full = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1,
dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)]);
($rc, $err) = run_with_commands($module, $full);
is($rc, 0, 'a payload carrying every command the module names passes') or diag($err);
# installs has to be checked against the payload. A name starting with "/" is installed at
# that same path; the rest are commands.
my $tmpdir = tempdir(CLEANUP => 1);
my $module = "$tmpdir/module-setup.sh";
write_text($module, <<'SH');
#!/bin/bash
my $noopenssl = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1,
dhclient => 1, mktemp => 1, commands => [qw(wget tar)]);
($rc, $err) = run_with_commands($module, $noopenssl);
isnt($rc, 0, 'a payload without openssl fails');
like($err, qr/openssl/, 'the missing openssl is named');
install() {
dracut_install -o openssl wget tar # a trailing comment
dracut_install /usr/bin/awk /etc/services
if command -v dhclient >/dev/null 2>&1; then
dracut_install dhclient
fi
}
# dracut_install installs an absolute path at that same path, so a name starting with "/" is a
# command the payload must carry. doxcat, getdestiny and the firmware wrappers all run awk.
my $noawk = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1,
dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['usr/bin/awk']);
($rc, $err) = run_with_commands($module, $noawk);
isnt($rc, 0, 'a payload without the absolute path /usr/bin/awk fails');
like($err, qr{/usr/bin/awk}, 'the missing /usr/bin/awk is named');
# The module names data files by absolute path too. Genesis resolves service names with
# /etc/services.
my $noservices = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1,
dhclient => 1, mktemp => 1, commands => [qw(openssl wget tar)], absent => ['etc/services']);
($rc, $err) = run_with_commands($module, $noservices);
isnt($rc, 0, 'a payload without the absolute path /etc/services fails');
like($err, qr{/etc/services}, 'the missing /etc/services is named');
installkernel() {
dracut_install notacommand
}
SH
my @commands = module_commands($module);
# The DHCP client is release-dependent, so the module installs it inside a conditional. Those
# names are not the contract; the spec passes the one it wants as a required path.
my $conditional = write_module_setup(['wget'], ['dhclient']);
my $nodhclient = build_payload(sshd_execs_session => 1, session_helper => 1, tmux => 1, locale => 1,
dhclient => 0, mktemp => 1, commands => ['wget']);
($rc, $err) = run_with_commands($conditional, $nodhclient);
is($rc, 0, 'a name installed under a condition is not required') or diag($err);
is_deeply(\@commands, [qw(/etc/services /usr/bin/awk openssl tar wget)],
'the top-level names of install() are read back, without options, comments, conditionals
or other functions');
# A module the verifier cannot read names for covers nothing, so say so instead of passing.
my %names = (commands => \@commands, source => $module, sshd => $OPENSSH_99);
is_deeply([ missing_paths(carries(@COMPLETE), %names) ], [],
'a payload carrying every command the module names passes');
is_deeply([ missing_paths(carries(grep({ $_ ne 'usr/bin/wget' } @COMPLETE), 'sbin/wget'), %names) ],
[], 'a command under sbin counts as present');
is_deeply([ missing_paths(all_but('usr/bin/openssl'), %names) ],
["openssl (installed by $module)"],
'a payload without openssl fails and names it');
# doxcat, getdestiny and the firmware wrappers all run awk.
is_deeply([ missing_paths(all_but('usr/bin/awk'), %names) ],
["/usr/bin/awk (installed by $module)"],
'a payload without the absolute path /usr/bin/awk fails');
# Genesis resolves service names with /etc/services.
is_deeply([ missing_paths(all_but('etc/services'), %names) ],
["/etc/services (installed by $module)"],
'a payload without the absolute path /etc/services fails');
# A module the verifier cannot read names from covers nothing, so say so instead of passing.
my $unparsable = "$tmpdir/module-setup-unparsable.sh";
write_text($unparsable, "#!/bin/bash\nsetup() {\n dracut_install wget\n}\n");
($rc, $err) = run_with_commands($unparsable, $full);
is($rc, 2, 'a module the verifier finds no command names in is a usage error');
like($err, qr/command name/, 'the empty command list is named');
ok(!eval { module_commands($unparsable); 1 }, 'a module with no install() names is refused');
is($@, "verify-genesis-payload: no command name read from $unparsable\n",
'the empty command list is named');
($rc, $err) = run_with_commands("$tmpdir/no-such-module", $full);
is($rc, 2, 'a module file that cannot be read is a usage error');
ok(!eval { module_commands("$tmpdir/no-such-module"); 1 }, 'an unreadable module is refused');
is($@, "verify-genesis-payload: cannot read $tmpdir/no-such-module\n",
'the unreadable module is named');
($rc, $err) = run("$tmpdir/does-not-exist");
is($rc >> 0, 2, 'a missing payload directory is a usage error');
# --- the command line reads a real payload tree ---------------------------------------------
my $good = payload_tree(@COMPLETE);
write_text("$good/usr/sbin/sshd", $OPENSSH_99);
my $nodhcp = payload_tree(grep { $_ ne 'usr/sbin/dhclient' } @COMPLETE);
my $nohelper = payload_tree(grep { $_ ne 'usr/libexec/openssh/sshd-session' } @COMPLETE);
write_text("$nohelper/usr/sbin/sshd", $OPENSSH_99);
is_deeply([ check_payload($good, 'usr/sbin/dhclient') ],
[ 0, "verify-genesis-payload: $good is complete\n" ],
'a complete payload exits 0 and says so');
is_deeply([ check_payload($nodhcp, 'usr/sbin/dhclient') ],
[ 1, "verify-genesis-payload: $nodhcp is incomplete:\n"
. " usr/sbin/dhclient (required by the build)\n" ],
'an incomplete payload exits 1 and lists what is missing');
is_deeply([ check_payload($nohelper) ],
[ 1, "verify-genesis-payload: $nohelper is incomplete:\n"
. " usr/libexec/openssh/sshd-session (this sshd execs it for every connection)\n" ],
'the command line reads usr/sbin/sshd to decide on sshd-session');
is_deeply([ check_payload('--commands-from', $module, $nodhcp) ],
[ 0, "verify-genesis-payload: $nodhcp is complete\n" ],
'--commands-from does not require a name installed under a condition');
my $noopenssl = payload_tree(grep { $_ ne 'usr/bin/openssl' } @COMPLETE);
is_deeply([ check_payload("--commands-from=$module", $noopenssl) ],
[ 1, "verify-genesis-payload: $noopenssl is incomplete:\n"
. " openssl (installed by $module)\n" ],
'--commands-from=<file> checks the names of the module');
is_deeply([ check_payload('--commands-from', $unparsable, $good) ],
[ 2, "verify-genesis-payload: no command name read from $unparsable\n" ],
'a module with no command names is a usage error');
is_deeply([ check_payload('--commands-from', "$tmpdir/no-such-module", $good) ],
[ 2, "verify-genesis-payload: cannot read $tmpdir/no-such-module\n" ],
'a module file that cannot be read is a usage error');
is_deeply([ check_payload("$tmpdir/does-not-exist") ],
[ 2, "verify-genesis-payload: not a payload directory: $tmpdir/does-not-exist\n" ],
'a missing payload directory is a usage error');
is_deeply([ check_payload() ],
[ 2, "verify-genesis-payload: not a payload directory: <empty>\n" ],
'no payload directory at all is a usage error');
done_testing();
#---
# build_payload: make a payload tree with the pieces the verifier reasons about.
# payload_tree: a payload directory that carries the given paths as empty files.
#---
sub build_payload {
my (%opt) = @_;
sub payload_tree {
my $root = tempdir(DIR => $tmpdir, CLEANUP => 1);
make_path("$root/usr/sbin", "$root/usr/bin", "$root/usr/libexec/openssh");
write_text("$root/usr/sbin/sshd",
$opt{sshd_execs_session}
? "OpenSSH_9.9p1\n/usr/libexec/openssh/sshd-session\n"
: "OpenSSH_8.0p1\n");
write_text("$root/usr/libexec/openssh/sshd-session", "helper\n") if $opt{session_helper};
write_text("$root/usr/bin/tmux", "tmux\n") if $opt{tmux};
if ($opt{locale}) {
make_path("$root/usr/lib/locale/C.utf8");
write_text("$root/usr/lib/locale/C.utf8/LC_CTYPE", "ctype\n");
}
write_text("$root/usr/sbin/dhclient", "dhclient\n") if $opt{dhclient};
write_text("$root/usr/bin/mktemp", "mktemp\n") if $opt{mktemp};
write_text("$root/usr/bin/$_", "$_\n") for @{ $opt{commands} || [] };
# The module written by write_module_setup names these two by absolute path.
my %absent = map { $_ => 1 } @{ $opt{absent} || [] };
for my $path (qw(usr/bin/awk etc/services)) {
next if $absent{$path};
my ($dir) = $path =~ m{^(.*)/};
make_path("$root/$dir");
write_text("$root/$path", "$path\n");
for my $path (@_) {
make_path(dirname("$root/$path"));
write_text("$root/$path", '');
}
return $root;
}
#---
# run: run the verifier and return its exit status and stderr.
#---
sub run {
my ($root, @required) = @_;
my $errfile = "$tmpdir/err.$$";
my $cmd = join ' ', map { "'$_'" } ($verifier, $root, @required);
system("/bin/bash $cmd >/dev/null 2>$errfile");
my $status = $? >> 8;
my $err = -f $errfile ? read_text($errfile) : '';
unlink $errfile;
return ($status, $err);
}
#---
# write_module_setup: a dracut module whose install() names commands at the top level, and
# optionally more inside a conditional.
#---
sub write_module_setup {
my ($top, $conditional) = @_;
my $path = "$tmpdir/module-setup." . ++$module_seq . ".sh";
my $text = "#!/bin/bash\n\ninstall() {\n";
$text .= " dracut_install " . join(' ', @$top) . " # a trailing comment\n";
$text .= " dracut_install /usr/bin/awk /etc/services\n";
if ($conditional) {
$text .= " if command -v " . $conditional->[0] . " >/dev/null 2>&1; then\n";
$text .= " dracut_install " . join(' ', @$conditional) . "\n";
$text .= " fi\n";
}
$text .= "}\n";
write_text($path, $text);
return $path;
}
#---
# run_with_commands: run the verifier with the command list read back from a dracut module.
#---
sub run_with_commands {
my ($module, $root) = @_;
my $errfile = "$tmpdir/err.commands.$$";
my $cmd = join ' ', map { "'$_'" } ($verifier, '--commands-from', $module, $root);
system("/bin/bash $cmd >/dev/null 2>$errfile");
my $status = $? >> 8;
my $err = -f $errfile ? read_text($errfile) : '';
unlink $errfile;
return ($status, $err);
}
+56 -64
View File
@@ -3,97 +3,89 @@
# mandatory. dracut_install reports a missing command and returns 0, so a hole in the image
# does not fail the build.
#
# The mandatory list comes from RUNNING the module: module-setup.sh is sourced with
# dracut_install shadowed, _dracut_install_opt neutralised, and install() called. The
# package list comes from evaluating the REQUIRED_PACKAGES assignment in the build script.
# XCAT::GenesisBuildRoot::required_packages lists the packages of the build root, and
# XCAT::GenesisPayload::module_commands reads the mandatory commands from the module, as
# verify-genesis-payload does. The refusal of a root of another release is in
# genesis_deb_per_codename.t.
use strict;
use warnings;
use File::Slurper qw(read_text);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use lib "$FindBin::Bin/../../xCAT-genesis-builder/lib";
use Test::More;
use XCAT::GenesisBuildRoot qw(required_packages);
use XCAT::GenesisPayload qw(module_commands);
use XCAT::Test::File qw(repo_path);
my $builder = repo_path('xCAT-genesis-builder/builddeb-genesis-base');
my $module = repo_path('xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh');
die "builddeb-genesis-base not found\n" unless -f $builder;
die "ubuntu module-setup.sh not found\n" unless -f $module;
plan tests => 9;
my $builder = repo_path('xCAT-genesis-builder/builddeb-genesis-base');
my $module = repo_path('xCAT-genesis-builder/dracut_105/ubuntu/module-setup.sh');
# Mandatory commands a minimal Ubuntu server root does NOT already provide, and the packages
# that supply each one. hwclock has two names: it left util-linux for util-linux-extra in
# 23.04, and the build root asks apt which name this release carries.
# 23.04.
my %PACKAGES_FOR = (
dhclient => ['isc-dhcp-client'],
ifenslave => ['ifenslave'],
hwclock => [ 'util-linux-extra', 'util-linux' ],
);
my %mandatory = map { $_ => 1 } mandatory_commands($module);
my @packages = required_packages($builder);
# A release carries exactly the names in its list.
sub release {
my %carried = map { $_ => 1 } @_;
return sub { $carried{ $_[0] } };
}
my $NOBLE = release(qw(bind9-dnsutils dnsutils util-linux-extra util-linux tzdata-legacy));
my $FOCAL = release(qw(dnsutils util-linux));
my @noble = required_packages('amd64', 'noble', $NOBLE);
is_deeply([ @noble[ -5 .. -1 ] ],
[qw(dmidecode efibootmgr bind9-dnsutils util-linux-extra tzdata-legacy)],
'amd64 adds dmidecode and efibootmgr, then the first name the release carries');
my @focal = required_packages('amd64', 'focal', $FOCAL);
is_deeply([ @focal[ -2 .. -1 ] ], [qw(dnsutils util-linux)],
'a release without the new names gets dnsutils and util-linux, and no tzdata-legacy');
my @ppc = required_packages('ppc64el', 'noble', $NOBLE);
is_deeply([ @ppc[ -3 .. -1 ] ], [qw(bind9-dnsutils util-linux-extra tzdata-legacy)],
'ppc64el gets neither dmidecode nor efibootmgr');
is_deeply([ @ppc[ 0 .. $#ppc - 3 ] ], [ @noble[ 0 .. $#noble - 5 ] ],
'ppc64el and amd64 share the base packages');
my @asked;
required_packages('amd64', 'noble', sub { push @asked, $_[0]; $NOBLE->($_[0]) });
is_deeply(\@asked, [qw(bind9-dnsutils util-linux-extra tzdata-legacy)],
'apt is asked for the older name only when the newer one is absent');
ok(!eval { required_packages('amd64', 'oddball', release('bind9-dnsutils')); 1 },
'a release that carries no hwclock package fails');
is($@, "ERROR: oddball carries none of these packages: util-linux-extra util-linux\n",
'the failure names the release and the missing alternatives');
# --- every mandatory command has a package in the build root -----------------------------
# An absolute path is a data file, not a command.
my %mandatory = map { $_ => 1 } grep { !m{^/} } module_commands($module);
my %packages = map { $_ => 1 } @noble;
for my $command (sort keys %PACKAGES_FOR) {
my @provider = @{ $PACKAGES_FOR{$command} };
ok($mandatory{$command}, "the Ubuntu dracut module installs '$command' unconditionally");
my @named = grep { my $p = $_; grep { $_ eq $p } @packages } @provider;
ok(scalar @named,
ok(scalar(grep { $packages{$_} } @provider),
"the build root installs @{[ join ' or ', @provider ]}, which provides '$command'");
}
ok(grep({ $_ eq 'util-linux' } @focal), 'a release before 23.04 gets hwclock from util-linux');
# doxcat asks dhclient for the provisioning lease.
ok($mandatory{dhclient} && scalar(grep { $_ eq 'isc-dhcp-client' } @packages),
ok($mandatory{dhclient} && $packages{'isc-dhcp-client'},
'the Genesis image can obtain a DHCP lease');
# dracut_install is silent about a missing command, so the payload needs its own gate.
# xCAT-genesis-base.spec runs the same verifier on the EL path.
my $text = do { open my $fh, '<', $builder or die "$builder: $!"; local $/; <$fh> };
like($text, qr{verify-genesis-payload}, 'builddeb-genesis-base verifies the payload it packages');
# The build cannot run here, so the call to the payload gate is read from the script. The
# gate itself is exercised by genesis_payload_verification.t.
like(read_text($builder),
qr{^bash "\$DIR/verify-genesis-payload" --commands-from "\$DRACUTMODDIR/module-setup\.sh" "\$GENESIS_FS"}m,
'builddeb-genesis-base verifies the payload it packages against the module');
# The image belongs to the release whose kernel it carries, so the builder must refuse a
# root of any other release.
like($text, qr{--expect-codename}, 'builddeb-genesis-base takes the release it is building for');
# An absolute path in the install() output is a data file, not a command.
sub mandatory_commands {
my ($path) = @_;
my $dir = tempdir(CLEANUP => 1);
my $driver = "$dir/collect.sh";
open my $fh, '>', $driver or die "$driver: $!";
print $fh <<"BASH";
dracut_install() { printf '%s\\n' "\$\@"; }
instmods() { :; }
inst_multiple() { :; }
inst() { :; }
dpkg-architecture() { echo x86_64-linux-gnu; }
. '$path'
# _dracut_install_opt installs only what the build root already has. Neutralise it after
# sourcing, so its commands stay out of the mandatory set.
_dracut_install_opt() { :; }
install
BASH
close $fh;
my @out = qx{bash '$driver' 2>/dev/null};
die("running install() from $path produced nothing") unless @out;
my %seen;
my @names = grep { !$seen{$_}++ } grep { length && !m{^/} } map { chomp; $_ } @out;
die("install() from $path named no bare commands") unless @names;
return @names;
}
# Evaluate the assignment rather than parse it, so the list is the value the script uses.
# add_first_available names the alternatives for a package that was renamed between releases,
# so its candidates count too.
sub required_packages {
my ($path) = @_;
my $text = do { open my $fh, '<', $path or die "$path: $!"; local $/; <$fh> };
my ($block) = $text =~ /^(REQUIRED_PACKAGES="[^"]*")/ms;
die("no REQUIRED_PACKAGES assignment in $path") unless $block;
my $out = qx{bash -c 'set -u; $block; printf "%s\\n" \$REQUIRED_PACKAGES' 2>/dev/null};
my @packages = grep { length } split /\s+/, ($out // '');
die("REQUIRED_PACKAGES in $path evaluated to nothing") unless @packages;
push @packages, grep { length } split /\s+/, $1
while $text =~ /^add_first_available\s+(.+)$/mg;
return @packages;
}
done_testing();