github.com/champo/mobile@v0.0.0-20190107162257-dc0771356504/cmd/gomobile/build_iosapp.go (about)

     1  // Copyright 2015 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  package main
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/x509"
    10  	"encoding/pem"
    11  	"fmt"
    12  	"go/build"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path"
    17  	"path/filepath"
    18  	"strings"
    19  	"text/template"
    20  )
    21  
    22  func goIOSBuild(pkg *build.Package, bundleID string, archs []string) (map[string]bool, error) {
    23  	src := pkg.ImportPath
    24  	if buildO != "" && !strings.HasSuffix(buildO, ".app") {
    25  		return nil, fmt.Errorf("-o must have an .app for -target=ios")
    26  	}
    27  
    28  	productName := rfc1034Label(path.Base(pkg.ImportPath))
    29  	if productName == "" {
    30  		productName = "ProductName" // like xcode.
    31  	}
    32  
    33  	infoplist := new(bytes.Buffer)
    34  	if err := infoplistTmpl.Execute(infoplist, infoplistTmplData{
    35  		// TODO: better bundle id.
    36  		BundleID: bundleID + "." + productName,
    37  		Name:     strings.Title(path.Base(pkg.ImportPath)),
    38  	}); err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	files := []struct {
    43  		name     string
    44  		contents []byte
    45  	}{
    46  		{tmpdir + "/main.xcodeproj/project.pbxproj", []byte(projPbxproj)},
    47  		{tmpdir + "/main/Info.plist", infoplist.Bytes()},
    48  		{tmpdir + "/main/Images.xcassets/AppIcon.appiconset/Contents.json", []byte(contentsJSON)},
    49  	}
    50  
    51  	for _, file := range files {
    52  		if err := mkdir(filepath.Dir(file.name)); err != nil {
    53  			return nil, err
    54  		}
    55  		if buildX {
    56  			printcmd("echo \"%s\" > %s", file.contents, file.name)
    57  		}
    58  		if !buildN {
    59  			if err := ioutil.WriteFile(file.name, file.contents, 0644); err != nil {
    60  				return nil, err
    61  			}
    62  		}
    63  	}
    64  
    65  	// We are using lipo tool to build multiarchitecture binaries.
    66  	cmd := exec.Command(
    67  		"xcrun", "lipo",
    68  		"-o", filepath.Join(tmpdir, "main/main"),
    69  		"-create",
    70  	)
    71  	var nmpkgs map[string]bool
    72  	for _, arch := range archs {
    73  		path := filepath.Join(tmpdir, arch)
    74  		// Disable DWARF; see golang.org/issues/25148.
    75  		if err := goBuild(src, darwinEnv[arch], "-ldflags=-w", "-o="+path); err != nil {
    76  			return nil, err
    77  		}
    78  		if nmpkgs == nil {
    79  			var err error
    80  			nmpkgs, err = extractPkgs(darwinArmNM, path)
    81  			if err != nil {
    82  				return nil, err
    83  			}
    84  		}
    85  		cmd.Args = append(cmd.Args, path)
    86  	}
    87  
    88  	if err := runCmd(cmd); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	// TODO(jbd): Set the launcher icon.
    93  	if err := iosCopyAssets(pkg, tmpdir); err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	// Detect the team ID
    98  	teamID, err := detectTeamID()
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	// Build and move the release build to the output directory.
   104  	cmdStrings := []string{
   105  		"xcodebuild",
   106  		"-configuration", "Release",
   107  		"-project", tmpdir + "/main.xcodeproj",
   108  		"-allowProvisioningUpdates",
   109  		"DEVELOPMENT_TEAM=" + teamID,
   110  	}
   111  
   112  	cmd = exec.Command("xcrun", cmdStrings...)
   113  	if err := runCmd(cmd); err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	// TODO(jbd): Fallback to copying if renaming fails.
   118  	if buildO == "" {
   119  		n := pkg.ImportPath
   120  		if n == "." {
   121  			// use cwd name
   122  			cwd, err := os.Getwd()
   123  			if err != nil {
   124  				return nil, fmt.Errorf("cannot create .app; cannot get the current working dir: %v", err)
   125  			}
   126  			n = cwd
   127  		}
   128  		n = path.Base(n)
   129  		buildO = n + ".app"
   130  	}
   131  	if buildX {
   132  		printcmd("mv %s %s", tmpdir+"/build/Release-iphoneos/main.app", buildO)
   133  	}
   134  	if !buildN {
   135  		// if output already exists, remove.
   136  		if err := os.RemoveAll(buildO); err != nil {
   137  			return nil, err
   138  		}
   139  		if err := os.Rename(tmpdir+"/build/Release-iphoneos/main.app", buildO); err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  	return nmpkgs, nil
   144  }
   145  
   146  func detectTeamID() (string, error) {
   147  	// Grabs the first certificate for "iPhone Developer"; will not work if there
   148  	// are multiple certificates and the first is not desired.
   149  	cmd := exec.Command(
   150  		"security", "find-certificate",
   151  		"-c", "iPhone Developer", "-p",
   152  	)
   153  	pemString, err := cmd.Output()
   154  	if err != nil {
   155  		err = fmt.Errorf("failed to pull the signing certificate to determine your team ID: %v", err)
   156  		return "", err
   157  	}
   158  
   159  	block, _ := pem.Decode(pemString)
   160  	if block == nil {
   161  		err = fmt.Errorf("failed to decode the PEM to determine your team ID: %s", pemString)
   162  		return "", err
   163  	}
   164  
   165  	cert, err := x509.ParseCertificate(block.Bytes)
   166  	if err != nil {
   167  		err = fmt.Errorf("failed to parse your signing certificate to determine your team ID: %v", err)
   168  		return "", err
   169  	}
   170  
   171  	if len(cert.Subject.OrganizationalUnit) == 0 {
   172  		err = fmt.Errorf("the signing certificate has no organizational unit (team ID).")
   173  		return "", err
   174  	}
   175  
   176  	return cert.Subject.OrganizationalUnit[0], nil
   177  }
   178  
   179  func iosCopyAssets(pkg *build.Package, xcodeProjDir string) error {
   180  	dstAssets := xcodeProjDir + "/main/assets"
   181  	if err := mkdir(dstAssets); err != nil {
   182  		return err
   183  	}
   184  
   185  	srcAssets := filepath.Join(pkg.Dir, "assets")
   186  	fi, err := os.Stat(srcAssets)
   187  	if err != nil {
   188  		if os.IsNotExist(err) {
   189  			// skip walking through the directory to deep copy.
   190  			return nil
   191  		}
   192  		return err
   193  	}
   194  	if !fi.IsDir() {
   195  		// skip walking through to deep copy.
   196  		return nil
   197  	}
   198  	// if assets is a symlink, follow the symlink.
   199  	srcAssets, err = filepath.EvalSymlinks(srcAssets)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	return filepath.Walk(srcAssets, func(path string, info os.FileInfo, err error) error {
   204  		if err != nil {
   205  			return err
   206  		}
   207  		if name := filepath.Base(path); strings.HasPrefix(name, ".") {
   208  			// Do not include the hidden files.
   209  			return nil
   210  		}
   211  		if info.IsDir() {
   212  			return nil
   213  		}
   214  		dst := dstAssets + "/" + path[len(srcAssets)+1:]
   215  		return copyFile(dst, path)
   216  	})
   217  }
   218  
   219  type infoplistTmplData struct {
   220  	BundleID string
   221  	Name     string
   222  }
   223  
   224  var infoplistTmpl = template.Must(template.New("infoplist").Parse(`<?xml version="1.0" encoding="UTF-8"?>
   225  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   226  <plist version="1.0">
   227  <dict>
   228    <key>CFBundleDevelopmentRegion</key>
   229    <string>en</string>
   230    <key>CFBundleExecutable</key>
   231    <string>main</string>
   232    <key>CFBundleIdentifier</key>
   233    <string>{{.BundleID}}</string>
   234    <key>CFBundleInfoDictionaryVersion</key>
   235    <string>6.0</string>
   236    <key>CFBundleName</key>
   237    <string>{{.Name}}</string>
   238    <key>CFBundlePackageType</key>
   239    <string>APPL</string>
   240    <key>CFBundleShortVersionString</key>
   241    <string>1.0</string>
   242    <key>CFBundleSignature</key>
   243    <string>????</string>
   244    <key>CFBundleVersion</key>
   245    <string>1</string>
   246    <key>LSRequiresIPhoneOS</key>
   247    <true/>
   248    <key>UILaunchStoryboardName</key>
   249    <string>LaunchScreen</string>
   250    <key>UIRequiredDeviceCapabilities</key>
   251    <array>
   252      <string>armv7</string>
   253    </array>
   254    <key>UISupportedInterfaceOrientations</key>
   255    <array>
   256      <string>UIInterfaceOrientationPortrait</string>
   257      <string>UIInterfaceOrientationLandscapeLeft</string>
   258      <string>UIInterfaceOrientationLandscapeRight</string>
   259    </array>
   260    <key>UISupportedInterfaceOrientations~ipad</key>
   261    <array>
   262      <string>UIInterfaceOrientationPortrait</string>
   263      <string>UIInterfaceOrientationPortraitUpsideDown</string>
   264      <string>UIInterfaceOrientationLandscapeLeft</string>
   265      <string>UIInterfaceOrientationLandscapeRight</string>
   266    </array>
   267  </dict>
   268  </plist>
   269  `))
   270  
   271  const projPbxproj = `// !$*UTF8*$!
   272  {
   273    archiveVersion = 1;
   274    classes = {
   275    };
   276    objectVersion = 46;
   277    objects = {
   278  
   279  /* Begin PBXBuildFile section */
   280      254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 254BB84E1B1FD08900C56DE9 /* Images.xcassets */; };
   281      254BB8681B1FD16500C56DE9 /* main in Resources */ = {isa = PBXBuildFile; fileRef = 254BB8671B1FD16500C56DE9 /* main */; };
   282      25FB30331B30FDEE0005924C /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 25FB30321B30FDEE0005924C /* assets */; };
   283  /* End PBXBuildFile section */
   284  
   285  /* Begin PBXFileReference section */
   286      254BB83E1B1FD08900C56DE9 /* main.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = main.app; sourceTree = BUILT_PRODUCTS_DIR; };
   287      254BB8421B1FD08900C56DE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
   288      254BB84E1B1FD08900C56DE9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
   289      254BB8671B1FD16500C56DE9 /* main */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = main; sourceTree = "<group>"; };
   290      25FB30321B30FDEE0005924C /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = main/assets; sourceTree = "<group>"; };
   291  /* End PBXFileReference section */
   292  
   293  /* Begin PBXGroup section */
   294      254BB8351B1FD08900C56DE9 = {
   295        isa = PBXGroup;
   296        children = (
   297          25FB30321B30FDEE0005924C /* assets */,
   298          254BB8401B1FD08900C56DE9 /* main */,
   299          254BB83F1B1FD08900C56DE9 /* Products */,
   300        );
   301        sourceTree = "<group>";
   302        usesTabs = 0;
   303      };
   304      254BB83F1B1FD08900C56DE9 /* Products */ = {
   305        isa = PBXGroup;
   306        children = (
   307          254BB83E1B1FD08900C56DE9 /* main.app */,
   308        );
   309        name = Products;
   310        sourceTree = "<group>";
   311      };
   312      254BB8401B1FD08900C56DE9 /* main */ = {
   313        isa = PBXGroup;
   314        children = (
   315          254BB8671B1FD16500C56DE9 /* main */,
   316          254BB84E1B1FD08900C56DE9 /* Images.xcassets */,
   317          254BB8411B1FD08900C56DE9 /* Supporting Files */,
   318        );
   319        path = main;
   320        sourceTree = "<group>";
   321      };
   322      254BB8411B1FD08900C56DE9 /* Supporting Files */ = {
   323        isa = PBXGroup;
   324        children = (
   325          254BB8421B1FD08900C56DE9 /* Info.plist */,
   326        );
   327        name = "Supporting Files";
   328        sourceTree = "<group>";
   329      };
   330  /* End PBXGroup section */
   331  
   332  /* Begin PBXNativeTarget section */
   333      254BB83D1B1FD08900C56DE9 /* main */ = {
   334        isa = PBXNativeTarget;
   335        buildConfigurationList = 254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */;
   336        buildPhases = (
   337          254BB83C1B1FD08900C56DE9 /* Resources */,
   338        );
   339        buildRules = (
   340        );
   341        dependencies = (
   342        );
   343        name = main;
   344        productName = main;
   345        productReference = 254BB83E1B1FD08900C56DE9 /* main.app */;
   346        productType = "com.apple.product-type.application";
   347      };
   348  /* End PBXNativeTarget section */
   349  
   350  /* Begin PBXProject section */
   351      254BB8361B1FD08900C56DE9 /* Project object */ = {
   352        isa = PBXProject;
   353        attributes = {
   354          LastUpgradeCheck = 0630;
   355          ORGANIZATIONNAME = Developer;
   356          TargetAttributes = {
   357            254BB83D1B1FD08900C56DE9 = {
   358              CreatedOnToolsVersion = 6.3.1;
   359            };
   360          };
   361        };
   362        buildConfigurationList = 254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */;
   363        compatibilityVersion = "Xcode 3.2";
   364        developmentRegion = English;
   365        hasScannedForEncodings = 0;
   366        knownRegions = (
   367          en,
   368          Base,
   369        );
   370        mainGroup = 254BB8351B1FD08900C56DE9;
   371        productRefGroup = 254BB83F1B1FD08900C56DE9 /* Products */;
   372        projectDirPath = "";
   373        projectRoot = "";
   374        targets = (
   375          254BB83D1B1FD08900C56DE9 /* main */,
   376        );
   377      };
   378  /* End PBXProject section */
   379  
   380  /* Begin PBXResourcesBuildPhase section */
   381      254BB83C1B1FD08900C56DE9 /* Resources */ = {
   382        isa = PBXResourcesBuildPhase;
   383        buildActionMask = 2147483647;
   384        files = (
   385          25FB30331B30FDEE0005924C /* assets in Resources */,
   386          254BB8681B1FD16500C56DE9 /* main in Resources */,
   387          254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */,
   388        );
   389        runOnlyForDeploymentPostprocessing = 0;
   390      };
   391  /* End PBXResourcesBuildPhase section */
   392  
   393  /* Begin XCBuildConfiguration section */
   394      254BB8601B1FD08900C56DE9 /* Release */ = {
   395        isa = XCBuildConfiguration;
   396        buildSettings = {
   397          ALWAYS_SEARCH_USER_PATHS = NO;
   398          CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
   399          CLANG_CXX_LIBRARY = "libc++";
   400          CLANG_ENABLE_MODULES = YES;
   401          CLANG_ENABLE_OBJC_ARC = YES;
   402          CLANG_WARN_BOOL_CONVERSION = YES;
   403          CLANG_WARN_CONSTANT_CONVERSION = YES;
   404          CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
   405          CLANG_WARN_EMPTY_BODY = YES;
   406          CLANG_WARN_ENUM_CONVERSION = YES;
   407          CLANG_WARN_INT_CONVERSION = YES;
   408          CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
   409          CLANG_WARN_UNREACHABLE_CODE = YES;
   410          CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
   411          "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
   412          COPY_PHASE_STRIP = NO;
   413          DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
   414          ENABLE_NS_ASSERTIONS = NO;
   415          ENABLE_STRICT_OBJC_MSGSEND = YES;
   416          GCC_C_LANGUAGE_STANDARD = gnu99;
   417          GCC_NO_COMMON_BLOCKS = YES;
   418          GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
   419          GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
   420          GCC_WARN_UNDECLARED_SELECTOR = YES;
   421          GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
   422          GCC_WARN_UNUSED_FUNCTION = YES;
   423          GCC_WARN_UNUSED_VARIABLE = YES;
   424          IPHONEOS_DEPLOYMENT_TARGET = 8.3;
   425          MTL_ENABLE_DEBUG_INFO = NO;
   426          SDKROOT = iphoneos;
   427          TARGETED_DEVICE_FAMILY = "1,2";
   428          VALIDATE_PRODUCT = YES;
   429        };
   430        name = Release;
   431      };
   432      254BB8631B1FD08900C56DE9 /* Release */ = {
   433        isa = XCBuildConfiguration;
   434        buildSettings = {
   435          ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
   436          INFOPLIST_FILE = main/Info.plist;
   437          LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
   438          PRODUCT_NAME = "$(TARGET_NAME)";
   439        };
   440        name = Release;
   441      };
   442  /* End XCBuildConfiguration section */
   443  
   444  /* Begin XCConfigurationList section */
   445      254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */ = {
   446        isa = XCConfigurationList;
   447        buildConfigurations = (
   448          254BB8601B1FD08900C56DE9 /* Release */,
   449        );
   450        defaultConfigurationIsVisible = 0;
   451        defaultConfigurationName = Release;
   452      };
   453      254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */ = {
   454        isa = XCConfigurationList;
   455        buildConfigurations = (
   456          254BB8631B1FD08900C56DE9 /* Release */,
   457        );
   458        defaultConfigurationIsVisible = 0;
   459        defaultConfigurationName = Release;
   460      };
   461  /* End XCConfigurationList section */
   462    };
   463    rootObject = 254BB8361B1FD08900C56DE9 /* Project object */;
   464  }
   465  `
   466  
   467  const contentsJSON = `{
   468    "images" : [
   469      {
   470        "idiom" : "iphone",
   471        "size" : "29x29",
   472        "scale" : "2x"
   473      },
   474      {
   475        "idiom" : "iphone",
   476        "size" : "29x29",
   477        "scale" : "3x"
   478      },
   479      {
   480        "idiom" : "iphone",
   481        "size" : "40x40",
   482        "scale" : "2x"
   483      },
   484      {
   485        "idiom" : "iphone",
   486        "size" : "40x40",
   487        "scale" : "3x"
   488      },
   489      {
   490        "idiom" : "iphone",
   491        "size" : "60x60",
   492        "scale" : "2x"
   493      },
   494      {
   495        "idiom" : "iphone",
   496        "size" : "60x60",
   497        "scale" : "3x"
   498      },
   499      {
   500        "idiom" : "ipad",
   501        "size" : "29x29",
   502        "scale" : "1x"
   503      },
   504      {
   505        "idiom" : "ipad",
   506        "size" : "29x29",
   507        "scale" : "2x"
   508      },
   509      {
   510        "idiom" : "ipad",
   511        "size" : "40x40",
   512        "scale" : "1x"
   513      },
   514      {
   515        "idiom" : "ipad",
   516        "size" : "40x40",
   517        "scale" : "2x"
   518      },
   519      {
   520        "idiom" : "ipad",
   521        "size" : "76x76",
   522        "scale" : "1x"
   523      },
   524      {
   525        "idiom" : "ipad",
   526        "size" : "76x76",
   527        "scale" : "2x"
   528      }
   529    ],
   530    "info" : {
   531      "version" : 1,
   532      "author" : "xcode"
   533    }
   534  }
   535  `
   536  
   537  // rfc1034Label sanitizes the name to be usable in a uniform type identifier.
   538  // The sanitization is similar to xcode's rfc1034identifier macro that
   539  // replaces illegal characters (not conforming the rfc1034 label rule) with '-'.
   540  func rfc1034Label(name string) string {
   541  	// * Uniform type identifier:
   542  	//
   543  	// According to
   544  	// https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
   545  	//
   546  	// A uniform type identifier is a Unicode string that usually contains characters
   547  	// in the ASCII character set. However, only a subset of the ASCII characters are
   548  	// permitted. You may use the Roman alphabet in upper and lower case (A–Z, a–z),
   549  	// the digits 0 through 9, the dot (“.”), and the hyphen (“-”). This restriction
   550  	// is based on DNS name restrictions, set forth in RFC 1035.
   551  	//
   552  	// Uniform type identifiers may also contain any of the Unicode characters greater
   553  	// than U+007F.
   554  	//
   555  	// Note: the actual implementation of xcode does not allow some unicode characters
   556  	// greater than U+007f. In this implementation, we just replace everything non
   557  	// alphanumeric with "-" like the rfc1034identifier macro.
   558  	//
   559  	// * RFC1034 Label
   560  	//
   561  	// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
   562  	// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
   563  	// <let-dig-hyp> ::= <let-dig> | "-"
   564  	// <let-dig> ::= <letter> | <digit>
   565  	const surrSelf = 0x10000
   566  	begin := false
   567  
   568  	var res []rune
   569  	for i, r := range name {
   570  		if r == '.' && !begin {
   571  			continue
   572  		}
   573  		begin = true
   574  
   575  		switch {
   576  		case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z':
   577  			res = append(res, r)
   578  		case '0' <= r && r <= '9':
   579  			if i == 0 {
   580  				res = append(res, '-')
   581  			} else {
   582  				res = append(res, r)
   583  			}
   584  		default:
   585  			if r < surrSelf {
   586  				res = append(res, '-')
   587  			} else {
   588  				res = append(res, '-', '-')
   589  			}
   590  		}
   591  	}
   592  	return string(res)
   593  }