go/misc/ios/go_ios_exec.go

367 lines
8.7 KiB
Go
Raw Normal View History

// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This program can be used as go_ios_$GOARCH_exec by the Go tool. It executes
// binaries on the iOS Simulator using the XCode toolchain.
package main
import (
"fmt"
"go/build"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
)
const debug = false
var tmpdir string
var (
devID string
appID string
teamID string
bundleID string
deviceID string
)
// lock is a file lock to serialize iOS runs. It is global to avoid the
// garbage collector finalizing it, closing the file and releasing the
// lock prematurely.
var lock *os.File
func main() {
log.SetFlags(0)
log.SetPrefix("go_ios_exec: ")
if debug {
log.Println(strings.Join(os.Args, " "))
}
if len(os.Args) < 2 {
log.Fatal("usage: go_ios_exec a.out")
}
// For compatibility with the old builders, use a fallback bundle ID
bundleID = "golang.gotest"
exitCode, err := runMain()
if err != nil {
log.Fatalf("%v\n", err)
}
os.Exit(exitCode)
}
func runMain() (int, error) {
var err error
tmpdir, err = os.MkdirTemp("", "go_ios_exec_")
if err != nil {
return 1, err
}
if !debug {
defer os.RemoveAll(tmpdir)
}
appdir := filepath.Join(tmpdir, "gotest.app")
os.RemoveAll(appdir)
if err := assembleApp(appdir, os.Args[1]); err != nil {
return 1, err
}
// This wrapper uses complicated machinery to run iOS binaries. It
// works, but only when running one binary at a time.
// Use a file lock to make sure only one wrapper is running at a time.
//
// The lock file is never deleted, to avoid concurrent locks on distinct
// files with the same path.
lockName := filepath.Join(os.TempDir(), "go_ios_exec-"+deviceID+".lock")
lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666)
if err != nil {
return 1, err
}
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
return 1, err
}
err = runOnSimulator(appdir)
if err != nil {
return 1, err
}
return 0, nil
}
func runOnSimulator(appdir string) error {
if err := installSimulator(appdir); err != nil {
return err
}
return runSimulator(appdir, bundleID, os.Args[2:])
}
func assembleApp(appdir, bin string) error {
if err := os.MkdirAll(appdir, 0755); err != nil {
return err
}
if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
return err
}
pkgpath, err := copyLocalData(appdir)
if err != nil {
return err
}
entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
if err := os.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist(pkgpath)), 0744); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
return err
}
return nil
}
func installSimulator(appdir string) error {
cmd := exec.Command(
"xcrun", "simctl", "install",
"booted", // Install to the booted simulator.
appdir,
)
if out, err := cmd.CombinedOutput(); err != nil {
os.Stderr.Write(out)
return fmt.Errorf("xcrun simctl install booted %q: %v", appdir, err)
}
return nil
}
func runSimulator(appdir, bundleID string, args []string) error {
xcrunArgs := []string{"simctl", "spawn",
"booted",
appdir + "/gotest",
}
xcrunArgs = append(xcrunArgs, args...)
cmd := exec.Command("xcrun", xcrunArgs...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("xcrun simctl launch booted %q: %v", bundleID, err)
}
return nil
}
func copyLocalDir(dst, src string) error {
if err := os.Mkdir(dst, 0755); err != nil {
return err
}
d, err := os.Open(src)
if err != nil {
return err
}
defer d.Close()
fi, err := d.Readdir(-1)
if err != nil {
return err
}
for _, f := range fi {
if f.IsDir() {
if f.Name() == "testdata" {
if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
return err
}
}
continue
}
if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
return err
}
}
return nil
}
func cp(dst, src string) error {
out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
if err != nil {
os.Stderr.Write(out)
}
return err
}
func copyLocalData(dstbase string) (pkgpath string, err error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
finalPkgpath, underGoRoot, err := subdir()
if err != nil {
return "", err
}
cwd = strings.TrimSuffix(cwd, finalPkgpath)
// Copy all immediate files and testdata directories between
// the package being tested and the source root.
pkgpath = ""
all: replace strings.Split with strings.SplitSeq In Go 1.25+, strings.SplitSeq offers better performance. Here are the benchmark results comparing strings.Split and strings.SplitSeq in a for-loop, with the benchmark code located in src/strings/iter_test.go: goos: darwin goarch: amd64 pkg: cmd/go/internal/auth cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ ParseGitAuth/standard-8 281.4n ± 1% 218.0n ± 11% -22.54% (p=0.000 n=10) ParseGitAuth/with_url-8 549.1n ± 1% 480.5n ± 13% -12.48% (p=0.002 n=10) ParseGitAuth/minimal-8 235.4n ± 1% 197.3n ± 7% -16.20% (p=0.000 n=10) ParseGitAuth/complex-8 797.6n ± 2% 805.2n ± 4% ~ (p=0.481 n=10) ParseGitAuth/empty-8 87.48n ± 3% 63.25n ± 6% -27.71% (p=0.000 n=10) ParseGitAuth/malformed-8 228.8n ± 1% 171.2n ± 3% -25.17% (p=0.000 n=10) geomean 288.9n 237.7n -17.72% │ old.txt │ new.txt │ │ B/op │ B/op vs base │ ParseGitAuth/standard-8 192.00 ± 0% 96.00 ± 0% -50.00% (p=0.000 n=10) ParseGitAuth/with_url-8 400.0 ± 0% 288.0 ± 0% -28.00% (p=0.000 n=10) ParseGitAuth/minimal-8 144.00 ± 0% 80.00 ± 0% -44.44% (p=0.000 n=10) ParseGitAuth/complex-8 528.0 ± 0% 400.0 ± 0% -24.24% (p=0.000 n=10) ParseGitAuth/empty-8 32.00 ± 0% 16.00 ± 0% -50.00% (p=0.000 n=10) ParseGitAuth/malformed-8 176.00 ± 0% 80.00 ± 0% -54.55% (p=0.000 n=10) geomean 179.0 102.1 -42.96% │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ ParseGitAuth/standard-8 3.000 ± 0% 2.000 ± 0% -33.33% (p=0.000 n=10) ParseGitAuth/with_url-8 4.000 ± 0% 3.000 ± 0% -25.00% (p=0.000 n=10) ParseGitAuth/minimal-8 3.000 ± 0% 2.000 ± 0% -33.33% (p=0.000 n=10) ParseGitAuth/complex-8 4.000 ± 0% 3.000 ± 0% -25.00% (p=0.000 n=10) ParseGitAuth/empty-8 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.000 n=10) ParseGitAuth/malformed-8 3.000 ± 0% 2.000 ± 0% -33.33% (p=0.000 n=10) geomean 3.086 2.040 -33.91% Updates #69315. Change-Id: Id0219edea45d9658d527b863162ebe917e7821d9 GitHub-Last-Rev: 392b315e122f2c9ef8703ca2dbce8f82ec198556 GitHub-Pull-Request: golang/go#75259 Reviewed-on: https://go-review.googlesource.com/c/go/+/701015 Reviewed-by: Keith Randall <khr@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Knyszek <mknyszek@google.com> Reviewed-by: Emmanuel Odeke <emmanuel@orijtech.com> Reviewed-by: Keith Randall <khr@google.com> Auto-Submit: Emmanuel Odeke <emmanuel@orijtech.com>
2025-09-15 14:39:58 +00:00
for element := range strings.SplitSeq(finalPkgpath, string(filepath.Separator)) {
if debug {
log.Printf("copying %s", pkgpath)
}
pkgpath = filepath.Join(pkgpath, element)
dst := filepath.Join(dstbase, pkgpath)
src := filepath.Join(cwd, pkgpath)
if err := copyLocalDir(dst, src); err != nil {
return "", err
}
}
if underGoRoot {
// Copy timezone file.
//
// Typical apps have the zoneinfo.zip in the root of their app bundle,
// read by the time package as the working directory at initialization.
// As we move the working directory to the GOROOT pkg directory, we
// install the zoneinfo.zip file in the pkgpath.
err := cp(
filepath.Join(dstbase, pkgpath),
filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
)
if err != nil {
return "", err
}
// Copy src/runtime/textflag.h for (at least) Test386EndToEnd in
// cmd/asm/internal/asm.
runtimePath := filepath.Join(dstbase, "src", "runtime")
if err := os.MkdirAll(runtimePath, 0755); err != nil {
return "", err
}
err = cp(
filepath.Join(runtimePath, "textflag.h"),
filepath.Join(cwd, "src", "runtime", "textflag.h"),
)
if err != nil {
return "", err
}
}
return finalPkgpath, nil
}
// subdir determines the package based on the current working directory,
// and returns the path to the package source relative to $GOROOT (or $GOPATH).
func subdir() (pkgpath string, underGoRoot bool, err error) {
cwd, err := os.Getwd()
if err != nil {
return "", false, err
}
cwd, err = filepath.EvalSymlinks(cwd)
if err != nil {
log.Fatal(err)
}
goroot, err := filepath.EvalSymlinks(runtime.GOROOT())
if err != nil {
return "", false, err
}
if strings.HasPrefix(cwd, goroot) {
subdir, err := filepath.Rel(goroot, cwd)
if err != nil {
return "", false, err
}
return subdir, true, nil
}
for _, p := range filepath.SplitList(build.Default.GOPATH) {
pabs, err := filepath.EvalSymlinks(p)
if err != nil {
return "", false, err
}
if !strings.HasPrefix(cwd, pabs) {
continue
}
subdir, err := filepath.Rel(pabs, cwd)
if err == nil {
return subdir, false, nil
}
}
return "", false, fmt.Errorf(
"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
cwd,
runtime.GOROOT(),
build.Default.GOPATH,
)
}
func infoPlist(pkgpath string) string {
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>golang.gotest</string>
<key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
<key>CFBundleExecutable</key><string>gotest</string>
<key>CFBundleVersion</key><string>1.0</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleIdentifier</key><string>` + bundleID + `</string>
<key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
<key>LSRequiresIPhoneOS</key><true/>
<key>CFBundleDisplayName</key><string>gotest</string>
<key>GoExecWrapperWorkingDirectory</key><string>` + pkgpath + `</string>
</dict>
</plist>
`
}
func entitlementsPlist() string {
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array><string>` + appID + `</string></array>
<key>get-task-allow</key>
<true/>
<key>application-identifier</key>
<string>` + appID + `</string>
<key>com.apple.developer.team-identifier</key>
<string>` + teamID + `</string>
</dict>
</plist>
`
}
const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>rules</key>
<dict>
<key>.*</key>
<true/>
<key>Info.plist</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<integer>10</integer>
</dict>
<key>ResourceRules.plist</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<integer>100</integer>
</dict>
</dict>
</dict>
</plist>
`