在 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,點擊下載即可安裝使用输玷。
雖然云打包服務(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)如下:
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赡译,只是剛好筆者有這樣的需求。歡迎留言交流模捂。