1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
//go:build linux

package builder_test

import (
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/nixos/lix/gonix/builder"
)

// TestMain intercepts two re-exec cases before the test framework runs:
//
//  1. "__builder"      — the sandbox launcher (see LauncherMain).
//  2. "__builder_probe" — the user-namespace availability probe.
//
// Without this, the test binary re-exec'd by Spawn would fall through to the
// test runner and fail to act as the launcher.
func TestMain(m *testing.M) {
	// Must be first — launcher/probe exit before the runtime is fully set up.
	handleProbe()
	builder.MaybeLauncherMain()
	os.Exit(m.Run())
}

// TestSandboxTrivialBuild runs a dep-free build inside a user+mount namespace:
//
//	busybox sh -c 'echo hello > "$out"'
//
// It verifies that the output file is created with the expected content.
func TestSandboxTrivialBuild(t *testing.T) {
	if runtime.GOOS != "linux" {
		t.Skip("sandbox only on Linux")
	}
	userNSAvailable(t)

	bbDir := busyboxDir(t)

	// The output lands in a host temp dir that is bind-mounted at /out.
	outHostDir := t.TempDir()

	dirs, cleanup, err := builder.SetupChrootDir()
	if err != nil {
		t.Fatalf("SetupChrootDir: %v", err)
	}
	defer cleanup()

	spec := builder.BuildSpec{
		Builder: "/bin/sh",
		Args:    []string{"sh", "-c", `echo hello > "$out"`},
		Env: map[string]string{
			"out":  "/out/result",
			"PATH": "/bin",
		},
		System: "x86_64-linux",
		SandboxPaths: []builder.BoundPath{
			{Source: bbDir, Target: "/bin"},
			{Source: outHostDir, Target: "/out"},
		},
	}

	req, err := spec.ToRequest(dirs.Root, dirs.BuildDir)
	if err != nil {
		t.Fatalf("ToRequest: %v", err)
	}

	var stdout, stderr strings.Builder
	err = builder.Spawn(req, &builder.SpawnConfig{
		SelfExe:      selfExe(t),
		Unprivileged: true,
		Stdout:       &stdout,
		Stderr:       &stderr,
	})
	if stderr.Len() > 0 {
		t.Logf("builder stderr:\n%s", stderr.String())
	}
	if err != nil {
		t.Fatalf("Spawn: %v", err)
	}

	result := filepath.Join(outHostDir, "result")
	got, err := os.ReadFile(result)
	if err != nil {
		t.Fatalf("reading output %s: %v", result, err)
	}
	if want := "hello\n"; string(got) != want {
		t.Fatalf("output: got %q want %q", string(got), want)
	}
	t.Logf("output: %q ✓", string(got))
}

// selfExe returns the path to the test binary itself, which acts as the
// launcher because TestMain handles __builder.
func selfExe(t *testing.T) string {
	t.Helper()
	exe, err := os.Executable()
	if err != nil {
		t.Fatalf("os.Executable: %v", err)
	}
	return exe
}