2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-10-20 06:35:32 +00:00

Merge pull request #86 from VersatusHPC/fix/goconserver-goamd64-v1

fix(xcat-dep): goconserver is built for x86-64-v3 and SIGILLs on an older node
This commit is contained in:
Daniel Hilst
2026-10-08 11:20:15 -03:00
committed by GitHub
5 changed files with 172 additions and 9 deletions
+14
View File
@@ -1,11 +1,25 @@
#!/usr/bin/make -f
export CGO_ENABLED=0
# Set explicitly, so the compiler never takes a distro default. A GOAMD64=v3 binary stops a
# pre-AVX2 node with SIGILL.
export GOAMD64=v1
%:
dh $@
override_dh_auto_configure:
# The last recipe line reads GOAMD64 out of the binaries that were just compiled. An export line
# can stop reaching the compiler, and a grep for that line passes when it does.
override_dh_auto_build:
go build -trimpath -buildvcs=false -ldflags "-X main.Version=0.3.3" -o goconserver goconserver.go
go build -trimpath -buildvcs=false -ldflags "-X main.Version=0.3.3" -o congo cmd/congo.go
if [ "$$(go env GOARCH)" = amd64 ]; then \
for bin in goconserver congo; do \
setting=$$(go version -m $$bin | awk -F'\t' '$$2 == "build" && $$3 ~ /^GOAMD64=/ { print $$3 }'); \
if [ "$$setting" != "GOAMD64=v1" ]; then \
echo "FATAL: $$bin reports '$$setting', not GOAMD64=v1" >&2; \
exit 1; \
fi; \
done; \
fi
override_dh_auto_install:
install -D -m 755 goconserver $(CURDIR)/debian/goconserver/usr/bin/goconserver
install -D -m 755 congo $(CURDIR)/debian/goconserver/usr/bin/congo
+51 -1
View File
@@ -279,7 +279,10 @@ $go_prep
# 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
# GOAMD64 is set here, not inherited: EL10 raises its platform baseline to x86-64-v3 and its
# golang bakes GOAMD64=v3 in as the default. One el10 chroot compiles the x86_64 binary that
# is published for rh8, rh9 and rh10, and a v3 binary stops a pre-AVX2 node with SIGILL.
export GOFLAGS=-mod=mod GOTOOLCHAIN=local CGO_ENABLED=0 GOAMD64=v1
export GOCACHE=%{_builddir}/.gocache GOPATH=%{_builddir}/.gopath GOMODCACHE=%{_builddir}/.gomodcache
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
@@ -291,6 +294,24 @@ install -Dm0644 %{SOURCE1} %{buildroot}/usr/lib/systemd/system/goconserver.servi
install -Dm0644 %{SOURCE2} %{buildroot}/etc/goconserver/server.conf
mkdir -p %{buildroot}/var/log/goconserver %{buildroot}/var/lib/goconserver
%check
$go_environment
# Read GOAMD64 out of the binaries that are staged for packaging. An export line can stop
# reaching the compiler, and a grep for that line passes when it does. The build metadata in the
# binary names what compiled it, so this gate fails the build instead of publishing a v3 binary.
# Do not write an rpm section name in this comment. rpm reads the name as the start of a new
# section, even behind a hash, and refuses the spec.
if [ "\$(go env GOARCH)" = amd64 ]; then
for bin in goconserver congo; do
setting=\$(go version -m %{buildroot}/usr/bin/\$bin |
awk -F'\\t' '\$2 == "build" && \$3 ~ /^GOAMD64=/ { print \$3 }')
if [ "\$setting" != "GOAMD64=v1" ]; then
echo "FATAL: \$bin reports '\$setting', not GOAMD64=v1" >&2
exit 1
fi
done
fi
%files
/usr/bin/goconserver
/usr/bin/congo
@@ -384,6 +405,8 @@ sub cross_build_and_package {
local $ENV{GOFLAGS} = '-mod=mod';
local $ENV{GOTOOLCHAIN} = 'local';
local $ENV{GOARCH} = $goarch{$target_arch};
# Explicit, so no build inherits a distro default again (see the %build comment).
local $ENV{GOAMD64} = 'v1';
my $bin_dir = "$work_dir/bin";
make_path($bin_dir);
@@ -395,6 +418,7 @@ sub cross_build_and_package {
sh_quote($ldflags) . " -o " . sh_quote("$bin_dir/$out") . " " . sh_quote($main) .
" >" . sh_quote("$log_dir/go-build-$out.log") . " 2>&1");
die "$out binary not built\n" if !-x "$bin_dir/$out";
assert_goamd64_v1("$bin_dir/$out");
}
print_step("Assemble SRPM sources");
@@ -501,6 +525,32 @@ SPEC
return;
}
#-------------------------------------------------------------------------------
=head3 assert_goamd64_v1
Descriptions: Stop the build when an amd64 Go binary was not compiled with GOAMD64=v1.
The check reads the binary's own build metadata, because an export line can
stop reaching the compiler and still be present in the script.
Arguments: $binary -- path to a compiled Go binary
Returns: nothing; dies when the setting is absent or not v1
=cut
#-------------------------------------------------------------------------------
sub assert_goamd64_v1 {
my ($binary) = @_;
my $quoted = sh_quote($binary);
my $meta = `go version -m $quoted 2>&1`;
die "FATAL: go version -m $binary failed:\n$meta" if $? != 0;
# GOAMD64 applies to amd64 only; another GOARCH records no such setting.
return if $meta !~ /^\s*build\s+GOARCH=amd64\s*$/m;
my ($setting) = $meta =~ /^\s*build\s+(GOAMD64=\S+)\s*$/m;
$setting = 'no GOAMD64 setting' if !defined $setting;
die "FATAL: $binary reports $setting, not GOAMD64=v1\n" if $setting ne 'GOAMD64=v1';
return;
}
sub run_rc {
my ($cmd) = @_;
print "+ $cmd\n";
+4
View File
@@ -74,6 +74,10 @@ curl -fsSL "https://go.dev/dl/go${GO_PIN}.linux-${go_host_arch}.tar.gz" | tar -C
export PATH="$gotoolchain/bin:$PATH"
export GOTOOLCHAIN=local # use exactly the pinned toolchain; never auto-download another
export GOOS=linux GOARCH="$go_target_arch"
# GOAMD64 is set, never inherited from the chroot: a v3 binary stops a pre-AVX2 node with
# SIGILL, and one build serves every release of the family. debian/rules asserts the setting
# reached the compiler.
export GOAMD64=v1
go version
go env GOHOSTARCH GOARCH
+48 -5
View File
@@ -94,10 +94,20 @@ if ($command eq 'uname') {
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;
if ($ARGV[0] eq 'version') {
# The real compiler stamps the setting it was given into the binary. FIXTURE_GOAMD64 lets a
# case report a different one, which is what a compiler ignoring the request looks like.
die 'Unexpected go version arguments' unless @ARGV == 3 && $ARGV[1] eq '-m';
my $goarch = $ENV{GOARCH} || 'amd64';
my $setting = $ENV{FIXTURE_GOAMD64} || $ENV{GOAMD64} || '';
print "$ARGV[2]: go1.25.12\n\tpath\tcommand-line-arguments\n\tbuild\tGOARCH=$goarch\n";
print "\tbuild\tGOAMD64=$setting\n" if $goarch eq 'amd64' && length $setting;
} else {
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;
@@ -149,6 +159,7 @@ sub run_case {
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;
local $ENV{FIXTURE_GOAMD64} = $options{goamd64} || '';
my $pid = fork();
die $! unless defined $pid;
if (!$pid) {
@@ -165,6 +176,12 @@ sub run_case {
commands => \@commands };
}
sub spec_check {
my ($case) = @_;
my ($section) = $case->{spec} =~ /^%check\n(.*?)^%files$/ms;
return defined($section) ? $section : '';
}
sub calls {
my ($case, $command, $argument) = @_;
return [grep { $_->{command} eq $command && (!defined($argument) || grep { $_ eq $argument } @{$_->{args}}) } @{$case->{commands}}];
@@ -198,6 +215,10 @@ for my $cell (@cells) {
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");
# Each spec section runs its own shell, so %check has to put the private compiler back on PATH
# or it cannot read the metadata it gates on.
like(spec_check($case), qr/^export PATH=\Q%{_builddir}\E\/go\/bin:/m,
"$config puts the private compiler on PATH for the GOAMD64 gate");
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");
@@ -275,12 +296,16 @@ for my $row (
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");
}
# The in-chroot compile is gated inside the spec, where the compiler is. Nothing else runs it,
# so this holds the section down against a silent deletion.
like(spec_check($case), qr/go version -m \Q%{buildroot}\E\/usr\/bin\/\$bin/,
"EL$release gates the staged binaries on their own build metadata");
}
{
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');
my $go = calls($case, 'go', 'build');
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');
@@ -288,4 +313,22 @@ for my $row (
is(read_text("$case->{directory}/results/goconserver-0.3.3-4.riscv64.rpm"), "fixture binary RPM\n", 'EL cross path collects its package output');
}
# The published x86_64 goconserver was compiled GOAMD64=v3 and stopped a pre-AVX2 headnode with
# SIGILL. These two cases hold the gate down: it reads the setting from the binary, so it fails
# when the compiler ignored the request, and an export line alone cannot satisfy it.
{
my %cross = (config => 'rocky+epel-9-ppc64le', arch => 'ppc64le', target => 'x86_64');
my $case = run_case(%cross);
is($case->{status}, 0, 'amd64 cross build completes when the compiler honours GOAMD64=v1')
or diag($case->{output});
is(scalar @{calls($case, 'go', 'version')}, 2, 'the gate reads the metadata of both binaries');
my $bad = run_case(%cross, goamd64 => 'v3');
isnt($bad->{status}, 0, 'a v3 amd64 binary fails the build');
like($bad->{output}, qr/reports GOAMD64=v3, not GOAMD64=v1/,
'the diagnostic names the setting the binary carries');
ok(!-f "$bad->{directory}/results/goconserver-0.3.3-4.x86_64.rpm",
'a v3 amd64 binary is never packaged');
}
done_testing();
+55 -3
View File
@@ -54,7 +54,7 @@ sub write_stub {
# run_build($target_arch): run the build shell with the chroot's architecture reported as
# $target_arch, and return what it asked the outside world to do.
sub run_build {
my ($target_arch) = @_;
my ($target_arch, %options) = @_;
my $root = tempdir(CLEANUP => 1);
my ($bin, $rec, $work) = ("$root/bin", "$root/rec", "$root/work");
make_path($bin, $rec, $work);
@@ -98,9 +98,33 @@ sub run_build {
# The real debian/rules has to see the environment, so run its build target for real.
write_stub($bin, 'dpkg-buildpackage', 'make -f debian/rules override_dh_auto_build');
# The downloaded toolchain never lands, so `go` always resolves here. It records the environment
# of each invocation, which is the thing under test.
# of each invocation, which is the thing under test, and it stamps the GOAMD64 it was given into
# the binary it emits -- the real compiler does that, and debian/rules reads it back.
# FIXTURE_GOAMD64 makes it report a different setting, which is what a compiler that ignores the
# request looks like.
write_stub($bin, 'go', qq{
echo "GOARCH=\${GOARCH-} GOOS=\${GOOS-} ARGV=\$*" >> '$rec/go-calls'
echo "GOARCH=\${GOARCH-} GOOS=\${GOOS-} GOAMD64=\${GOAMD64-<unset>} ARGV=\$*" >> '$rec/go-calls'
case "\$1" in
build)
out=''; prev=''
for a in "\$\@"; do [ "\$prev" = -o ] && out="\$a"; prev="\$a"; done
[ -n "\$out" ] || exit 0
printf '#!/bin/sh\\nexit 0\\n' > "\$out"; chmod 755 "\$out"
{ printf '%s: go1.25.12\\n\\tpath\\tcommand-line-arguments\\n' "\$out"
printf '\\tbuild\\tGOARCH=%s\\n' "\${GOARCH-}"
[ "\${GOARCH-}" = amd64 ] &&
printf '\\tbuild\\tGOAMD64=%s\\n' "\${FIXTURE_GOAMD64:-\${GOAMD64-}}"
} > "\$out.meta"
;;
env)
for v in "\$\@"; do
case "\$v" in env) ;; GOARCH) echo "\${GOARCH-}";; *) echo '';; esac
done
;;
version)
if [ "\$2" = -m ]; then cat "\$3.meta"; else echo 'go version go1.25.12 linux/\${GOARCH-}'; fi
;;
esac
exit 0
});
@@ -108,8 +132,10 @@ sub run_build {
print {$fh} build_script();
close($fh);
my $fixture = $options{goamd64} || '';
my $rc = system('/bin/bash', '-c',
"cd '$work' && PATH=\"$bin:\$PATH\" SOURCE_DATE_EPOCH=1789413339 "
. "FIXTURE_GOAMD64='$fixture' "
. "bash '$root/build.sh' > '$root/build.log' 2>&1");
my $slurp = sub {
@@ -179,4 +205,30 @@ compiles_for($n, $host_go, 'a native cell compiles for its own architecture');
my $p = run_build('ppc64el');
compiles_for($p, 'ppc64le', 'the dpkg name ppc64el reaches go as ppc64le');
# ---- the x86-64 baseline the amd64 deb is compiled for -------------------------------------------
# The published x86_64 goconserver was compiled GOAMD64=v3 and stopped a pre-AVX2 headnode with
# SIGILL. debian/rules is where dpkg-buildpackage compiles, so the setting has to reach that file's
# own build target and the binary has to be read back. An export line is not evidence: the failure
# is the export not reaching the compiler, and a grep for the line passes when it does not.
{
my $a = run_build('amd64');
is($a->{rc}, 0, 'an amd64 cell builds when the compiler honours GOAMD64=v1')
or diag($a->{log});
my @builds = grep { /ARGV=.*\bbuild\b/ } @{ $a->{go_calls} };
is(scalar @builds, 2, 'the amd64 cell reaches both `go build` calls')
or diag(join("\n", @{ $a->{go_calls} }));
is_deeply([ grep { !/\bGOAMD64=v1\b/ } @builds ], [],
'every `go build` in the amd64 cell is told GOAMD64=v1')
or diag(join("\n", @builds));
is(scalar(grep { /ARGV=version -m / } @{ $a->{go_calls} }), 2,
'the gate reads the build metadata of both amd64 binaries')
or diag(join("\n", @{ $a->{go_calls} }));
my $bad = run_build('amd64', goamd64 => 'v3');
isnt($bad->{rc}, 0, 'a v3 amd64 binary fails the deb build');
like($bad->{log}, qr/reports 'GOAMD64=v3', not GOAMD64=v1/,
'the diagnostic names the setting the amd64 binary carries')
or diag($bad->{log});
}
done_testing;