// 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 = "" for _, element := range strings.Split(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 ` CFBundleNamegolang.gotest CFBundleSupportedPlatformsiPhoneOS CFBundleExecutablegotest CFBundleVersion1.0 CFBundleShortVersionString1.0 CFBundleIdentifier` + bundleID + ` CFBundleResourceSpecificationResourceRules.plist LSRequiresIPhoneOS CFBundleDisplayNamegotest GoExecWrapperWorkingDirectory` + pkgpath + ` ` } func entitlementsPlist() string { return ` keychain-access-groups ` + appID + ` get-task-allow application-identifier ` + appID + ` com.apple.developer.team-identifier ` + teamID + ` ` } const resourceRules = ` rules .* Info.plist omit weight 10 ResourceRules.plist omit weight 100 `