Source file misc/ios/go_ios_exec.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // This program can be used as go_ios_$GOARCH_exec by the Go tool. It executes
     6  // binaries on the iOS Simulator using the XCode toolchain.
     7  package main
     8  
     9  import (
    10  	"fmt"
    11  	"go/build"
    12  	"log"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"runtime"
    17  	"strings"
    18  	"syscall"
    19  )
    20  
    21  const debug = false
    22  
    23  var tmpdir string
    24  
    25  var (
    26  	devID    string
    27  	appID    string
    28  	teamID   string
    29  	bundleID string
    30  	deviceID string
    31  )
    32  
    33  // lock is a file lock to serialize iOS runs. It is global to avoid the
    34  // garbage collector finalizing it, closing the file and releasing the
    35  // lock prematurely.
    36  var lock *os.File
    37  
    38  func main() {
    39  	log.SetFlags(0)
    40  	log.SetPrefix("go_ios_exec: ")
    41  	if debug {
    42  		log.Println(strings.Join(os.Args, " "))
    43  	}
    44  	if len(os.Args) < 2 {
    45  		log.Fatal("usage: go_ios_exec a.out")
    46  	}
    47  
    48  	// For compatibility with the old builders, use a fallback bundle ID
    49  	bundleID = "golang.gotest"
    50  
    51  	exitCode, err := runMain()
    52  	if err != nil {
    53  		log.Fatalf("%v\n", err)
    54  	}
    55  	os.Exit(exitCode)
    56  }
    57  
    58  func runMain() (int, error) {
    59  	var err error
    60  	tmpdir, err = os.MkdirTemp("", "go_ios_exec_")
    61  	if err != nil {
    62  		return 1, err
    63  	}
    64  	if !debug {
    65  		defer os.RemoveAll(tmpdir)
    66  	}
    67  
    68  	appdir := filepath.Join(tmpdir, "gotest.app")
    69  	os.RemoveAll(appdir)
    70  
    71  	if err := assembleApp(appdir, os.Args[1]); err != nil {
    72  		return 1, err
    73  	}
    74  
    75  	// This wrapper uses complicated machinery to run iOS binaries. It
    76  	// works, but only when running one binary at a time.
    77  	// Use a file lock to make sure only one wrapper is running at a time.
    78  	//
    79  	// The lock file is never deleted, to avoid concurrent locks on distinct
    80  	// files with the same path.
    81  	lockName := filepath.Join(os.TempDir(), "go_ios_exec-"+deviceID+".lock")
    82  	lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666)
    83  	if err != nil {
    84  		return 1, err
    85  	}
    86  	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
    87  		return 1, err
    88  	}
    89  
    90  	err = runOnSimulator(appdir)
    91  	if err != nil {
    92  		return 1, err
    93  	}
    94  	return 0, nil
    95  }
    96  
    97  func runOnSimulator(appdir string) error {
    98  	if err := installSimulator(appdir); err != nil {
    99  		return err
   100  	}
   101  
   102  	return runSimulator(appdir, bundleID, os.Args[2:])
   103  }
   104  
   105  func assembleApp(appdir, bin string) error {
   106  	if err := os.MkdirAll(appdir, 0755); err != nil {
   107  		return err
   108  	}
   109  
   110  	if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
   111  		return err
   112  	}
   113  
   114  	pkgpath, err := copyLocalData(appdir)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
   120  	if err := os.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
   121  		return err
   122  	}
   123  	if err := os.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist(pkgpath)), 0744); err != nil {
   124  		return err
   125  	}
   126  	if err := os.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
   127  		return err
   128  	}
   129  	return nil
   130  }
   131  
   132  func installSimulator(appdir string) error {
   133  	cmd := exec.Command(
   134  		"xcrun", "simctl", "install",
   135  		"booted", // Install to the booted simulator.
   136  		appdir,
   137  	)
   138  	if out, err := cmd.CombinedOutput(); err != nil {
   139  		os.Stderr.Write(out)
   140  		return fmt.Errorf("xcrun simctl install booted %q: %v", appdir, err)
   141  	}
   142  	return nil
   143  }
   144  
   145  func runSimulator(appdir, bundleID string, args []string) error {
   146  	xcrunArgs := []string{"simctl", "spawn",
   147  		"booted",
   148  		appdir + "/gotest",
   149  	}
   150  	xcrunArgs = append(xcrunArgs, args...)
   151  	cmd := exec.Command("xcrun", xcrunArgs...)
   152  	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
   153  	err := cmd.Run()
   154  	if err != nil {
   155  		return fmt.Errorf("xcrun simctl launch booted %q: %v", bundleID, err)
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  func copyLocalDir(dst, src string) error {
   162  	if err := os.Mkdir(dst, 0755); err != nil {
   163  		return err
   164  	}
   165  
   166  	d, err := os.Open(src)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer d.Close()
   171  	fi, err := d.Readdir(-1)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	for _, f := range fi {
   177  		if f.IsDir() {
   178  			if f.Name() == "testdata" {
   179  				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   180  					return err
   181  				}
   182  			}
   183  			continue
   184  		}
   185  		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   186  			return err
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  func cp(dst, src string) error {
   193  	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
   194  	if err != nil {
   195  		os.Stderr.Write(out)
   196  	}
   197  	return err
   198  }
   199  
   200  func copyLocalData(dstbase string) (pkgpath string, err error) {
   201  	cwd, err := os.Getwd()
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  
   206  	finalPkgpath, underGoRoot, err := subdir()
   207  	if err != nil {
   208  		return "", err
   209  	}
   210  	cwd = strings.TrimSuffix(cwd, finalPkgpath)
   211  
   212  	// Copy all immediate files and testdata directories between
   213  	// the package being tested and the source root.
   214  	pkgpath = ""
   215  	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
   216  		if debug {
   217  			log.Printf("copying %s", pkgpath)
   218  		}
   219  		pkgpath = filepath.Join(pkgpath, element)
   220  		dst := filepath.Join(dstbase, pkgpath)
   221  		src := filepath.Join(cwd, pkgpath)
   222  		if err := copyLocalDir(dst, src); err != nil {
   223  			return "", err
   224  		}
   225  	}
   226  
   227  	if underGoRoot {
   228  		// Copy timezone file.
   229  		//
   230  		// Typical apps have the zoneinfo.zip in the root of their app bundle,
   231  		// read by the time package as the working directory at initialization.
   232  		// As we move the working directory to the GOROOT pkg directory, we
   233  		// install the zoneinfo.zip file in the pkgpath.
   234  		err := cp(
   235  			filepath.Join(dstbase, pkgpath),
   236  			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
   237  		)
   238  		if err != nil {
   239  			return "", err
   240  		}
   241  		// Copy src/runtime/textflag.h for (at least) Test386EndToEnd in
   242  		// cmd/asm/internal/asm.
   243  		runtimePath := filepath.Join(dstbase, "src", "runtime")
   244  		if err := os.MkdirAll(runtimePath, 0755); err != nil {
   245  			return "", err
   246  		}
   247  		err = cp(
   248  			filepath.Join(runtimePath, "textflag.h"),
   249  			filepath.Join(cwd, "src", "runtime", "textflag.h"),
   250  		)
   251  		if err != nil {
   252  			return "", err
   253  		}
   254  	}
   255  
   256  	return finalPkgpath, nil
   257  }
   258  
   259  // subdir determines the package based on the current working directory,
   260  // and returns the path to the package source relative to $GOROOT (or $GOPATH).
   261  func subdir() (pkgpath string, underGoRoot bool, err error) {
   262  	cwd, err := os.Getwd()
   263  	if err != nil {
   264  		return "", false, err
   265  	}
   266  	cwd, err = filepath.EvalSymlinks(cwd)
   267  	if err != nil {
   268  		log.Fatal(err)
   269  	}
   270  	goroot, err := filepath.EvalSymlinks(runtime.GOROOT())
   271  	if err != nil {
   272  		return "", false, err
   273  	}
   274  	if strings.HasPrefix(cwd, goroot) {
   275  		subdir, err := filepath.Rel(goroot, cwd)
   276  		if err != nil {
   277  			return "", false, err
   278  		}
   279  		return subdir, true, nil
   280  	}
   281  
   282  	for _, p := range filepath.SplitList(build.Default.GOPATH) {
   283  		pabs, err := filepath.EvalSymlinks(p)
   284  		if err != nil {
   285  			return "", false, err
   286  		}
   287  		if !strings.HasPrefix(cwd, pabs) {
   288  			continue
   289  		}
   290  		subdir, err := filepath.Rel(pabs, cwd)
   291  		if err == nil {
   292  			return subdir, false, nil
   293  		}
   294  	}
   295  	return "", false, fmt.Errorf(
   296  		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
   297  		cwd,
   298  		runtime.GOROOT(),
   299  		build.Default.GOPATH,
   300  	)
   301  }
   302  
   303  func infoPlist(pkgpath string) string {
   304  	return `<?xml version="1.0" encoding="UTF-8"?>
   305  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   306  <plist version="1.0">
   307  <dict>
   308  <key>CFBundleName</key><string>golang.gotest</string>
   309  <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
   310  <key>CFBundleExecutable</key><string>gotest</string>
   311  <key>CFBundleVersion</key><string>1.0</string>
   312  <key>CFBundleShortVersionString</key><string>1.0</string>
   313  <key>CFBundleIdentifier</key><string>` + bundleID + `</string>
   314  <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
   315  <key>LSRequiresIPhoneOS</key><true/>
   316  <key>CFBundleDisplayName</key><string>gotest</string>
   317  <key>GoExecWrapperWorkingDirectory</key><string>` + pkgpath + `</string>
   318  </dict>
   319  </plist>
   320  `
   321  }
   322  
   323  func entitlementsPlist() string {
   324  	return `<?xml version="1.0" encoding="UTF-8"?>
   325  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   326  <plist version="1.0">
   327  <dict>
   328  	<key>keychain-access-groups</key>
   329  	<array><string>` + appID + `</string></array>
   330  	<key>get-task-allow</key>
   331  	<true/>
   332  	<key>application-identifier</key>
   333  	<string>` + appID + `</string>
   334  	<key>com.apple.developer.team-identifier</key>
   335  	<string>` + teamID + `</string>
   336  </dict>
   337  </plist>
   338  `
   339  }
   340  
   341  const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
   342  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   343  <plist version="1.0">
   344  <dict>
   345  	<key>rules</key>
   346  	<dict>
   347  		<key>.*</key>
   348  		<true/>
   349  		<key>Info.plist</key>
   350  		<dict>
   351  			<key>omit</key>
   352  			<true/>
   353  			<key>weight</key>
   354  			<integer>10</integer>
   355  		</dict>
   356  		<key>ResourceRules.plist</key>
   357  		<dict>
   358  			<key>omit</key>
   359  			<true/>
   360  			<key>weight</key>
   361  			<integer>100</integer>
   362  		</dict>
   363  	</dict>
   364  </dict>
   365  </plist>
   366  `
   367  

View as plain text