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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Builder: a static busybox for the tolino, compiled with `zig cc`, so that
// `adb shell` has a shell and the everyday tools (ls, cat, ps, dmesg,
// mount, df). Nothing in the system runs it; only adbd's shell service.
//
// Inputs: zig, src (busybox tarball), target, cpu, sh, toolPath, plus
// gnutar/bzip2/gnumake.
package main

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

	"drv"
)

func main() {
	top := drv.Env("NIX_BUILD_TOP")
	zig := drv.Env("zig") + "/bin/zig"
	sh := drv.Env("sh")
	cache := filepath.Join(top, "zig-cache")
	os.Setenv("HOME", top)
	os.Setenv("ZIG_GLOBAL_CACHE_DIR", cache)
	os.Setenv("ZIG_LOCAL_CACHE_DIR", cache)
	os.Setenv("PATH", drv.Env("toolPath"))

	drv.RunIn(top, drv.Env("gnutar")+"/bin/tar", "-xjf", drv.Env("src"))
	dirs := drv.Glob(top + "/busybox-*")
	if len(dirs) != 1 {
		drv.Fatalf("expected one busybox-* directory, got %v", dirs)
	}
	dir := dirs[0]

	wrap := filepath.Join(top, "wrap")
	target := "-target " + drv.Env("target") + " -mcpu=" + drv.Env("cpu")
	// busybox's scripts/trylink passes GNU ld options zig's linker (lld)
	// rejects outright; they only affect diagnostics and padding, so drop
	// them. Everything else passes through.
	drv.WriteShebang(wrap+"/cc", sh,
		`for a; do`,
		`  shift`,
		`  case "$a" in`,
		`    -Wl,--warn-common|-Wl,--verbose|-Wl,-Map,*|-Wl,--sort-common|-Wl,--sort-section,*) ;;`,
		`    *) set -- "$@" "$a" ;;`,
		`  esac`,
		`done`,
		`exec `+zig+` cc `+target+` "$@"`)
	drv.WriteShebang(wrap+"/ar", sh, `exec `+zig+` ar "$@"`)
	drv.WriteShebang(wrap+"/strip", sh, `exit 0`)
	// The host compiler, for busybox's build-time helpers (fixdep, kconfig).
	drv.WriteShebang(wrap+"/hostcc", sh, `exec `+zig+` cc "$@"`)

	mk := func(args ...string) {
		jobs := drv.Env("NIX_BUILD_CORES")
		if jobs == "0" {
			jobs = strconv.Itoa(runtime.NumCPU())
		}
		base := []string{"-j" + jobs,
			"CC=" + wrap + "/cc", "AR=" + wrap + "/ar", "STRIP=" + wrap + "/strip",
			"HOSTCC=" + wrap + "/hostcc",
			"CROSS_COMPILE=",
			// Already stripped at link time (-s in EXTRA_CFLAGS); the strip
			// stub would not produce the copy the Makefile expects.
			"SKIP_STRIP=y",
		}
		drv.RunIn(dir, drv.Env("gnumake")+"/bin/make", append(base, args...)...)
	}

	mk("defconfig")
	// Static, size-optimised, and without what does not build against
	// musl or is meaningless here.
	conf := filepath.Join(dir, ".config")
	b, err := os.ReadFile(conf)
	drv.Check(err)
	c := string(b)
	set := func(key, val string) {
		lines := strings.Split(c, "\n")
		found := false
		for i, l := range lines {
			if strings.HasPrefix(l, key+"=") || l == "# "+key+" is not set" {
				lines[i] = key + "=" + val
				if val == "n" {
					lines[i] = "# " + key + " is not set"
				}
				found = true
			}
		}
		if !found {
			lines = append(lines, key+"="+val)
		}
		c = strings.Join(lines, "\n")
	}
	set("CONFIG_STATIC", "y")
	// BB_GLOBAL_CONST: busybox declares its globals pointers (ptr_to_globals
	// and friends) const, to let gcc keep them in a register, and assigns
	// them once at applet start. zig cc is clang, which then reuses a load of
	// the pointer from before the assignment: top crashed reading NULL+0x118.
	// Empty makes them plain pointers.
	set("CONFIG_EXTRA_CFLAGS", `"-Os -s -DBB_GLOBAL_CONST="`)
	// tc and the console-tools ioctls need kernel headers musl/zig lack.
	for _, k := range []string{"CONFIG_TC", "CONFIG_FEATURE_TC_INGRESS", "CONFIG_SELINUX",
		"CONFIG_PAM", "CONFIG_FEATURE_HAVE_RPC", "CONFIG_FEATURE_INETD_RPC",
		"CONFIG_FEATURE_SYSTEMD", "CONFIG_FEATURE_MOUNT_NFS",
		// x86 SHA-NI code paths: defconfig enables them for the build host.
		"CONFIG_SHA1_HWACCEL", "CONFIG_SHA256_HWACCEL"} {
		set(k, "n")
	}
	drv.Write(conf, c, 0o644)
	mk("oldconfig")
	mk("busybox")

	out := drv.Out()
	drv.Mkdir(out + "/bin")
	drv.CopyFile(dir+"/busybox", out+"/bin/busybox", 0o755)
	// The applet list, one name per line, for the system builder to link.
	// From the build's busybox.links: the binary is ARM and cannot run here.
	mk("busybox.links")
	al, err := os.ReadFile(dir + "/busybox.links")
	drv.Check(err)
	var names []string
	for _, l := range strings.Fields(string(al)) {
		names = append(names, filepath.Base(l))
	}
	drv.Write(out+"/applets", strings.Join(names, "\n")+"\n", 0o644)
}