Detect the base binary's OS when packing a cross-platform viewer

Packing with --base pointed at a kage built for another OS used to guess the
target from the file name: a base ending in .exe meant Windows, anything else
meant the host. That misfired in both directions. A Windows base without the
.exe suffix produced a viewer with no extension that will not run on Windows,
and an --out name that dropped .exe made the run hint print a macOS quarantine
note for a Windows file.

Sniff the base's executable header (ELF, PE, Mach-O) instead, so the target OS
comes from the bytes rather than the name. A Windows viewer now always gets a
.exe suffix, and the run hint names the real target and only mentions Gatekeeper
for actual macOS viewers.
This commit is contained in:
Duc-Tam Nguyen
2026-06-15 00:15:16 +07:00
parent c4895d5b92
commit d81b90b38c
3 changed files with 145 additions and 20 deletions
+48 -20
View File
@@ -79,16 +79,22 @@ func runPack(mirrorArg string, f *packFlags) error {
if err != nil {
return err
}
target := resolveTargetOS(f.base)
out := f.out
if out == "" {
out = defaultBinaryName(dir, f.base)
out = defaultBinaryName(dir)
}
// A Windows viewer must end in .exe to run, whether the name came from
// --out or the default, so make sure it does.
if target == "windows" && !strings.HasSuffix(strings.ToLower(out), ".exe") {
out += ".exe"
}
path, size, err := pack.BuildBinary(zbytes, pack.BinaryOptions{Out: out, Base: f.base})
if err != nil {
return err
}
printPackResult(path, size)
printRunHint(path)
printRunHint(path, target)
return nil
default:
@@ -110,27 +116,32 @@ func resolveMirror(arg string) string {
return arg
}
// defaultBinaryName derives a clean program name from the mirror's host: strip a
// trailing dot-suffix (paulgraham.com -> paulgraham), and append .exe when the
// target is Windows. The target is the running OS unless --base names a binary
// that looks like it was built for Windows.
func defaultBinaryName(dir, base string) string {
// defaultBinaryName derives a clean program name from the mirror's host by
// stripping a trailing dot-suffix (paulgraham.com -> paulgraham). The caller
// appends .exe for Windows targets.
func defaultBinaryName(dir string) string {
host := filepath.Base(dir)
name := host
if i := strings.IndexByte(host, '.'); i > 0 {
name = host[:i]
return host[:i]
}
if windowsTarget(base) {
name += ".exe"
}
return name
return host
}
func windowsTarget(base string) bool {
// resolveTargetOS reports which OS the packed viewer will run on. With no
// --base it is this kage's OS; with one, we sniff the base's executable header
// so detection does not hinge on the file being named ".exe". If the header is
// unrecognised we fall back to that name heuristic.
func resolveTargetOS(base string) string {
if base == "" {
return runtime.GOOS == "windows"
return runtime.GOOS
}
return strings.HasSuffix(strings.ToLower(base), ".exe")
if os := pack.SniffOS(base); os != "" {
return os
}
if strings.HasSuffix(strings.ToLower(base), ".exe") {
return "windows"
}
return ""
}
func printPackResult(path string, size int64) {
@@ -138,21 +149,38 @@ func printPackResult(path string, size int64) {
fmt.Fprintf(os.Stderr, " %s %s\n", styleAccent.Render("size"), humanBytes(size))
}
func printRunHint(path string) {
func printRunHint(path, target string) {
rel := path
if !strings.ContainsAny(path, "/\\") {
rel = "./" + path
}
if windowsTarget(path) && runtime.GOOS != "windows" {
fmt.Fprintf(os.Stderr, " this is a Windows viewer; run %s on Windows\n", styleAccent.Render(filepath.Base(path)))
// A viewer built for another OS cannot run here, so say where it goes
// instead of printing a run command that would not work.
if target != "" && target != runtime.GOOS {
fmt.Fprintf(os.Stderr, " this is a %s viewer; copy %s to that machine to run it\n",
osLabel(target), styleAccent.Render(filepath.Base(path)))
return
}
fmt.Fprintf(os.Stderr, " run %s to view the site offline\n", styleAccent.Render(rel))
if runtime.GOOS == "darwin" {
if target == "darwin" {
fmt.Fprintln(os.Stderr, styleDim.Render(" (macOS may quarantine it: xattr -d com.apple.quarantine "+rel+")"))
}
}
// osLabel turns a GOOS value into a friendly name for the run hint.
func osLabel(goos string) string {
switch goos {
case "windows":
return "Windows"
case "darwin":
return "macOS"
case "linux":
return "Linux"
default:
return goos
}
}
// humanBytes renders a byte count in B, KiB, MiB, or GiB.
func humanBytes(n int64) string {
const unit = 1024
+57
View File
@@ -0,0 +1,57 @@
package pack
import "os"
// Executable-format magic numbers, enough to tell the three desktop targets
// apart by their first bytes. We only need the family (windows, darwin, linux),
// not the architecture, so the smallest distinguishing prefix is plenty.
var (
magicELF = []byte{0x7f, 'E', 'L', 'F'} // Linux, FreeBSD, and other ELF systems
magicPE = []byte{'M', 'Z'} // Windows PE/COFF (DOS stub header)
machOLE64 = []byte{0xcf, 0xfa, 0xed, 0xfe} // Mach-O 64-bit, little-endian (amd64, arm64)
machOLE32 = []byte{0xce, 0xfa, 0xed, 0xfe} // Mach-O 32-bit, little-endian
machOBE64 = []byte{0xfe, 0xed, 0xfa, 0xcf} // Mach-O 64-bit, big-endian
machOBE32 = []byte{0xfe, 0xed, 0xfa, 0xce} // Mach-O 32-bit, big-endian
machOFat = []byte{0xca, 0xfe, 0xba, 0xbe} // Mach-O universal (fat) binary
)
// SniffOS reads the first bytes of an executable and returns the GOOS family it
// was built for: "windows", "darwin", "linux", or "" when the bytes match none
// of them. It is how pack decides whether a cross-built viewer needs a .exe
// suffix and which run hint to print, without trusting the base's file name.
func SniffOS(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer func() { _ = f.Close() }()
head := make([]byte, 4)
if _, err := f.ReadAt(head, 0); err != nil {
return ""
}
switch {
case hasPrefix(head, magicPE):
return "windows"
case hasPrefix(head, magicELF):
return "linux"
case hasPrefix(head, machOLE64), hasPrefix(head, machOLE32),
hasPrefix(head, machOBE64), hasPrefix(head, machOBE32),
hasPrefix(head, machOFat):
return "darwin"
default:
return ""
}
}
func hasPrefix(b, prefix []byte) bool {
if len(b) < len(prefix) {
return false
}
for i := range prefix {
if b[i] != prefix[i] {
return false
}
}
return true
}
+40
View File
@@ -0,0 +1,40 @@
package pack
import (
"os"
"path/filepath"
"testing"
)
func TestSniffOS(t *testing.T) {
cases := []struct {
name string
head []byte
want string
}{
{"elf", []byte{0x7f, 'E', 'L', 'F', 0x02, 0x01}, "linux"},
{"pe", []byte{'M', 'Z', 0x90, 0x00}, "windows"},
{"macho-le64", []byte{0xcf, 0xfa, 0xed, 0xfe}, "darwin"},
{"macho-le32", []byte{0xce, 0xfa, 0xed, 0xfe}, "darwin"},
{"macho-be64", []byte{0xfe, 0xed, 0xfa, 0xcf}, "darwin"},
{"macho-fat", []byte{0xca, 0xfe, 0xba, 0xbe}, "darwin"},
{"unknown", []byte{0x00, 0x01, 0x02, 0x03}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
p := filepath.Join(t.TempDir(), tc.name)
if err := os.WriteFile(p, tc.head, 0o644); err != nil {
t.Fatal(err)
}
if got := SniffOS(p); got != tc.want {
t.Errorf("SniffOS(%s) = %q, want %q", tc.name, got, tc.want)
}
})
}
}
func TestSniffOSMissingFile(t *testing.T) {
if got := SniffOS(filepath.Join(t.TempDir(), "nope")); got != "" {
t.Errorf("SniffOS(missing) = %q, want empty", got)
}
}