From 4fa4ad34a0d8a801abe1751110ae1232276d13b3 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 8 Apr 2024 15:17:38 +0200 Subject: [PATCH] bib: detect missing qemu-user early This commit checks early if cross architecture building support via `qemu-user-static` (or similar tooling) is missing and errors in a more user friendly way. Note that there is no integration test right now because testing this for real requires mutating the very global state of `echo 0 > /proc/sys/fs/binfmt_misc/qemu-aarch64` which would make the test non-parallelizable and even risks failing other cross-arch tests running on the same host (because binfmt-misc is not namespaced (yet)). --- bib/internal/setup/export_test.go | 3 ++ bib/internal/setup/setup.go | 35 ++++++++++++++++- bib/internal/setup/setup_test.go | 63 +++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 bib/internal/setup/export_test.go create mode 100644 bib/internal/setup/setup_test.go diff --git a/bib/internal/setup/export_test.go b/bib/internal/setup/export_test.go new file mode 100644 index 0000000..2e50889 --- /dev/null +++ b/bib/internal/setup/export_test.go @@ -0,0 +1,3 @@ +package setup + +var ValidateCanRunTargetArch = validateCanRunTargetArch diff --git a/bib/internal/setup/setup.go b/bib/internal/setup/setup.go index 6801e14..16da129 100644 --- a/bib/internal/setup/setup.go +++ b/bib/internal/setup/setup.go @@ -3,10 +3,14 @@ package setup import ( "fmt" "os" + "os/exec" "path/filepath" + "runtime" "golang.org/x/sys/unix" + "github.com/sirupsen/logrus" + "github.com/osbuild/bootc-image-builder/bib/internal/podmanutil" "github.com/osbuild/bootc-image-builder/bib/internal/util" ) @@ -76,7 +80,7 @@ func EnsureEnvironment(storePath string) error { // Validate checks that the environment is supported (e.g. caller set up the // container correctly) -func Validate() error { +func Validate(targetArch string) error { isRootless, err := podmanutil.IsRootless() if err != nil { return fmt.Errorf("checking rootless: %w", err) @@ -95,6 +99,11 @@ func Validate() error { return fmt.Errorf("this command requires a privileged container") } + // Try to run the cross arch binary + if err := validateCanRunTargetArch(targetArch); err != nil { + return fmt.Errorf("cannot run binary in target arch: %w", err) + } + return nil } @@ -115,3 +124,27 @@ func ValidateHasContainerStorageMounted() error { } return nil } + +func validateCanRunTargetArch(targetArch string) error { + if targetArch == runtime.GOARCH || targetArch == "" { + return nil + } + + canaryCmd := fmt.Sprintf("bib-canary-%s", targetArch) + if _, err := exec.LookPath(canaryCmd); err != nil { + // we could error here but in principle with a working qemu-user + // any arch should work so let's just warn. the common case + // (arm64/amd64) is covered properly + logrus.Warningf("cannot check architecture support for %v: no canary binary found", targetArch) + return nil + } + output, err := exec.Command(canaryCmd).CombinedOutput() + if err != nil { + return fmt.Errorf("cannot run canary binary for %q, do you have 'qemu-user-static' installed?\n%s", targetArch, err) + } + if string(output) != "ok\n" { + return fmt.Errorf("internal error: unexpected output from cross-architecture canary: %q", string(output)) + } + + return nil +} diff --git a/bib/internal/setup/setup_test.go b/bib/internal/setup/setup_test.go new file mode 100644 index 0000000..bb57848 --- /dev/null +++ b/bib/internal/setup/setup_test.go @@ -0,0 +1,63 @@ +package setup_test + +import ( + "bytes" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/osbuild/bootc-image-builder/bib/internal/setup" +) + +func TestValidateCanRunTargetArchTrivial(t *testing.T) { + for _, arch := range []string{runtime.GOARCH, ""} { + err := setup.ValidateCanRunTargetArch(arch) + assert.NoError(t, err) + } +} + +func TestValidateCanRunTargetArchUnsupportedCanary(t *testing.T) { + var logbuf bytes.Buffer + logrus.SetOutput(&logbuf) + + err := setup.ValidateCanRunTargetArch("unsupported-arch") + assert.NoError(t, err) + assert.Contains(t, logbuf.String(), `level=warning msg="cannot check architecture support for unsupported-arch: no canary binary found"`) +} + +func makeFakeCanary(t *testing.T, content string) { + tmpdir := t.TempDir() + t.Setenv("PATH", os.Getenv("PATH")+":"+tmpdir) + err := os.WriteFile(filepath.Join(tmpdir, "bib-canary-fakearch"), []byte(content), 0755) + assert.NoError(t, err) +} + +func TestValidateCanRunTargetArchHappy(t *testing.T) { + var logbuf bytes.Buffer + logrus.SetOutput(&logbuf) + + makeFakeCanary(t, "#!/bin/sh\necho ok") + + err := setup.ValidateCanRunTargetArch("fakearch") + assert.NoError(t, err) + assert.Equal(t, "", logbuf.String()) +} + +func TestValidateCanRunTargetArchExecFormatError(t *testing.T) { + makeFakeCanary(t, "") + + err := setup.ValidateCanRunTargetArch("fakearch") + assert.ErrorContains(t, err, `cannot run canary binary for "fakearch", do you have 'qemu-user-static' installed?`) + assert.ErrorContains(t, err, `: exec format error`) +} + +func TestValidateCanRunTargetArchUnexpectedOutput(t *testing.T) { + makeFakeCanary(t, "#!/bin/sh\necho xxx") + + err := setup.ValidateCanRunTargetArch("fakearch") + assert.ErrorContains(t, err, `internal error: unexpected output`) +}