github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/iosbuild.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package main 4 5 import ( 6 "archive/zip" 7 "crypto/sha1" 8 "encoding/hex" 9 "errors" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "strings" 17 "time" 18 19 "golang.org/x/sync/errgroup" 20 ) 21 22 const minIOSVersion = "9.0" 23 24 func buildIOS(tmpDir, target string, bi *buildInfo) error { 25 appName := bi.name 26 switch *buildMode { 27 case "archive": 28 framework := *destPath 29 if framework == "" { 30 framework = fmt.Sprintf("%s.framework", strings.Title(appName)) 31 } 32 return archiveIOS(tmpDir, target, framework, bi) 33 case "exe": 34 out := *destPath 35 if out == "" { 36 out = appName + ".ipa" 37 } 38 forDevice := strings.HasSuffix(out, ".ipa") 39 // Filter out unsupported architectures. 40 for i := len(bi.archs) - 1; i >= 0; i-- { 41 switch bi.archs[i] { 42 case "arm", "arm64": 43 if forDevice { 44 continue 45 } 46 case "386", "amd64": 47 if !forDevice { 48 continue 49 } 50 } 51 52 bi.archs = append(bi.archs[:i], bi.archs[i+1:]...) 53 } 54 tmpFramework := filepath.Join(tmpDir, "Gio.framework") 55 if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil { 56 return err 57 } 58 if !forDevice && !strings.HasSuffix(out, ".app") { 59 return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", out) 60 } 61 if !forDevice { 62 return exeIOS(tmpDir, target, out, bi) 63 } 64 payload := filepath.Join(tmpDir, "Payload") 65 appDir := filepath.Join(payload, appName+".app") 66 if err := os.MkdirAll(appDir, 0755); err != nil { 67 return err 68 } 69 if err := exeIOS(tmpDir, target, appDir, bi); err != nil { 70 return err 71 } 72 if err := signIOS(bi, tmpDir, appDir); err != nil { 73 return err 74 } 75 return zipDir(out, tmpDir, "Payload") 76 default: 77 panic("unreachable") 78 } 79 } 80 81 func signIOS(bi *buildInfo, tmpDir, app string) error { 82 home, err := os.UserHomeDir() 83 if err != nil { 84 return err 85 } 86 provPattern := filepath.Join(home, "Library", "MobileDevice", "Provisioning Profiles", "*.mobileprovision") 87 provisions, err := filepath.Glob(provPattern) 88 if err != nil { 89 return err 90 } 91 provInfo := filepath.Join(tmpDir, "provision.plist") 92 var avail []string 93 for _, prov := range provisions { 94 // Decode the provision file to a plist. 95 _, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", provInfo)) 96 if err != nil { 97 return err 98 } 99 expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ExpirationDate", provInfo)) 100 if err != nil { 101 return err 102 } 103 exp, err := time.Parse(time.UnixDate, expUnix) 104 if err != nil { 105 return fmt.Errorf("sign: failed to parse expiration date from %q: %v", prov, err) 106 } 107 if exp.Before(time.Now()) { 108 continue 109 } 110 appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ApplicationIdentifierPrefix:0", provInfo)) 111 if err != nil { 112 return err 113 } 114 provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:Entitlements:application-identifier", provInfo)) 115 if err != nil { 116 return err 117 } 118 expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID) 119 avail = append(avail, provAppID) 120 if expAppID != provAppID { 121 continue 122 } 123 // Copy provisioning file. 124 embedded := filepath.Join(app, "embedded.mobileprovision") 125 if err := copyFile(embedded, prov); err != nil { 126 return err 127 } 128 certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:DeveloperCertificates:0", provInfo)) 129 if err != nil { 130 return err 131 } 132 // Omit trailing newline. 133 certDER = certDER[:len(certDER)-1] 134 entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-x", "-c", "Print:Entitlements", provInfo)) 135 if err != nil { 136 return err 137 } 138 entFile := filepath.Join(tmpDir, "entitlements.plist") 139 if err := ioutil.WriteFile(entFile, []byte(entitlements), 0660); err != nil { 140 return err 141 } 142 identity := sha1.Sum(certDER) 143 idHex := hex.EncodeToString(identity[:]) 144 _, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", "--entitlements", entFile, app)) 145 return err 146 } 147 return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail) 148 } 149 150 func exeIOS(tmpDir, target, app string, bi *buildInfo) error { 151 if bi.appID == "" { 152 return errors.New("app id is empty; use -appid to set it") 153 } 154 if err := os.RemoveAll(app); err != nil { 155 return err 156 } 157 if err := os.Mkdir(app, 0755); err != nil { 158 return err 159 } 160 mainm := filepath.Join(tmpDir, "main.m") 161 const mainmSrc = `@import UIKit; 162 @import Gio; 163 164 @interface GioAppDelegate : UIResponder <UIApplicationDelegate> 165 @property (strong, nonatomic) UIWindow *window; 166 @end 167 168 @implementation GioAppDelegate 169 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 170 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 171 GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil]; 172 self.window.rootViewController = controller; 173 [self.window makeKeyAndVisible]; 174 return YES; 175 } 176 @end 177 178 int main(int argc, char * argv[]) { 179 @autoreleasepool { 180 return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class])); 181 } 182 }` 183 if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil { 184 return err 185 } 186 appName := strings.Title(bi.name) 187 exe := filepath.Join(app, appName) 188 lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") 189 var builds errgroup.Group 190 for _, a := range bi.archs { 191 clang, cflags, err := iosCompilerFor(target, a) 192 if err != nil { 193 return err 194 } 195 exeSlice := filepath.Join(tmpDir, "app-"+a) 196 lipo.Args = append(lipo.Args, exeSlice) 197 compile := exec.Command(clang, cflags...) 198 compile.Args = append(compile.Args, 199 "-Werror", 200 "-fmodules", 201 "-fobjc-arc", 202 "-x", "objective-c", 203 "-F", tmpDir, 204 "-o", exeSlice, 205 mainm, 206 ) 207 builds.Go(func() error { 208 _, err := runCmd(compile) 209 return err 210 }) 211 } 212 if err := builds.Wait(); err != nil { 213 return err 214 } 215 if _, err := runCmd(lipo); err != nil { 216 return err 217 } 218 infoPlist := buildInfoPlist(bi) 219 plistFile := filepath.Join(app, "Info.plist") 220 if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil { 221 return err 222 } 223 if _, err := os.Stat(bi.iconPath); err == nil { 224 assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath) 225 if err != nil { 226 return err 227 } 228 // Merge assets plist with Info.plist 229 cmd := exec.Command( 230 "/usr/libexec/PlistBuddy", 231 "-c", "Merge "+assetPlist, 232 plistFile, 233 ) 234 if _, err := runCmd(cmd); err != nil { 235 return err 236 } 237 } 238 if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", plistFile)); err != nil { 239 return err 240 } 241 return nil 242 } 243 244 // iosIcons builds an asset catalog and compile it with the Xcode command actool. 245 // iosIcons returns the asset plist file to be merged into Info.plist. 246 func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) { 247 assets := filepath.Join(tmpDir, "Assets.xcassets") 248 if err := os.Mkdir(assets, 0700); err != nil { 249 return "", err 250 } 251 appIcon := filepath.Join(assets, "AppIcon.appiconset") 252 err := buildIcons(appIcon, icon, []iconVariant{ 253 {path: "ios_2x.png", size: 120}, 254 {path: "ios_3x.png", size: 180}, 255 // The App Store icon is not allowed to contain 256 // transparent pixels. 257 {path: "ios_store.png", size: 1024, fill: true}, 258 }) 259 if err != nil { 260 return "", err 261 } 262 contentJson := `{ 263 "images" : [ 264 { 265 "size" : "60x60", 266 "idiom" : "iphone", 267 "filename" : "ios_2x.png", 268 "scale" : "2x" 269 }, 270 { 271 "size" : "60x60", 272 "idiom" : "iphone", 273 "filename" : "ios_3x.png", 274 "scale" : "3x" 275 }, 276 { 277 "size" : "1024x1024", 278 "idiom" : "ios-marketing", 279 "filename" : "ios_store.png", 280 "scale" : "1x" 281 } 282 ] 283 }` 284 contentFile := filepath.Join(appIcon, "Contents.json") 285 if err := ioutil.WriteFile(contentFile, []byte(contentJson), 0600); err != nil { 286 return "", err 287 } 288 assetPlist := filepath.Join(tmpDir, "assets.plist") 289 compile := exec.Command( 290 "actool", 291 "--compile", appDir, 292 "--platform", iosPlatformFor(bi.target), 293 "--minimum-deployment-target", minIOSVersion, 294 "--app-icon", "AppIcon", 295 "--output-partial-info-plist", assetPlist, 296 assets) 297 _, err = runCmd(compile) 298 return assetPlist, err 299 } 300 301 func buildInfoPlist(bi *buildInfo) string { 302 appName := strings.Title(bi.name) 303 platform := iosPlatformFor(bi.target) 304 var supportPlatform string 305 switch bi.target { 306 case "ios": 307 supportPlatform = "iPhoneOS" 308 case "tvos": 309 supportPlatform = "AppleTVOS" 310 } 311 return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> 312 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 313 <plist version="1.0"> 314 <dict> 315 <key>CFBundleDevelopmentRegion</key> 316 <string>en</string> 317 <key>CFBundleExecutable</key> 318 <string>%s</string> 319 <key>CFBundleIdentifier</key> 320 <string>%s</string> 321 <key>CFBundleInfoDictionaryVersion</key> 322 <string>6.0</string> 323 <key>CFBundleName</key> 324 <string>%s</string> 325 <key>CFBundlePackageType</key> 326 <string>APPL</string> 327 <key>CFBundleShortVersionString</key> 328 <string>1.0.%d</string> 329 <key>CFBundleVersion</key> 330 <string>%d</string> 331 <key>UILaunchStoryboardName</key> 332 <string>LaunchScreen</string> 333 <key>UIRequiredDeviceCapabilities</key> 334 <array><string>arm64</string></array> 335 <key>DTPlatformName</key> 336 <string>%s</string> 337 <key>DTPlatformVersion</key> 338 <string>12.4</string> 339 <key>MinimumOSVersion</key> 340 <string>%s</string> 341 <key>UIDeviceFamily</key> 342 <array> 343 <integer>1</integer> 344 <integer>2</integer> 345 </array> 346 <key>CFBundleSupportedPlatforms</key> 347 <array> 348 <string>%s</string> 349 </array> 350 <key>UISupportedInterfaceOrientations</key> 351 <array> 352 <string>UIInterfaceOrientationPortrait</string> 353 <string>UIInterfaceOrientationLandscapeLeft</string> 354 <string>UIInterfaceOrientationLandscapeRight</string> 355 </array> 356 <key>DTCompiler</key> 357 <string>com.apple.compilers.llvm.clang.1_0</string> 358 <key>DTPlatformBuild</key> 359 <string>16G73</string> 360 <key>DTSDKBuild</key> 361 <string>16G73</string> 362 <key>DTSDKName</key> 363 <string>%s12.4</string> 364 <key>DTXcode</key> 365 <string>1030</string> 366 <key>DTXcodeBuild</key> 367 <string>10G8</string> 368 </dict> 369 </plist>`, appName, bi.appID, appName, bi.version, bi.version, platform, minIOSVersion, supportPlatform, platform) 370 } 371 372 func iosPlatformFor(target string) string { 373 switch target { 374 case "ios": 375 return "iphoneos" 376 case "tvos": 377 return "appletvos" 378 default: 379 panic("invalid platform " + target) 380 } 381 } 382 383 func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error { 384 framework := filepath.Base(frameworkRoot) 385 const suf = ".framework" 386 if !strings.HasSuffix(framework, suf) { 387 return fmt.Errorf("the specified output %q does not end in '.framework'", frameworkRoot) 388 } 389 framework = framework[:len(framework)-len(suf)] 390 if err := os.RemoveAll(frameworkRoot); err != nil { 391 return err 392 } 393 frameworkDir := filepath.Join(frameworkRoot, "Versions", "A") 394 for _, dir := range []string{"Headers", "Modules"} { 395 p := filepath.Join(frameworkDir, dir) 396 if err := os.MkdirAll(p, 0755); err != nil { 397 return err 398 } 399 } 400 symlinks := [][2]string{ 401 {"Versions/Current/Headers", "Headers"}, 402 {"Versions/Current/Modules", "Modules"}, 403 {"Versions/Current/" + framework, framework}, 404 {"A", filepath.Join("Versions", "Current")}, 405 } 406 for _, l := range symlinks { 407 if err := os.Symlink(l[0], filepath.Join(frameworkRoot, l[1])); err != nil && !os.IsExist(err) { 408 return err 409 } 410 } 411 exe := filepath.Join(frameworkDir, framework) 412 lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") 413 var builds errgroup.Group 414 tags := bi.tags 415 goos := "ios" 416 supportsIOS, err := supportsGOOS("ios") 417 if err != nil { 418 return err 419 } 420 if !supportsIOS { 421 // Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios. 422 goos = "darwin" 423 tags = "ios " + tags 424 } 425 for _, a := range bi.archs { 426 clang, cflags, err := iosCompilerFor(target, a) 427 if err != nil { 428 return err 429 } 430 lib := filepath.Join(tmpDir, "gio-"+a) 431 cmd := exec.Command( 432 "go", 433 "build", 434 "-ldflags=-s -w "+bi.ldflags, 435 "-buildmode=c-archive", 436 "-o", lib, 437 "-tags", tags, 438 bi.pkgPath, 439 ) 440 lipo.Args = append(lipo.Args, lib) 441 cflagsLine := strings.Join(cflags, " ") 442 cmd.Env = append( 443 os.Environ(), 444 "GOOS="+goos, 445 "GOARCH="+a, 446 "CGO_ENABLED=1", 447 "CC="+clang, 448 "CGO_CFLAGS="+cflagsLine, 449 "CGO_LDFLAGS="+cflagsLine, 450 ) 451 builds.Go(func() error { 452 _, err := runCmd(cmd) 453 return err 454 }) 455 } 456 if err := builds.Wait(); err != nil { 457 return err 458 } 459 if _, err := runCmd(lipo); err != nil { 460 return err 461 } 462 appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "github.com/cybriq/giocore/app/internal/wm")) 463 if err != nil { 464 return err 465 } 466 headerDst := filepath.Join(frameworkDir, "Headers", framework+".h") 467 headerSrc := filepath.Join(appDir, "framework_ios.h") 468 if err := copyFile(headerDst, headerSrc); err != nil { 469 return err 470 } 471 module := fmt.Sprintf(`framework module "%s" { 472 header "%[1]s.h" 473 474 export * 475 }`, framework) 476 moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap") 477 return ioutil.WriteFile(moduleFile, []byte(module), 0644) 478 } 479 480 func supportsGOOS(wantGoos string) (bool, error) { 481 geese, err := runCmd(exec.Command("go", "tool", "dist", "list")) 482 if err != nil { 483 return false, err 484 } 485 for _, pair := range strings.Split(geese, "\n") { 486 s := strings.SplitN(pair, "/", 2) 487 if len(s) != 2 { 488 return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s", pair) 489 } 490 goos := s[0] 491 if goos == wantGoos { 492 return true, nil 493 } 494 } 495 return false, nil 496 } 497 498 func iosCompilerFor(target, arch string) (string, []string, error) { 499 var platformSDK string 500 var platformOS string 501 switch target { 502 case "ios": 503 platformOS = "ios" 504 platformSDK = "iphone" 505 case "tvos": 506 platformOS = "tvos" 507 platformSDK = "appletv" 508 } 509 switch arch { 510 case "arm", "arm64": 511 platformSDK += "os" 512 case "386", "amd64": 513 platformOS += "-simulator" 514 platformSDK += "simulator" 515 default: 516 return "", nil, fmt.Errorf("unsupported -arch: %s", arch) 517 } 518 sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path")) 519 if err != nil { 520 return "", nil, err 521 } 522 clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang")) 523 if err != nil { 524 return "", nil, err 525 } 526 cflags := []string{ 527 "-fembed-bitcode", 528 "-arch", allArchs[arch].iosArch, 529 "-isysroot", sdkPath, 530 "-m" + platformOS + "-version-min=" + minIOSVersion, 531 } 532 return clang, cflags, nil 533 } 534 535 func zipDir(dst, base, dir string) (err error) { 536 f, err := os.Create(dst) 537 if err != nil { 538 return err 539 } 540 defer func() { 541 if cerr := f.Close(); err == nil { 542 err = cerr 543 } 544 }() 545 zipf := zip.NewWriter(f) 546 err = filepath.Walk(filepath.Join(base, dir), func(path string, f os.FileInfo, err error) error { 547 if err != nil { 548 return err 549 } 550 if f.IsDir() { 551 return nil 552 } 553 rel := filepath.ToSlash(path[len(base)+1:]) 554 entry, err := zipf.Create(rel) 555 if err != nil { 556 return err 557 } 558 src, err := os.Open(path) 559 if err != nil { 560 return err 561 } 562 defer src.Close() 563 _, err = io.Copy(entry, src) 564 return err 565 }) 566 if err != nil { 567 return err 568 } 569 return zipf.Close() 570 }