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. 7package main 8 9import ( 10 "fmt" 11 "go/build" 12 "log" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "runtime" 17 "strings" 18 "syscall" 19) 20 21const debug = false 22 23var tmpdir string 24 25var ( 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. 36var lock *os.File 37 38func 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 58func 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 97func 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 105func 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 132func 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 145func 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 161func 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 192func 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 200func 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). 261func 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 303func 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 323func 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 341const 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