Xcodeで提供されている基本的なコマンドラインツールでiOSアプリをビルドし、AppStoreに提出する方法を記述。
使うコマンドラインツール
Xcodeの選択
複数のバージョンのXcodeが入っている場合はxcode-select -s <パス>
で使用するバージョンを選択できる。
$ sudo xcode-select -s '/Applications/Xcode 8.app/Contents/Developer'
$ xcode-select -p
/Applications/Xcode 8.app/Contents/Developer
ビルドはXcodeに含まれているxcodebuild
を使う。
$ xcodebuild -version
Xcode 8.0
Build version 8A218a
アプリのビルド
アーカイブを作成してIPAをエクスポートする。
$ xcodebuild \
-workspace myapp.xcworkspace \
-scheme myapp \
-configuration Debug \
archive \
-archivePath . \
HOGE_FEATURE_DISABLE=1
# (ビルドログ省略)
$ xcodebuild \
-exportArchive \
-archivePath . \
-exportPath ./build \
-exportOptionsPlist ./build/exportOptions.plist
# (ビルドログ省略)
上記の例では、
- 作成されるアーカイブ:
./myapp.xcarchive
ディレクトリ。 - 作成されるIPA:
./build/myapp.ipa
。 - dSYMのありか:
./build/myapp.xcarchive/dSYMs
ディレクトリ。 - ソース内で
#if HOGE_FEATURE_DISABLE
で囲んだコードが有効。 - エクスポートパラメータである
./build/exportOptions.plist
は作るIPAの種類によって内容が違う。
エクスポートパラメータ
xcodebuild
のエクスポートパラメータ(-exportOptionsPlist
)に指定するファイルはplist形式。
AppStore向けのエクスポートかどうかで指定するプロパティが別れる。
プロパティ一覧はxcodebuild -h
で確認できる。
AppStoreに提出するIPA
<?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>method</key>
<string>app-store</string>
<key>teamID</key>
<string>XXXXXXXXXX</string>
</dict>
</plist>
method
プロパティにapp-store
を指定する。
Enterprise、Ad Hoc等で展開するIPA
<?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>method</key>
<string>enterprise</string>
<key>teamID</key>
<string>XXXXXXXXXX</string>
<key>compileBitcode</key>
<false/>
<key>embedOnDemandResourcesAssetPacksInBundle</key>
<false/>
</dict>
</plist>
-
method
プロパティにad-hoc
、enterprise
、development
等が指定できる。(標準:development
) - bitcodeに対応していない場合、
compileBitcode
プロパティをfalseにして、一からビルドするように指摘できる。
コードの署名
xcodebuild
でビルド中の最後の方でコードの署名(codesign
)が発生する。
codesign
がチームの証明書と鍵が入ったキーチェインへアクセスする際、パスワードの入力GUIが出てしまう場合がある。
次の2項目(両方)を実行しておくとGUIが出るのを回避できる模様。
- ビルド直前に、キーチェインのロック解除をする。(ロック解除は制限時間があるが、その制限時間も設定できる。)
- 鍵のアクセスを「全てのアプリケーションを許可」状態にしておく。
注意点:証明書は「常に信頼」にすると問題が発生する模様。証明書は変更せず「システムデフォルト」のままにしておく。鍵のアクセスは変更しても大丈夫な模様。
鍵のアクセスを「全てのアプリケーションを許可」
チームの証明書・鍵を含むp12ファイルを-A
でキーチェインにインポートすると、全アクセス許可状態で鍵が入る。
$ security import MyCertificate.p12 \
-k ~/Library/Keychains/login.keychain \
-P MyCertificatePassword \
-A
$ security help import
Usage: import inputfile [-k keychain] [-t type] [-f format] [-w] [-P passphrase] [options...]
-k Target keychain to import into
#(省略)
-A Allow any application to access the imported key without warning (insecure, not recommended!)
キーチェインのロック解除
ビルド直前にチームの証明書と鍵が入ったキーチェインをロック解除する。通常ロック解除は制限時間があるが、その制限時間も設定できる。
$ security unlock-keychain -p Password ~/Library/Keychains/login.keychain
ロック解除の制限時間を無くす場合:
$ security set-keychain-settings ~/Library/Keychains/login.keychain
$ security unlock-keychain -p Password ~/Library/Keychains/login.keychain
$ security help set-keychain-settings
Usage: set-keychain-settings [-lu] [-t timeout] [keychain]
-l Lock keychain when the system sleeps
-u Lock keychain after timeout interval
-t Timeout in seconds (omitting this option specifies "no timeout")
どのキーチェインに入れるか
ログイン中にビルドする場合
ターミナルやSSH上やXcode上等、特定のユーザーでログインしてビルドする場合は標準キーチェインがログインキーチェインになっている。よって、ログインキーチェインに証明書と鍵が入っていれば、それらが参照される。
$ security list-keychains
"/Users/rsahara/Library/Keychains/login.keychain"
"/Library/Keychains/System.keychain"
$ security default-keychain
"/Users/rsahara/Library/Keychains/login.keychain"
launchdデーモンでビルドする場合
launchd
デーモンの場合、システムキーチェインしか使えない初期状態になっている。
実際にビルドができた方法は次のパターン:
- ログインキーチェインに証明書と鍵を入れる方法:
launchd
デーモンの設定ファイルのUserName
プロパティにビルドユーザーを指定し、SessionCreate
プロパティにtrue
を指定すると、そのユーザーでログイン中と同様にログインキーチェインが標準キーチェインになる。(launchd
パラメータについてはこちらに記述。) - システムキーチェインに証明書と鍵を入れる方法:
launchd
デーモンの場合、システムキーチェインが標準キーチェインになっているので、そのまま使える。
前者のログインキーチェインを使うのが良さげだった理由:
ログインキーチェインに証明書と鍵を入れておけばXcodeでビルドするときも、上の設定でlaunchd
デーモンからビルドするときでも、同じログインキーチェインにある証明書と鍵が参照されるのでテストしやすくなる利点がある。
AppStoreにIPAをアップロード
Xcodeに含まれているaltool
(Application Loader)を使って提出できる。
パスが長いし空白が含まれてる。
$ ALTOOL_ABSOLUTEPATH="$(xcode-select -p)/../Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool"
$ "${ALTOOL_ABSOLUTEPATH}" --upload-app \
--file ./build/myapp.ipa \
--username appleid@domain.jp \
--password password
$ "${ALTOOL_ABSOLUTEPATH}" --validate-app \
--file ./build/myapp.ipa \
--username appleid@domain.jp \
--password password
ビルドスクリプト一例
ユースケース
- コマンドラインでビルドしたい。
- 設定を簡単に編集できるようにしたい。
- 別のスクリプトからも呼び出したい(BOTやlaunchdで定期ビルド等)。
- 呼び出し側から設定を指定できるようにしたい。
- ビルドログが見れるようにしたい(でも基本的にログは見たくない)。
入れる機能
- 設定が足りない場合やおかしいときはすぐに分かる。
- タイムアウトを付ける(ついでにビルド時間が分かる)。
- 意図:通信ができない等でエラーは出ないけど止まるときがあるため。
- 結果はJSON形式で返せる。
- 意図:IPAファイルのパス等を呼び出し側は知りたい。
- Xcodeのバージョンが合っているかチェック。
- 意図:プロジェクトごとに違う場合がある。
- ビルド前にスクリプトを指定して実行できる。
- 意図:ライブラリの準備(
pod repo update; pod install
)やXcodeバージョンの選択(xcode-select -p <パス>
)やキーチェインのロック解除(security unlock-keychain -p <パスワード> <キーチチェインファイル>
)が事前にできる。
- 意図:ライブラリの準備(
- ビルド後にスクリプトを指定して実行できる。
- 意図:dSYMをバグトラッカーに提出、IPAをAppStoreに提出したりコピーする等の用途のため。
- ファイル名にアプリのバージョンとビルド時間を指定できる:
- 例:出力ファイル名
app_#VERSION#_#DATETIME#.ipa
と指定したら、app_1.0.0_20170118_1206.ipa
が作成される。
- 例:出力ファイル名
- ビルド設定は環境変数で渡す。
- スクリプトの中で設定を簡単に編集できるようにする:
SETTING=VALUE
を上の方に書く。 - 別スクリプトで設定を指定するときは同じ方法で統一:
SETTING=VALUE
してからsource build.sh
。 - 外部からスクリプトを呼び出す場合は:
SETTING=VALUE; export SETTING; build.sh
。
- スクリプトの中で設定を簡単に編集できるようにする:
環境
- ソースが取得できる。
- SSHを使う場合、SSHキー等が設定されている。
- Xcodeが入っている。
- 複数バージョンを入れている場合は
xcode-select
で事前に設定するか、ビルド前にスクリプトを実行する機能を利用して設定する。
- 複数バージョンを入れている場合は
- 必要なプロビジョニングプロファイルがXcodeに入っている。
- チームの証明書と鍵が参照対象のキーチェインに入っている。
- ビルド前にスクリプトを実行する機能を利用してそのキーチェインをロック解除できる。
- ビルドでexportOptionsPlistに指定するplistファイルがどこかにある (ソースに含まれている、又はどこかアクセス可能な固定のパスにある)。
ベーススクリプト
#!/bin/bash
# ===
# Copyright 2017 Runo Sahara
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ===
set -e
# === QUICK SETTINGS
#BUILD_USEQUICKSETTINGS="YES" # Uncomment this line if you want to use the quick settings below.
[ -z "$BUILD_USEQUICKSETTINGS" ] || {
# - Required settings.
# Source to build, specified by a git url and branch name (recommended),
BUILD_GIT_URL="https://github.com/rsahara/project.git"
BUILD_GIT_BRANCH="master"
# or directly by a script to deploy the source in $BUILD_SRC_DIR of the current directory.
#BUILD_DEPLOYSCRIPT='git clone -b "$BUILD_GIT_BRANCH" "$BUILD_GIT_URL" "$BUILD_SRC_DIR"'
# Xcode configuration (Debug, Release, etc...).
BUILD_XCODE_CONFIGURATION="Debug"
# Path to a plist file containing the export options, relative to the root of the repository.
BUILD_EXPORTOPTIONS_PATH="build/exportOptions.plist"
# The project files can be specified by the project name (recommended),
BUILD_PROJECTNAME="testproject"
# or with the actual file paths and scheme.
#BUILD_WORKSPACE_PATH="$BUILD_PROJECTNAME.xcworkspace"
#BUILD_ARCHIVE_PATH="$BUILD_PROJECTNAME.xcarchive"
#BUILD_IPA_PATH="$BUILD_PROJECTNAME.ipa"
#BUILD_XCODE_SCHEME="$BUILD_PROJECTNAME"
# - The output file. (Optional)
# #VERSION# will be replaced with the app version, #DATETIME# with the current datetime.
#BUILD_OUTPUT_FORMAT="${BUILD_PROJECTNAME}_#VERSION#_#DATETIME#.ipa"
# Path to the Info.plist file of the project. Required if #VERSION# is used in BUILD_OUTPUT_FORMAT.
#BUILD_INFOPLIST_PATH=""
# - Other settings. (Optional)
# Pre-build script to be executed right before the build command, in the source directory.
#BUILD_PREBUILDSCRIPT="pod update; security unlock-keychain -p password"
# Post-build script to be executed after the build is done, in the source directory. (${BUILD_SUCCEEDED} is set to "YES" if the build succeeded.)
#BUILD_POSTBUILDSCRIPT=""
# Verify that the correct version of Xcode is being used.
#BUILD_XCODE_VERSION="8.0"
# Additional parameter to be added to the build command line of xcodebuild.
#BUILD_XCODE_PARAM="DEBUG_TEST=1"
# Show the result in JSON dictionary like format.
#BUILD_JSONRESULT="YES"
# Print the settings and don't build.
#BUILD_PRINTSETTINGSONLY="YES"
# Specify the build directory, rather than letting the script use a random temporary directory.
#BUILD_DIR_ABSOLUTEPATH="/tmp/build"
# Specify the timeout in seconds. (Default: 1000 seconds)
#BUILD_TIMEOUT="1000"
}
# === LOAD SETTINGS
BUILD_JSONRESULTEMPTY="YES"
BUILD_SUCCEEDED="NO"
function build_print() {
if [ "$BUILD_JSONRESULT" = "YES" ]; then
if [ "$BUILD_JSONRESULTEMPTY" = "YES" ]; then
printf "\"$1\":\"$2\""
BUILD_JSONRESULTEMPTY="NO"
else
printf ",\n\"$1\":\"$2\""
fi
else
echo "$1: $2"
fi
}
function build_setting_require() {
[ ! -z "${!1}" ] || { echo "$1 not set" >&2 && exit 1; }
}
function build_setting_default() {
local default=$2
[ ! -z "${!1}" ] || eval "$1"=\"\$default\"
}
function build_setting_print() {
build_print "$1" "${!1}"
}
build_setting_default BUILD_JSONRESULT "NO"
if [ -z "$BUILD_GIT_URL" ]; then
build_setting_require BUILD_DEPLOYSCRIPT
else
build_setting_default BUILD_GIT_BRANCH "master"
build_setting_default BUILD_DEPLOYSCRIPT 'git clone -b "$BUILD_GIT_BRANCH" "$BUILD_GIT_URL" "$BUILD_SRC_DIR"'
fi
build_setting_require BUILD_XCODE_CONFIGURATION
build_setting_require BUILD_EXPORTOPTIONS_PATH
build_setting_default BUILD_PREBUILDSCRIPT ""
build_setting_default BUILD_SRC_DIR "build"
[ ! -z "$BUILD_DIR_ABSOLUTEPATH" ] || BUILD_DIR_ABSOLUTEPATH=`mktemp -d`
BUILD_WORKINGDIR_ABSOLUTEPATH="$(pwd)"
build_setting_default BUILD_DATETIME $(date +"%Y%m%d_%H%M")
build_setting_default BUILD_INFOPLIST_PATH ""
build_setting_default BUILD_XCODE_PARAM ""
build_setting_default BUILD_BUILD_PROJECTNAME "projectname"
build_setting_default BUILD_XCODE_SCHEME "$BUILD_PROJECTNAME"
build_setting_default BUILD_WORKSPACE_PATH "$BUILD_PROJECTNAME.xcworkspace"
build_setting_default BUILD_ARCHIVE_PATH "$BUILD_PROJECTNAME.xcarchive"
build_setting_default BUILD_IPA_PATH "$BUILD_PROJECTNAME.ipa"
build_setting_default BUILD_OUTPUT_FORMAT "${BUILD_PROJECTNAME}_#VERSION#_#DATETIME#.ipa"
build_setting_default BUILD_TIMEOUT "1000"
build_setting_default BUILD_PRINTSETTINGSONLY "NO"
build_setting_print BUILD_DIR_ABSOLUTEPATH
build_setting_print BUILD_JSONRESULT
build_setting_print BUILD_WORKINGDIR_ABSOLUTEPATH
build_setting_print BUILD_DATETIME
[ -z "$BUILD_GIT_URL" ] || build_setting_print BUILD_GIT_URL
[ -z "$BUILD_GIT_BRANCH" ] || build_setting_print BUILD_GIT_BRANCH
build_setting_print BUILD_SRC_DIR
build_setting_print BUILD_XCODE_CONFIGURATION
build_setting_print BUILD_XCODE_PARAM
build_setting_print BUILD_INFOPLIST_PATH
build_setting_print BUILD_XCODE_SCHEME
build_setting_print BUILD_WORKSPACE_PATH
build_setting_print BUILD_ARCHIVE_PATH
build_setting_print BUILD_IPA_PATH
build_setting_print BUILD_EXPORTOPTIONS_PATH
build_setting_print BUILD_TIMEOUT
# === BUILD
BUILD_REMAININGTIME="$BUILD_TIMEOUT"
# Prepare build directory.
{
[ "$BUILD_PRINTSETTINGSONLY" == "YES" ] || {
rm -fr "$BUILD_DIR_ABSOLUTEPATH"
rm -f "$BUILD_LOG_ABSOLUTEPATH"
}
dirname "$BUILD_LOG" | xargs mkdir -p
mkdir -p "$BUILD_DIR_ABSOLUTEPATH"
BUILD_LOG_ABSOLUTEPATH=`mktemp ${BUILD_DIR_ABSOLUTEPATH}/build.${BUILD_DATETIME}.XXXXXX`
} &> /dev/null
build_setting_print BUILD_LOG_ABSOLUTEPATH
# Build on another process.
BUILD_BEGINTIME=$(date +%s)
{
[ -z "$BUILD_XCODE_VERSION" ] || {
[ "Xcode $BUILD_XCODE_VERSION" = "`xcodebuild -version | head -n 1`" ] || { echo "Xcode version mismatch" >&2 && exit 1; }
}
[ "$BUILD_PRINTSETTINGSONLY" != "YES" ] || exit 0
cd "$BUILD_DIR_ABSOLUTEPATH"
eval "$BUILD_DEPLOYSCRIPT"
cd "$BUILD_DIR_ABSOLUTEPATH/$BUILD_SRC_DIR"
[ -z "$BUILD_PREBUILDSCRIPT" ] || eval "$BUILD_PREBUILDSCRIPT"
cd "$BUILD_DIR_ABSOLUTEPATH/$BUILD_SRC_DIR"
eval xcodebuild -workspace "$BUILD_WORKSPACE_PATH" \
-scheme "$BUILD_XCODE_SCHEME" \
-configuration "$BUILD_XCODE_CONFIGURATION" \
archive \
-archivePath "$BUILD_ARCHIVE_PATH" \
"$BUILD_XCODE_PARAM"
eval xcodebuild -exportArchive \
-archivePath "$BUILD_ARCHIVE_PATH" \
-exportPath "$BUILD_DIR_ABSOLUTEPATH" \
-exportOptionsPlist "$BUILD_EXPORTOPTIONS_PATH" \
"$BUILD_XCODE_PARAM"
} >> "$BUILD_LOG_ABSOLUTEPATH" 2>&1 &
BUILD_WAITINGPROCESSID=$!
while [ $BUILD_REMAININGTIME -gt 0 ]; do
sleep 1
kill -0 $BUILD_WAITINGPROCESSID &> /dev/null || break
BUILD_REMAININGTIME=$(($BUILD_REMAININGTIME - 1))
done
# Get build results.
if [ -f "$BUILD_DIR_ABSOLUTEPATH/$BUILD_IPA_PATH" ]; then
{
BUILD_VERSION="unknown"
if [ ! -z "$BUILD_INFOPLIST_PATH" ]; then
BUILD_INFOPLIST_ABSOLUTEPATH="$BUILD_DIR_ABSOLUTEPATH/$BUILD_SRC_DIR/$BUILD_INFOPLIST_PATH"
BUILD_VERSION=$(defaults read "${BUILD_INFOPLIST_ABSOLUTEPATH%.plist}" CFBundleShortVersionString)
fi
BUILD_OUTPUT="$BUILD_OUTPUT_FORMAT"
BUILD_OUTPUT="${BUILD_OUTPUT//#VERSION#/$BUILD_VERSION}"
BUILD_OUTPUT="${BUILD_OUTPUT//#DATETIME#/$BUILD_DATETIME}"
cp "$BUILD_DIR_ABSOLUTEPATH/$BUILD_IPA_PATH" "$BUILD_WORKINGDIR_ABSOLUTEPATH/$BUILD_OUTPUT"
BUILD_SUCCEEDED="YES"
} &> /dev/null
build_setting_print BUILD_VERSION
build_setting_print BUILD_OUTPUT
fi
# Post-build script on another process.
if [ $BUILD_REMAININGTIME -gt 0 ] && [ ! -z "$BUILD_POSTBUILDSCRIPT" ]; then
{
cd "$BUILD_DIR_ABSOLUTEPATH/$BUILD_SRC_DIR"
eval "$BUILD_POSTBUILDSCRIPT"
} >> "$BUILD_LOG_ABSOLUTEPATH" 2>&1 &
BUILD_WAITINGPROCESSID=$!
while [ $BUILD_REMAININGTIME -gt 0 ]; do
sleep 1
kill -0 $BUILD_WAITINGPROCESSID &> /dev/null || break
BUILD_REMAININGTIME=$(($BUILD_REMAININGTIME - 1))
done
fi
# Check timeout.
if [ ! $BUILD_REMAININGTIME -gt 0 ]; then
kill -s SIGKILL $BUILD_WAITINGPROCESSID &> /dev/null
echo "Timed out" >> "$BUILD_LOG_ABSOLUTEPATH"
fi
# === RESULT
build_setting_print BUILD_SUCCEEDED
BUILD_ENDTIME=$(date +%s)
BUILD_TIME=$(($BUILD_ENDTIME - $BUILD_BEGINTIME))
build_print "BUILD_TIME" "$BUILD_TIME"
[ "$BUILD_JSONRESULT" != "YES" ] || echo
Github:https://github.com/rsahara/iosbuild.git
派生スクリプト
ベーススクリプトから必要に応じて内容を特化した別スクリプトを派生できる。
例:developブランチをビルドし、成功したらAppStoreにアップロード、クラッシュ調査用のdSYMを処理。
#!/bin/bash
BUILD_GIT_BRANCH="develop"
SCRIPT_ABSOLUTEPATH="$(cd $(dirname $0) && pwd)"
ALTOOL_ABSOLUTEPATH="$(xcode-select -p)/../Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool"
BUILD_GIT_URL="https://github.com/rsahara/project.git"
BUILD_XCODE_CONFIGURATION="Release"
BUILD_EXPORTOPTIONS_PATH="build/exportOptionsAppStore.plist"
BUILD_PROJECTNAME="myapp"
BUILD_OUTPUT_FORMAT="myapp_appstore_#VERSION#_#DATETIME#.ipa"
BUILD_INFOPLIST_PATH="myapp/Info.plist"
BUILD_DIR_ABSOLUTEPATH="/tmp/build/myapp"
BUILD_PREBUILDSCRIPT="pod update; security unlock-keychain -p XXXXXXXX ~/Library/Keychains/login.keychain"
BUILD_POSTBUILDSCRIPT='\
if [ $BUILD_SUCCEEDED = "YES" ]; then \
echo "Uploading to Smartbeat..."; \
${SCRIPT_ABSOLUTEPATH}/../upload_smartbeat.sh \
"$BUILD_DIR_ABSOLUTEPATH/$BUILD_SRC_DIR/$BUILD_ARCHIVE_PATH/dSYMs/myapp.app.dSYM" || true; \
\
echo "Uploading to App Store..."; \
"${ALTOOL_ABSOLUTEPATH}" --upload-app \
--file "$BUILD_DIR_ABSOLUTEPATH/$BUILD_IPA_PATH" \
--username appleid@domain.jp \
--password password || BUILD_SUCCEEDED="NO"; \
fi \
'
BUILD_JSONRESULT="YES"
echo {
source "$SCRIPT_ABSOLUTEPATH/../build.sh"
echo }