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:
+48
-20
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user