Skip to content

Commit

Permalink
bib: detect missing qemu-user early
Browse files Browse the repository at this point in the history
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)).
  • Loading branch information
mvo5 authored and ondrejbudai committed May 2, 2024
1 parent 61ef43a commit 795ef0c
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ FROM registry.fedoraproject.org/fedora:39
COPY ./group_osbuild-osbuild-fedora-39.repo /etc/yum.repos.d/
COPY ./package-requires.txt .
RUN grep -vE '^#' package-requires.txt | xargs dnf install -y && rm -f package-requires.txt && dnf clean all
COPY --from=builder /build/bin/bootc-image-builder /usr/bin/bootc-image-builder
COPY --from=builder /build/bin/* /usr/bin/
COPY entrypoint.sh /
COPY bib/data /usr/share/bootc-image-builder

Expand Down
2 changes: 1 addition & 1 deletion bib/cmd/bootc-image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error {
targetArch, _ := cmd.Flags().GetString("target-arch")

logrus.Debug("Validating environment")
if err := setup.Validate(); err != nil {
if err := setup.Validate(targetArch); err != nil {
return fmt.Errorf("cannot validate the setup: %w", err)
}
logrus.Debug("Ensuring environment setup")
Expand Down
5 changes: 5 additions & 0 deletions bib/cmd/cross-arch/canary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

func main() {
println("ok")
}
3 changes: 3 additions & 0 deletions bib/internal/setup/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package setup

var ValidateCanRunTargetArch = validateCanRunTargetArch
35 changes: 34 additions & 1 deletion bib/internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand All @@ -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
}
63 changes: 63 additions & 0 deletions bib/internal/setup/setup_test.go
Original file line number Diff line number Diff line change
@@ -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`)
}
14 changes: 14 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,17 @@ CONTAINERS_STORAGE_THIN_TAGS="containers_image_openpgp exclude_graphdriver_btrfs
cd bib
set -x
go build -tags "${CONTAINERS_STORAGE_THIN_TAGS}" -o ../bin/bootc-image-builder ./cmd/bootc-image-builder

# expand the list as we support more architectures
for arch in amd64 arm64; do
if [ "$arch" = "$(go env GOARCH)" ]; then
continue
fi

# what is slightly sad is that this generates a 1MB file. Fedora does
# not have a cross gcc that can cross build userspace otherwise something
# like: `void _start() { syscall(SYS_exit() }` would work with
# `gcc -static -static-libgcc -nostartfiles -nostdlib -l` and give us a 10k
# cross platform binary. Or maybe no-std rust (thanks Colin)?
GOARCH="$arch" go build -ldflags="-s -w" -o ../bin/bib-canary-"$arch" ./cmd/cross-arch/
done

0 comments on commit 795ef0c

Please sign in to comment.