iOS 遠程打包腳本制作

在 iOS 開發(fā)中,一般打發(fā)布包都是在本地打包佃蚜,也就是工程師在自己開發(fā)電腦上使用 Xcode 編譯并導(dǎo)出安裝包來進行發(fā)布,為了提高效率可能會制作一些自動化打包腳本恭取。本文聊的是遠程打包的內(nèi)容妒貌,通過資源拷貝及參數(shù)替換然后編譯完成打包通危。

由于 HTML5 跨平臺的特點,很多技術(shù)團隊考慮到代碼復(fù)用灌曙,在部分模塊中會采用 h5 來描述界面菊碟。甚至有些不需要太復(fù)雜交互的 app,全部界面采用 h5 來編寫在刺,也就是一個 web 工程逆害。對于大部分現(xiàn)有的 web 工程,能打包成 app 就已經(jīng)滿足了業(yè)務(wù)訴求蚣驼。DCloud 團隊開發(fā)的 HBuilder(IDE)工具中提供了云打包的功能魄幕,用起來很方便,簡單的說隙姿,就是把 web 工程上傳到云打包服務(wù)器梅垄,最后打包生成 app,點擊下載即可安裝使用输玷。

dcloud_pack_policy.png

雖然云打包服務(wù)很方便队丝,但上傳源碼總感覺不太妥當,總有些秘密不想讓別人看見欲鹏,并且其他同事也有打包的需求机久,但不一定會使用 HBuilder。因此赔嚎,搭建一個自己的打包服務(wù)很有必要膘盖。

按照 HBuilder 提供的云打包功能,先定一個初步的需求:

  • 支持修改應(yīng)用 id尤误、版本號 侠畔、icon、啟動圖
  • 支持導(dǎo)入簽名文件

開工K鹞睢H砉住!

準備工作

首先尤勋,需要一臺安裝了 MacOS 的電腦(當做服務(wù)器使用)喘落。

筆者手頭上剛好有臺閑置的電腦就拿來當服務(wù)器使用了,裝了 VMware最冰,然后裝了 MacOS 虛擬機(問題較多瘦棋,不建議使用虛擬機)。

物理機 windows7暖哨,內(nèi)存 4G赌朋;虛擬機 MacOS,內(nèi)存 3G。

其次沛慢,在服務(wù)器上部署一個 web 服務(wù)服球,提供打包交互界面方便客戶端上傳資源文件及下載安裝包。我們的界面只提供了一個 www zip 包的上傳入口颠焦,所有應(yīng)用資源及打包相關(guān)的配置文件都在里面斩熊。www 目錄結(jié)構(gòu)如下:

www_directory.png

appConfig.json 文件內(nèi)容

{
    "id":"com.domain.pack",
    "appName":"我的應(yīng)用",
    "debug":true,
    "launchPath": "index.html",
    "version": {
        "name": "1.0.0",
        "code": "100"
    },
    ...
}

launchPath 對應(yīng) web 應(yīng)用入口文件,iOS 工程使用這個文件路徑作為 webview 的加載入口伐庭。

secret.json 文件內(nèi)容

{
    "ios" : {
        "p12Password" : "123456"
    },
    "android" : {
        "keyAlias" : "keyAlias",
        "keyPassword" : "123456",
        "storePassword" : "123456",
        "amapApiKey" : "",
        "jpushApiKey" : "",
        ...
    }
}

除了交互界面外粉渠,打包服務(wù)還需要提供調(diào)起 Python 腳本的功能。

Python 打包腳本

基本所有的功能都使用腳本實現(xiàn)圾另,使用 Python 編寫打包腳本是因為 Python 用起來方便霸株,剛開始打算用 Shell 來編寫,執(zhí)行效果可能好一些集乔,但是對這個不熟去件,只好將就用 Python。我們的 web 服務(wù)采用 Java 編寫扰路,Java 是可以調(diào)用 Python 腳本的 ProcessBuilder pb = new ProcessBuilder(command.split(" "));尤溜。打包腳本事先準備好,放在 web 服務(wù)站點根目錄下汗唱,在解壓完 www zip 包之后宫莱,把腳本拷貝到與 www 目錄同級目錄中,然后執(zhí)行腳本打包哩罪。打包腳本主要做以下幾件事情:

  • 下載 iOS 工程代碼到指定目錄
  • 將客戶端上傳的 www 文件資源拷貝到 iOS 工程目錄授霸,應(yīng)用圖標、啟動圖等
  • 修改 iOS 工程配置
  • 導(dǎo)入證書到系統(tǒng)鑰匙串
  • 導(dǎo)入 mobileprovision 文件
  • 編譯工程
  • 導(dǎo)出 ipa 安裝包

打包腳本和客戶端上傳的 www 文件夾需要放在同一目錄下际插。

實現(xiàn)難度不是很大碘耳,但是細節(jié)很多,需要反復(fù)實踐嘗試框弛。腳本全部內(nèi)容見文章末尾辛辨。

下載 iOS 工程代碼到指定目錄

svnChekoutCmd = 'svn co --username=%s --password=%s %s %s' %(SVN_USERNAME, SVN_PASSWORD, SVN_URL, checkoutPath())
p = subprocess.Popen(svnChekoutCmd, shell=True, stderr=subprocess.PIPE)
p.wait()

從 svn 倉庫拉取 iOS 工程代碼,使用 svn checkout 命令把代碼拷貝到指定目錄功咒,后面會使用這個目錄下的工程進行編譯愉阎。

將客戶端上傳的 www 文件資源拷貝到 iOS 工程目錄

sourceWWWDir = currentDir() + '/www'
projectWWWDir = '/packProject/www'
destinationWWWDir = checkoutPath() + projectWWWDir
copyFiles(sourceWWWDir, destinationWWWDir)
for file in os.listdir(destinationWWWDir):
    if file.startswith('secret.json') or file.endswith('.mobileprovision') or file.endswith('.p12'):
        os.remove(destinationWWWDir + '/' + file)

將客戶端上傳的 www 文件夾拷貝到 iOS 工程中的 www 目錄下绞蹦。

iconAssetDirectory = checkoutPath() + '/packProject/Assets.xcassets/AppIcon.appiconset'
iconSrcDirectory = projectWWWDir + '/Icons/ios'
items = os.listdir(iconSrcDirectory)
for filename in items:
    copyFile(iconSrcDirectory + '/' + filename, iconAssetDirectory + '/' + filename)
clearDir(iconSrcDirectory)

www/Icons/ios 文件夾中的各種尺寸的應(yīng)用圖標拷貝到 Assets.xcassets/AppIcon.appiconset 目錄中力奋。這個需要事先編寫好 AppIcon.appiconset 中的 Contents.json 文件,為每種尺寸的 icon 指定文件名幽七,這里的文件名與 Icons/ios 目錄下的圖片文件名一一對應(yīng)景殷,所以,Icons/ios 中的圖片名稱是固定不變的。Contents.json 文件部分內(nèi)容:

{
  "images" : [
    {
      "idiom" : "iphone",
      "size" : "20x20",
      "filename" : "40x40.png",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "size" : "20x20",
      "filename" : "60x60.png",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "size" : "29x29",
      "filename" : "58x58.png",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "size" : "29x29",
      "filename" : "87x87.png",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "size" : "40x40",
      "filename" : "80x80.png",
      "scale" : "2x"
    },
}

啟動圖資源的拷貝跟應(yīng)用圖標的拷貝一樣猿挚,需要事先編寫好 Contents.json 文件咐旧,并且啟動圖的名稱也是固定的。

修改 iOS 工程配置

需要根據(jù)客戶端上傳的配置文件 appConfig.json 來修改工程配置绩蜻。

首先铣墨,讀取配置文件的內(nèi)容,包括應(yīng)用 id 办绝、名稱伊约、版本號、編譯號孕蝉、應(yīng)用入口等屡律。Python 讀取 json 文件字符串類型的值默認會轉(zhuǎn)為 unicode 編碼表示,需要進行處理降淮,筆者專門寫了一個 json_load_byteified 函數(shù)來處理這個問題超埋。

其次,使用從配置文件中獲取到的內(nèi)容來修改 info.plist 文件佳鳖。這里需要使用 MacOS 系統(tǒng)自帶的工具 PlistBuddy 來輔助修改霍殴。

導(dǎo)入證書到系統(tǒng)鑰匙串

p12FilePath = findFileInDirectory('.p12', sourceWWWDir)
unlockKeychainCmd = 'security unlock-keychain -p %s' %MacOS_ADMIN_PASSWORD
p = subprocess.Popen(unlockKeychainCmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
if p.returncode != 0:
    print p.stderr.read()
    return
importCertCmd = 'security import %s -P %s -T /usr/bin/codesign' % (p12FilePath, p12Password)
p = subprocess.Popen(importCertCmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
if p.returncode != 0:
    print p.stderr.read()

使用系統(tǒng) security 工具將 p12 文件導(dǎo)入到系統(tǒng)鑰匙串中,先打開系統(tǒng)鑰匙串并提供系統(tǒng)管理員密碼系吩,然后再導(dǎo)入繁成。

證書和私鑰需要客戶端事先準備好,并導(dǎo)出為 p12 文件一并放入 www 文件夾中上傳(如何導(dǎo)出 p12 文件請自行查看官方文檔)淑玫。p12 文件的密碼規(guī)定寫在 secret.json 文件中巾腕。

導(dǎo)入 mobileprovision 文件

provisionFileExtension = '.mobileprovision'
provisionFilePath = findFileInDirectory(provisionFileExtension, sourceWWWDir)
if not len(provisionFilePath) > 0:
    print ("[packageFailed]: Not found \'%s\' file in \'www\' directory.") %(provisionFileExtension)
    return
teamIdentifier = getMobileProvisionItem(provisionFilePath, 'TeamIdentifier')
provisionUUID = getMobileProvisionItem(provisionFilePath, 'UUID')
provisionName = getMobileProvisionItem(provisionFilePath, 'Name')
# type – prints mobileprovision profile type (debug, ad-hoc, enterprise, appstore)
provisionType = getMobileProvisionItem(provisionFilePath, 'type')
teamName = getMobileProvisionItem(provisionFilePath, 'TeamName')
desProvisionFilePath = PROVISONING_PROFILE_DIRECTORY + provisionUUID + provisionFileExtension
copyFile(provisionFilePath, desProvisionFilePath)

讀取 .mobileprovision 文件的信息,并將 uuid 作為它的文件名保存到 /Users/%s/Library/MobileDevice/Provisioning Profiles/ 目錄絮蒿,完成導(dǎo)入尊搬。如果先前已經(jīng)導(dǎo)入過該類文件(一般雙擊文件導(dǎo)入),打開這個目錄可以看到土涝,文件名都是 uuid佛寿。這里,除了 uuid 之外但壮,還可以讀取團隊 id冀泻、名稱以及文件類型(debug, ad-hoc, enterprise, appstore)等信息。

為了方便讀取 .mobileprovision 文件信息蜡饵,這里使用一個第三方命令行小工具弹渔。安裝命令如下:

curl https://raw.githubusercontent.com/0xc010d/mobileprovision-read/master/main.m | clang -framework Foundation -framework Security -o /usr/local/bin/mobileprovision-read -x objective-c - 

安裝命令會使用 curl 工具下載源碼,然后使用 clang 編譯并將可執(zhí)行文件輸出到 /usr/local/bin/ 目錄溯祸,命名為 mobileprovision-read肢专,用法:

mobileprovision-read -f fileName [-o option]

該工具實現(xiàn)比較簡單舞肆,使用 security 庫解析 mobileprovision 文件,然后根據(jù)命令行輸入的 option 選擇輸出結(jié)果博杖,因為筆者沒有對源碼進行修改椿胯,所以需要對輸出結(jié)果中的控制字符 \n 進行處理(removeControlChars 函數(shù)的作用)。

編譯工程

編譯源碼剃根。以前在蘋果線上開發(fā)者文檔可以查看 xcodebuild 用法哩盲,不知道什么時候刪掉了,現(xiàn)在只能使用 man xcodebuild 查看 xcodebuild 用法狈醉,這個不多說种冬。需要注意的是,剛才只是導(dǎo)入了 .mobileprovision 文件舔糖,工程配置并沒有修改娱两,所以沒有關(guān)聯(lián)起來。在 project.pbxproj 文件中有以下幾個字段需要進行替換金吗,替換完之后才算完成整個工程編譯變量的配置十兢。

PRODUCT_BUNDLE_IDENTIFIER
PROVISIONING_PROFILE_SPECIFIER
PROVISIONING_PROFILE

可以在命令行傳入這幾個編譯變量完成替換,命令行中傳入的編譯變量優(yōu)先級最高摇庙。

project.pbxproj 不是常見的文件格式旱物,在不知道 xcodebuild 可以注入編譯變量之前,找了一圈發(fā)現(xiàn)沒有方便的工具可以用來編輯卫袒。有人建議先轉(zhuǎn)成 json 然后再使用 json 編輯工具進行修改宵呛。筆者沒有采納,筆者想到用 sed夕凝,但 sed 只對簡單的文本內(nèi)容有效宝穗,這種嵌套層級太多的內(nèi)容貌似匹配不了,所以码秉,無法進行修改逮矛。awk 應(yīng)該可以,但這個我沒有嘗試转砖。

導(dǎo)出 ipa 安裝包

創(chuàng)建 exportOptions.plist 文件并導(dǎo)出 .ipa 安裝包须鼎。把生成的 .ipa 文件路徑輸出給 java 進程,java 進程將結(jié)果顯示在界面上府蔗,方便客戶端進行下載晋控。

注意:
Python 腳本沒有執(zhí)行權(quán)限,需要使用 Chmod 命令添加執(zhí)行權(quán)限姓赤。

腳本全部內(nèi)容如下(詳見 github 源碼):

#!/usr/bin/env python
# _*_ coding:utf-8 _*_

import subprocess
import os
import json
import re

SVN_USERNAME = 'Hansen'
SVN_PASSWORD = '123456'
SVN_URL = 'https://Hansen@svn.domain.com/svn/****/trunk/iOS/packProject'
CHECKOUT_FOLDER = 'ios_source_code'
MacOS_ADMIN_USER = 'packrobot'
MacOS_ADMIN_PASSWORD = '123456'
EXPORT_MAIN_DIRECTORY = "/Users/%s/Documents/ios_appArchive/" % MacOS_ADMIN_USER
PROVISONING_PROFILE_DIRECTORY = "/Users/%s/Library/MobileDevice/Provisioning Profiles/" % MacOS_ADMIN_USER

def json_load_byteified(file_handle):
    return _byteify(
        json.load(file_handle, object_hook=_byteify),
        ignore_dicts=True
    )

def json_loads_byteified(json_text):
    return _byteify(
        json.loads(json_text, object_hook=_byteify),
        ignore_dicts=True
    )

def _byteify(data, ignore_dicts = False):
    # if this is a unicode string, return its string representation
    if isinstance(data, unicode):
        return data.encode('utf-8')
    # if this is a list of values, return list of byteified values
    if isinstance(data, list):
        return [ _byteify(item, ignore_dicts=True) for item in data ]
    # if this is a dictionary, return dictionary of byteified keys and values
    # but only if we haven't already byteified it
    if isinstance(data, dict) and not ignore_dicts:
        return {
            _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True)
            for key, value in data.iteritems()
        }
    # if it's anything else, return it in its original form
    return data

def currentDir():
    return os.path.split(os.path.realpath(__file__))[0]

def checkoutPath():
    return currentDir() + '/' + CHECKOUT_FOLDER

def pullSvnSourceCode():
    svnChekoutCmd = 'svn co --username=%s --password=%s %s %s' %(SVN_USERNAME, SVN_PASSWORD, SVN_URL, checkoutPath())
    p = subprocess.Popen(svnChekoutCmd, shell=True, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print ('[packageFailed]: %s') %p.stderr.read()
    else:
        print ('Sucessfullly checkout source code at path: %s') %(checkoutPath())

def clearDir(Dir):
    cleanCmd = "rm -r %s" %(Dir)
    process = subprocess.Popen(cleanCmd, shell=True)
    (stdoutdata, stderrdata) = process.communicate()

def getAppConfig():
    projectWWWDir = 'packProject/www'
    destinationWWWDir = currentDir() + '/' + CHECKOUT_FOLDER + '/' + projectWWWDir;
    appConfigFilePath = destinationWWWDir + '/appConfig.json'
    if os.path.exists(appConfigFilePath):
        appConfigReader = open(appConfigFilePath, 'r')
        appConfig = json_load_byteified(appConfigReader)
        appConfigReader.close()
        return appConfig
    return None


def copyFiles(sourceDir, destinationDir):
    if not os.path.exists(sourceDir):
        print ('[packageFailed]: Copy file -- sourceDir doesn\'t exist ')
        pass

    clearDir(destinationDir)
    for file in os.listdir(sourceDir):
        sourceFile = os.path.join(sourceDir, file)
        destinationFile = os.path.join(destinationDir, file)
        if os.path.isfile(sourceFile):
            if not os.path.exists(destinationDir):
                os.makedirs(destinationDir)
            if not os.path.exists(destinationFile) or (os.path.exists(destinationFile) and (os.path.getsize(destinationFile) != os.path.getsize(sourceFile))):
                open(destinationFile, "wb").write(open(sourceFile, "rb").read())
        if os.path.isdir(sourceFile):
            copyFiles(sourceFile, destinationFile)
    print ('Copy assets success!')

def copyFile(srcFile, dstFile):
    srcReader = open(srcFile, "rb")
    desWriter = open(dstFile, "wb")
    desWriter.write(srcReader.read())
    srcReader.close()
    desWriter.close()

def cleanArchiveFile(archiveFile):
    cleanCmd = "rm -r %s" %(archiveFile)
    process = subprocess.Popen(cleanCmd, shell=True)
    (stdoutdata, stderrdata) = process.communicate()

def buildExportDirectory():
    dateCmd = 'date "+%Y-%m-%d_%H-%M-%S"'
    process = subprocess.Popen(dateCmd, stdout=subprocess.PIPE, shell=True)
    (stdoutdata, stderrdata) = process.communicate()
    exportDirectory = "%s%s" %(EXPORT_MAIN_DIRECTORY, stdoutdata.strip())
    return exportDirectory

def getMobileProvisionItem(filepath, key):
    cmd = 'mobileprovision-read -f %s -o %s' %(filepath ,key)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
    p.wait()
    return removeControlChars(p.stdout.read())

def updatePlistEntry(filePath, key, value):
    cmd = "/usr/libexec/PlistBuddy -c 'Set :%s %s' %s" % (key, value, filePath)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print p.stderr.read()

def deletePlistEntry(filePath, key):
    cmd = "/usr/libexec/PlistBuddy -c 'Delete :%s' %s" %(key, filePath)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print p.stderr.read()

def addPlistEntry(filePath, key, _type, value):
    cmd = "/usr/libexec/PlistBuddy -c 'Add :%s %s %s' %s" % (key, _type, value, filePath)
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print p.stderr.read()

def findFileInDirectory(ext, dir):
    fileName = ''
    items = os.listdir(dir)
    for name in items:
        if name.endswith(ext):
            fileName = name
            break
    if not len(fileName) > 0:
        return ''
    return dir + '/' + fileName

def removeControlChars(s):
    control_chars = ''.join(map(unichr, range(0,32) + range(127,160)))
    control_char_re = re.compile('[%s]' % re.escape(control_chars))
    return control_char_re.sub('', s)

def main():

    # Pull ios project source code from svn.
    pullSvnSourceCode()

    # Copy 'www' files. 
    sourceWWWDir = currentDir() + '/www'
    projectWWWDir = '/packProject/www'
    destinationWWWDir = checkoutPath() + projectWWWDir
    copyFiles(sourceWWWDir, destinationWWWDir)
    for file in os.listdir(destinationWWWDir):
        if file.startswith('secret.json') or file.endswith('.mobileprovision') or file.endswith('.p12'):
            os.remove(destinationWWWDir + '/' + file)

    # Copy app icons.
    iconAssetDirectory = checkoutPath() + '/packProject/Assets.xcassets/AppIcon.appiconset'
    iconSrcDirectory = projectWWWDir + '/Icons/ios'
    items = os.listdir(iconSrcDirectory)
    for filename in items:
        copyFile(iconSrcDirectory + '/' + filename, iconAssetDirectory + '/' + filename)
    clearDir(iconSrcDirectory)

    # Copy launch images.
    launchImageAssetDirectory = checkoutPath() + '/packProject/Assets.xcassets/LaunchImage.launchimage'
    LaunchImageSrcDirectory = projectWWWDir + '/LaunchImages/ios'
    items = os.listdir(LaunchImageSrcDirectory)
    for filename in items:
        copyFile(LaunchImageSrcDirectory + '/' + filename, launchImageAssetDirectory + '/' + filename)
    clearDir(launchImageAssetDirectory)

    # Read 'appConfig.json' file.
    appConfig = getAppConfig()
    if appConfig is None:
        print ("[packageFailed]: Not found \'%s\' file in \'www\' directory.") % ('appConfig.json')
        return
    versionName = appConfig['version']['name']
    versionCode = int(appConfig['version']['code'])
    applicationId = appConfig['id']
    appName = appConfig['appName']
    mode = 'Debug' if appConfig['debug'] else 'Release'

    # Modify 'info.plist' file in project/workspace according to appconfig params those read from 'appConfig.json' file.
    infoPlistPath = checkoutPath() + '/packProject/' + 'info.plist'
    updatePlistEntry(infoPlistPath, 'CFBundleShortVersionString', versionName)
    updatePlistEntry(infoPlistPath, 'CFBundleVersion', versionCode)
    updatePlistEntry(infoPlistPath, 'CFBundleIdentifier', applicationId)
    updatePlistEntry(infoPlistPath, 'CFBundleDisplayName', appName)

    # Get p12 file's password.
    secretFilePath = sourceWWWDir + '/secret.json'
    if os.path.exists(secretFilePath):
        secretReader = open(secretFilePath, 'r')
        secretKeyDict = json_load_byteified(secretReader)
        secretReader.close()
    else:
        print ("[packageFailed]: Not found \'%s\' file in \'www\' directory.") % ('secret.json')
        return
    iosKeyDict = secretKeyDict['ios'] if 'ios' in secretKeyDict else None
    p12Password = iosKeyDict['p12Password'] if 'p12Password' in iosKeyDict else '123456'

    # Import p12 file into system keychain.
    p12FilePath = findFileInDirectory('.p12', sourceWWWDir)
    unlockKeychainCmd = 'security unlock-keychain -p %s' %MacOS_ADMIN_PASSWORD
    p = subprocess.Popen(unlockKeychainCmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print p.stderr.read()
        return
    importCertCmd = 'security import %s -P %s -T /usr/bin/codesign' % (p12FilePath, p12Password)
    p = subprocess.Popen(importCertCmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print p.stderr.read()

    # Read mobileprovision profile info.
    provisionFileExtension = '.mobileprovision'
    provisionFilePath = findFileInDirectory(provisionFileExtension, sourceWWWDir)
    if not len(provisionFilePath) > 0:
        print ("[packageFailed]: Not found \'%s\' file in \'www\' directory.") %(provisionFileExtension)
        return
    teamIdentifier = getMobileProvisionItem(provisionFilePath, 'TeamIdentifier') #MNxxxxx8
    provisionUUID = getMobileProvisionItem(provisionFilePath, 'UUID')
    provisionName = getMobileProvisionItem(provisionFilePath, 'Name')
    # type – prints mobileprovision profile type (debug, ad-hoc, enterprise, appstore)
    provisionType = getMobileProvisionItem(provisionFilePath, 'type')
    teamName = getMobileProvisionItem(provisionFilePath, 'TeamName')
    desProvisionFilePath = PROVISONING_PROFILE_DIRECTORY + provisionUUID + provisionFileExtension
    copyFile(provisionFilePath, desProvisionFilePath)

    # Build
    archiveName = "%s_%s.xcarchive" % (applicationId, versionName)
    archiveFilePath = currentDir() + '/' + archiveName
    xcworkspaceFilePath = findFileInDirectory('.xcworkspace', checkoutPath())
    projectSettingParams = 'PRODUCT_BUNDLE_IDENTIFIER=%s PROVISIONING_PROFILE_SPECIFIER=%s PROVISIONING_PROFILE=%s' %(applicationId, provisionName, provisionUUID)
    archiveCmd = 'xcodebuild -workspace %s -scheme %s -configuration %s archive -archivePath %s -destination generic/platform=iOS build %s' % (xcworkspaceFilePath, 'packProject', mode, archiveFilePath, projectSettingParams)
    p = subprocess.Popen(archiveCmd, shell=True, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print ("[packageFailed]: %s") %p.stderr.read()
        return

    # Create 'exportOptions.plist' file and export ipa.
    exportOptionsPlistFilePath = currentDir() + '/' + 'exportOptions.plist'
    addPlistEntry(exportOptionsPlistFilePath, 'provisioningProfiles', 'dict', '')
    addPlistEntry(exportOptionsPlistFilePath, 'provisioningProfiles:'+ applicationId, 'string', provisionUUID)
    addPlistEntry(exportOptionsPlistFilePath, 'teamID', 'string', teamIdentifier)
    # {app-store, ad-hoc, enterprise, development}
    method = 'development' if cmp(provisionType, 'debug') == 0 else provisionType
    method = 'app-store' if cmp(method, 'appstore') == 0 else method
    addPlistEntry(exportOptionsPlistFilePath, 'method', 'string', method)
    exportDirectory = buildExportDirectory()
    exportCmd = "xcodebuild -exportArchive -archivePath %s -exportPath %s -exportOptionsPlist %s" % (archiveFilePath, exportDirectory, exportOptionsPlistFilePath)
    p = subprocess.Popen(exportCmd, shell=True, stderr=subprocess.PIPE)
    p.wait()
    if p.returncode != 0:
        print ("[packageFailed]: %s") %p.stderr.read()
    else:
        ipaVersion = str(versionCode) if mode == 'Debug' else versionName
        ipaName = applicationId + '_' + ipaVersion + '.ipa'
        os.rename(exportDirectory + '/packProject.ipa', exportDirectory + '/' + ipaName)
        print("[packageName]: %s") % (ipaName)
        print("[packagePath]: %s") % (exportDirectory)

    cleanArchiveFile(archiveFilePath)

    p = subprocess.Popen('security lock-keychain', shell=True)
    p.wait()

if __name__ == '__main__':
    main()

腳本并不限于將 web 工程打成 app赡译,只是剛好筆者有這樣的需求。歡迎留言交流模捂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捶朵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狂男,更是在濱河造成了極大的恐慌综看,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岖食,死亡現(xiàn)場離奇詭異红碑,居然都是意外死亡,警方通過查閱死者的電腦和手機泡垃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門析珊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔑穴,你說我怎么就攤上這事忠寻。” “怎么了存和?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵奕剃,是天一觀的道長。 經(jīng)常有香客問我捐腿,道長纵朋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任茄袖,我火速辦了婚禮操软,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宪祥。我一直安慰自己聂薪,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布蝗羊。 她就那樣靜靜地躺著胆建,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肘交。 梳的紋絲不亂的頭發(fā)上笆载,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音涯呻,去河邊找鬼凉驻。 笑死,一個胖子當著我的面吹牛复罐,可吹牛的內(nèi)容都是我干的涝登。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼效诅,長吁一口氣:“原來是場噩夢啊……” “哼胀滚!你這毒婦竟也來了趟济?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咽笼,失蹤者是張志新(化名)和其女友劉穎顷编,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剑刑,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡媳纬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了施掏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钮惠。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖七芭,靈堂內(nèi)的尸體忽然破棺而出素挽,到底是詐尸還是另有隱情,我是刑警寧澤狸驳,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布毁菱,位于F島的核電站,受9級特大地震影響锌历,放射性物質(zhì)發(fā)生泄漏贮庞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一究西、第九天 我趴在偏房一處隱蔽的房頂上張望窗慎。 院中可真熱鬧,春花似錦卤材、人聲如沸遮斥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽术吗。三九已至,卻和暖如春帆精,著一層夾襖步出監(jiān)牢的瞬間较屿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工卓练, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留隘蝎,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓襟企,卻偏偏與公主長得像嘱么,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子顽悼,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1曼振、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評論 3 119
  • 教練收了一個新徒弟几迄,大學(xué)老師,肉類專業(yè)的冰评,女生映胁,很個性,中性打扮集索,博士屿愚,世界上不是有三類人嘛汇跨,男人务荆,女人,女博士穷遂。...
    南娼閱讀 238評論 0 0
  • 2018年5月2日王雯感恩日記 1函匕、感恩雨草姐每天解讀彼尚的輕而易舉的富足,我當下的情況沒有太多時間和機會看其他靈...
    安逸O閱讀 231評論 0 0
  • 孩子在慢慢長大,對于孩子的教育始終是糾結(jié)著忌穿。不知道該從哪兒下手抒寂,給予的太多怕是成了溺愛,懲罰過于嚴厲掠剑,又...
    nancy_4c77閱讀 289評論 0 0
  • 寶貝的畫越來越好
    章魚媽媽Mindmap閱讀 67評論 0 0