diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index 9d47f94..dc7d2b6 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -58,6 +58,7 @@ jobs: prove -v t/build_utils.t prove -v t/build_timeout.t prove -v t/sbuild-all.t + prove -v t/goconserver_cross_build.t prove -v t/mockbuild-all.t prove -v -It/lib t/genesis_openembedded_release.t sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t diff --git a/goconserver/sbuild.pl b/goconserver/sbuild.pl index cf509cb..6c77988 100755 --- a/goconserver/sbuild.pl +++ b/goconserver/sbuild.pl @@ -44,6 +44,8 @@ $build_timestamp = time() unless defined $build_timestamp; # The maintained debian/ is at ./debian in the copied package dir; the upstream source is cloned fresh # at the pinned SHA into ./gcsrc, the maintained debian/ copied in, and dpkg-buildpackage run there # (its .deb(s) land in the copied package dir, which the collector picks up). +# The build script below is lifted by t/goconserver_cross_build.t and run with the commands it +# calls shadowed. Keep the marker: the test dies when it can no longer find this region. my $build = <<'BUILD'; set -e VERSION=0.3.3 diff --git a/t/goconserver_cross_build.t b/t/goconserver_cross_build.t new file mode 100644 index 0000000..5fea87e --- /dev/null +++ b/t/goconserver_cross_build.t @@ -0,0 +1,182 @@ +#!/usr/bin/perl +# Behaviour test for the Go toolchain the Ubuntu goconserver build uses. +# +# riscv64 has no build host, so its chroot runs under qemu-user. A Go toolchain built FOR riscv64 +# therefore runs emulated, and `go build` parks its threads in futex_wait and never finishes: three +# xcat-dep-ubuntu-cd riscv64 cells burned the whole 9000s budget with no CPU ticks at all. Go +# cross-compiles, so the toolchain must be the BUILD HOST's and the target must come from GOARCH. +# +# The test LIFTS the build shell out of goconserver/sbuild.pl, RUNS it, and asserts on what the run +# asked for -- the toolchain tarball it fetched, and the environment the real debian/rules passed to +# `go build`. It never matches the source of the thing it tests. +# +# Every command that could write outside the scratch tree is shadowed by a recorder that refuses the +# write and reports it, so a build that reaches for /usr/local is a FAILED assertion here rather than +# damage to the host running the suite. +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); +use File::Path qw(make_path); +use FindBin qw($RealBin); + +my $pkg_dir = "$RealBin/../goconserver"; +plan skip_all => 'goconserver/sbuild.pl not found' unless -f "$pkg_dir/sbuild.pl"; +plan skip_all => 'bash is not available' unless -x '/bin/bash'; + +my $LIFT = 'lifted by t/goconserver_cross_build.t'; + +# The build shell, produced by the real builder code. The whole marked region is evaluated, so the +# architecture the builder stamps into the script comes from the builder rather than from this test. +# die (never BAIL_OUT) when the lift stops matching: prove stops the WHOLE suite on a bail-out, and a +# silent miss would leave this file covering nothing. +sub build_script { + open(my $fh, '<', "$pkg_dir/sbuild.pl") or die "read sbuild.pl: $!"; + my $src = do { local $/; <$fh> }; + close($fh); + my ($region) = $src =~ /^\#[^\n]*\Q$LIFT\E[^\n]*\n(.*?^BUILD$)/ms + or die "goconserver/sbuild.pl no longer marks its build script with '$LIFT' -- " + . "this test can no longer reach the code it covers\n"; + my $build = eval "$region\n\$build"; ## no critic + die "could not evaluate the lifted build script: $@\n" if $@; + die "the lifted build script is empty\n" unless defined $build && $build =~ /\S/; + return $build; +} + +sub write_stub { + my ($dir, $name, $body) = @_; + open(my $fh, '>', "$dir/$name") or die "write $dir/$name: $!"; + print {$fh} "#!/bin/bash\n$body\n"; + close($fh); + chmod(0755, "$dir/$name") or die "chmod $dir/$name: $!"; +} + +# 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 $root = tempdir(CLEANUP => 1); + my ($bin, $rec, $work) = ("$root/bin", "$root/rec", "$root/work"); + make_path($bin, $rec, $work); + + # The build runs with CWD = a copy of the package dir, and reads ../gomod and ./debian from it. + system('cp', '-rL', "$pkg_dir/$_", "$work/$_") == 0 or die "stage $_: $!" for qw(gomod debian); + + # dpkg answers for the CHROOT, which is the architecture the build must produce. + write_stub($bin, 'dpkg', qq{ + [ "\$1" = --print-architecture ] && { echo '$target_arch'; exit 0; } + exec /usr/bin/dpkg "\$\@" + }); + write_stub($bin, 'curl', qq{ + for a in "\$\@"; do case "\$a" in http*) echo "\$a" >> '$rec/curl-urls';; esac; done + exit 0 + }); + # tar and rm police their target: anything outside the scratch tree is recorded, not performed. + write_stub($bin, 'tar', qq{ + dest=''; prev='' + for a in "\$\@"; do [ "\$prev" = -C ] && dest="\$a"; prev="\$a"; done + case "\$dest" in '$root'/*) ;; *) echo "tar -C \$dest" >> '$rec/escapes';; esac + exit 0 + }); + write_stub($bin, 'rm', qq{ + for a in "\$\@"; do + case "\$a" in + -*|'$root'/*) ;; + /*) echo "rm \$a" >> '$rec/escapes'; exit 0;; + esac + done + exec /bin/rm "\$\@" + }); + # Only `git init