blob: f7b682389a7b9e25e9afa013cff59b0a749bf929 [file] [log] [blame]
// Copyright 2015 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.
package main
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
)
func goAppleBuild(pkg *packages.Package, bundleID string, targets []targetInfo) (map[string]bool, error) {
src := pkg.PkgPath
if buildO != "" && !strings.HasSuffix(buildO, ".app") {
return nil, fmt.Errorf("-o must have an .app for -target=ios")
}
productName := rfc1034Label(path.Base(pkg.PkgPath))
if productName == "" {
productName = "ProductName" // like xcode.
}
infoplist := new(bytes.Buffer)
if err := infoplistTmpl.Execute(infoplist, infoplistTmplData{
BundleID: bundleID + "." + productName,
Name: strings.Title(path.Base(pkg.PkgPath)),
}); err != nil {
return nil, err
}
// Detect the team ID
teamID, err := detectTeamID()
if err != nil {
return nil, err
}
projPbxproj := new(bytes.Buffer)
if err := projPbxprojTmpl.Execute(projPbxproj, projPbxprojTmplData{
TeamID: teamID,
}); err != nil {
return nil, err
}
files := []struct {
name string
contents []byte
}{
{tmpdir + "/main.xcodeproj/project.pbxproj", projPbxproj.Bytes()},
{tmpdir + "/main/Info.plist", infoplist.Bytes()},
{tmpdir + "/main/Images.xcassets/AppIcon.appiconset/Contents.json", []byte(contentsJSON)},
}
for _, file := range files {
if err := mkdir(filepath.Dir(file.name)); err != nil {
return nil, err
}
if buildX {
printcmd("echo \"%s\" > %s", file.contents, file.name)
}
if !buildN {
if err := ioutil.WriteFile(file.name, file.contents, 0644); err != nil {
return nil, err
}
}
}
// We are using lipo tool to build multiarchitecture binaries.
cmd := exec.Command(
"xcrun", "lipo",
"-o", filepath.Join(tmpdir, "main/main"),
"-create",
)
var nmpkgs map[string]bool
builtArch := map[string]bool{}
for _, t := range targets {
// Only one binary per arch allowed
// e.g. ios/arm64 + iossimulator/amd64
if builtArch[t.arch] {
continue
}
builtArch[t.arch] = true
path := filepath.Join(tmpdir, t.platform, t.arch)
// Disable DWARF; see golang.org/issues/25148.
if err := goBuild(src, appleEnv[t.String()], "-ldflags=-w", "-o="+path); err != nil {
return nil, err
}
if nmpkgs == nil {
var err error
nmpkgs, err = extractPkgs(appleNM, path)
if err != nil {
return nil, err
}
}
cmd.Args = append(cmd.Args, path)
}
if err := runCmd(cmd); err != nil {
return nil, err
}
// TODO(jbd): Set the launcher icon.
if err := appleCopyAssets(pkg, tmpdir); err != nil {
return nil, err
}
// Build and move the release build to the output directory.
cmdStrings := []string{
"xcodebuild",
"-configuration", "Release",
"-project", tmpdir + "/main.xcodeproj",
"-allowProvisioningUpdates",
"DEVELOPMENT_TEAM=" + teamID,
}
cmd = exec.Command("xcrun", cmdStrings...)
if err := runCmd(cmd); err != nil {
return nil, err
}
// TODO(jbd): Fallback to copying if renaming fails.
if buildO == "" {
n := pkg.PkgPath
if n == "." {
// use cwd name
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("cannot create .app; cannot get the current working dir: %v", err)
}
n = cwd
}
n = path.Base(n)
buildO = n + ".app"
}
if buildX {
printcmd("mv %s %s", tmpdir+"/build/Release-iphoneos/main.app", buildO)
}
if !buildN {
// if output already exists, remove.
if err := os.RemoveAll(buildO); err != nil {
return nil, err
}
if err := os.Rename(tmpdir+"/build/Release-iphoneos/main.app", buildO); err != nil {
return nil, err
}
}
return nmpkgs, nil
}
func detectTeamID() (string, error) {
// Grabs the first certificate for "Apple Development"; will not work if there
// are multiple certificates and the first is not desired.
cmd := exec.Command(
"security", "find-certificate",
"-c", "Apple Development", "-p",
)
pemString, err := cmd.Output()
if err != nil {
err = fmt.Errorf("failed to pull the signing certificate to determine your team ID: %v", err)
return "", err
}
block, _ := pem.Decode(pemString)
if block == nil {
err = fmt.Errorf("failed to decode the PEM to determine your team ID: %s", pemString)
return "", err
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
err = fmt.Errorf("failed to parse your signing certificate to determine your team ID: %v", err)
return "", err
}
if len(cert.Subject.OrganizationalUnit) == 0 {
err = fmt.Errorf("the signing certificate has no organizational unit (team ID)")
return "", err
}
return cert.Subject.OrganizationalUnit[0], nil
}
func appleCopyAssets(pkg *packages.Package, xcodeProjDir string) error {
dstAssets := xcodeProjDir + "/main/assets"
if err := mkdir(dstAssets); err != nil {
return err
}
// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
// Fix this to work with other Go tools.
srcAssets := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "assets")
fi, err := os.Stat(srcAssets)
if err != nil {
if os.IsNotExist(err) {
// skip walking through the directory to deep copy.
return nil
}
return err
}
if !fi.IsDir() {
// skip walking through to deep copy.
return nil
}
// if assets is a symlink, follow the symlink.
srcAssets, err = filepath.EvalSymlinks(srcAssets)
if err != nil {
return err
}
return filepath.Walk(srcAssets, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name := filepath.Base(path); strings.HasPrefix(name, ".") {
// Do not include the hidden files.
return nil
}
if info.IsDir() {
return nil
}
dst := dstAssets + "/" + path[len(srcAssets)+1:]
return copyFile(dst, path)
})
}
type infoplistTmplData struct {
BundleID string
Name string
}
var infoplistTmpl = template.Must(template.New("infoplist").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>main</string>
<key>CFBundleIdentifier</key>
<string>{{.BundleID}}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{.Name}}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
`))
type projPbxprojTmplData struct {
TeamID string
}
var projPbxprojTmpl = template.Must(template.New("projPbxproj").Parse(`// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 254BB84E1B1FD08900C56DE9 /* Images.xcassets */; };
254BB8681B1FD16500C56DE9 /* main in Resources */ = {isa = PBXBuildFile; fileRef = 254BB8671B1FD16500C56DE9 /* main */; };
25FB30331B30FDEE0005924C /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 25FB30321B30FDEE0005924C /* assets */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
254BB83E1B1FD08900C56DE9 /* main.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = main.app; sourceTree = BUILT_PRODUCTS_DIR; };
254BB8421B1FD08900C56DE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
254BB84E1B1FD08900C56DE9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
254BB8671B1FD16500C56DE9 /* main */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = main; sourceTree = "<group>"; };
25FB30321B30FDEE0005924C /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = main/assets; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
254BB8351B1FD08900C56DE9 = {
isa = PBXGroup;
children = (
25FB30321B30FDEE0005924C /* assets */,
254BB8401B1FD08900C56DE9 /* main */,
254BB83F1B1FD08900C56DE9 /* Products */,
);
sourceTree = "<group>";
usesTabs = 0;
};
254BB83F1B1FD08900C56DE9 /* Products */ = {
isa = PBXGroup;
children = (
254BB83E1B1FD08900C56DE9 /* main.app */,
);
name = Products;
sourceTree = "<group>";
};
254BB8401B1FD08900C56DE9 /* main */ = {
isa = PBXGroup;
children = (
254BB8671B1FD16500C56DE9 /* main */,
254BB84E1B1FD08900C56DE9 /* Images.xcassets */,
254BB8411B1FD08900C56DE9 /* Supporting Files */,
);
path = main;
sourceTree = "<group>";
};
254BB8411B1FD08900C56DE9 /* Supporting Files */ = {
isa = PBXGroup;
children = (
254BB8421B1FD08900C56DE9 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
254BB83D1B1FD08900C56DE9 /* main */ = {
isa = PBXNativeTarget;
buildConfigurationList = 254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */;
buildPhases = (
254BB83C1B1FD08900C56DE9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = main;
productName = main;
productReference = 254BB83E1B1FD08900C56DE9 /* main.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
254BB8361B1FD08900C56DE9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0630;
ORGANIZATIONNAME = Developer;
TargetAttributes = {
254BB83D1B1FD08900C56DE9 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = {{.TeamID}};
};
};
};
buildConfigurationList = 254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 254BB8351B1FD08900C56DE9;
productRefGroup = 254BB83F1B1FD08900C56DE9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
254BB83D1B1FD08900C56DE9 /* main */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
254BB83C1B1FD08900C56DE9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
25FB30331B30FDEE0005924C /* assets in Resources */,
254BB8681B1FD16500C56DE9 /* main in Resources */,
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
254BB8601B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
ENABLE_BITCODE = YES;
};
name = Release;
};
254BB8631B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8601B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8631B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 254BB8361B1FD08900C56DE9 /* Project object */;
}
`))
const contentsJSON = `{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
`
// rfc1034Label sanitizes the name to be usable in a uniform type identifier.
// The sanitization is similar to xcode's rfc1034identifier macro that
// replaces illegal characters (not conforming the rfc1034 label rule) with '-'.
func rfc1034Label(name string) string {
// * Uniform type identifier:
//
// According to
// https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
//
// A uniform type identifier is a Unicode string that usually contains characters
// in the ASCII character set. However, only a subset of the ASCII characters are
// permitted. You may use the Roman alphabet in upper and lower case (A–Z, a–z),
// the digits 0 through 9, the dot (“.”), and the hyphen (“-”). This restriction
// is based on DNS name restrictions, set forth in RFC 1035.
//
// Uniform type identifiers may also contain any of the Unicode characters greater
// than U+007F.
//
// Note: the actual implementation of xcode does not allow some unicode characters
// greater than U+007f. In this implementation, we just replace everything non
// alphanumeric with "-" like the rfc1034identifier macro.
//
// * RFC1034 Label
//
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
// <let-dig-hyp> ::= <let-dig> | "-"
// <let-dig> ::= <letter> | <digit>
const surrSelf = 0x10000
begin := false
var res []rune
for i, r := range name {
if r == '.' && !begin {
continue
}
begin = true
switch {
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z':
res = append(res, r)
case '0' <= r && r <= '9':
if i == 0 {
res = append(res, '-')
} else {
res = append(res, r)
}
default:
if r < surrSelf {
res = append(res, '-')
} else {
res = append(res, '-', '-')
}
}
}
return string(res)
}