Created
November 7, 2014 03:32
-
-
Save nicky-zs/ae5e96b51550123d99ec to your computer and use it in GitHub Desktop.
This is a bash script running on Mac OS with Xcode, which can automatically build an iOS APP from its source project.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# .ipa path: /tmp/packapp/com.xxx.${sub_domain}/build/com.xxx.${sub_domain}.app.ipa | |
# required progs: | |
# wget; | |
# python2.7 ./substitute.py; | |
# lockfile; | |
# imagemagick(convert, identify); | |
# required files: | |
# ./ProjectTemplate | |
# ./ProjectTemplate/Config/config.conf; | |
# ./ProjectTemplate/Config/Icon/; | |
# ./ProjectTemplate/Config/Loading/; | |
# required templates: | |
# ./ProjectTemplate/XXX/XXX-Info.plist | |
# ${DisplayName} | |
# ${BundleId} | |
# ${BundleName} | |
# ./ProjectTemplate/Config/config.conf | |
# ${SiteURL} | |
# Configuration | |
PID=$$ | |
cd $(dirname $0) | |
SCRIPT_DIR=$(pwd) | |
# host Mac OS X enviornment | |
KEYCHAIN="login" | |
KEYCHAIN_PASSWORD_CONF="/opt/xxx/conf/xxx-pwd.conf" | |
PROVISIONING_PROFILE="iPhone Distribution: XXXXXXXXXXXXXXXXXXXXX Co., Ltd." | |
# icon sizes and names | |
ICON_SIZE=(29x29 40x40 50x50 57x57 60x60 72x72 76x76 | |
58x58 80x80 100x100 114x114 120x120 144x144 152x152) | |
ICON_NAME=(29_29 40_40 50_50 57_57 60_60 72_72 76_76 | |
29_29@2x 40_40@2x 50_50@2x 57_57@2x 60_60@2x 72_72@2x 76_76@2x) | |
# building constants | |
DEFAULT_ICON_NAME=default.png | |
DEFAULT_COVER_NAME=default-cover.png | |
DEFAULT_COVER_H_NAME=default-cover-h.png | |
PROJECT_BASE=/tmp/packapp | |
BUNDLE_BASE=com.xxx | |
CLEAN_UP_LOCK=/tmp/xxx-app-clean.lock | |
CLEAN_UP_PID_FILE=/tmp/xxx-app-clean.pid | |
LOG_FILE_NAME=xcodebuild_output | |
MAX_PROJECT_DIR_N=10000 | |
RESERVE_PROJECT_DIR_N=5000 | |
TEMPLATE_DIR="$SCRIPT_DIR/ProjectTemplate" | |
PROJECT_NAME=XXXXXXXXX | |
SCHEME=XXXXXXXX_APP | |
# bash script options | |
display_name= | |
sub_domain= | |
icon_url= | |
cover_url= | |
cover_h_url= | |
site_url= | |
#generated variables | |
bundle_id= | |
project_dir= | |
working_dir= | |
building_lock= | |
building_pid= | |
work_mode= | |
failed() { | |
local error=${1:-"Undefined error"} | |
echo "Failed: $error" >&2 | |
exit 1 | |
} | |
usage() { | |
echo "usage: $0 <options>" | |
echo | |
echo "This script works in both strict mode and loose mode. | |
Strict-Mode: | |
If --cover_h_url option is given, this script will work in strict mode, | |
which means that all the image resources must be given in CORRECT size and format; | |
Loose-Mode: | |
If --cover_h_url option is NOT given, this script will work in loose mode, | |
which means that all the image resources will NOT be validated. | |
Options: | |
--display_name <display_name> The name of the APP which is to be displayed on iPhone. | |
--sub_domain <sub_domain> The level 2 domain used as the bundle id of the APP. | |
--icon_url <icon_url> The URL of the APP's icon. | |
--cover_url <cover_url> The URL of the APP's cover. | |
--cover_h_url <cover_h_url> The URL of the APP's cover for iPhone5. | |
--site_url <site_url> The URL of the user's content. | |
" | |
} | |
check_options_and_generate_variables() { | |
if [ -z "$display_name" ] || [ -z "$sub_domain" ] \ | |
|| [ -z "$icon_url" ] || [ -z "$cover_url" ] || [ -z "$site_url" ] \ | |
|| [[ $sub_domain =~ \. ]] | |
then | |
usage $0 | |
exit 1 | |
fi | |
echo "Options: | |
--display_name $display_name | |
--sub_domain $sub_domain | |
--icon_url $icon_url | |
--cover_url $cover_url | |
--cover_h_url $cover_h_url | |
--site_url $site_url | |
" | |
bundle_id="$BUNDLE_BASE.$sub_domain" | |
project_dir="$PROJECT_BASE/$bundle_id" | |
working_dir="$project_dir/build" | |
building_lock="$PROJECT_BASE/lock.$bundle_id" | |
building_pid="$PROJECT_BASE/pid.$bundle_id" | |
[ -n "$cover_h_url" ] && work_mode="STRICT" || work_mode="LOOSE" | |
echo "This script will work in $work_mode mode." | |
echo | |
echo "Arguments: | |
Bundle ID: $bundle_id | |
Project Directory: $project_dir | |
" | |
} | |
parse_command_line() { | |
while [ $# -ne 0 ] | |
do | |
case $1 in | |
"--display_name") | |
display_name=$2 | |
;; | |
"--sub_domain") | |
sub_domain="$2" | |
;; | |
"--icon_url") | |
icon_url=$2 | |
;; | |
"--cover_url") | |
cover_url=$2 | |
;; | |
"--cover_h_url") | |
cover_h_url=$2 | |
;; | |
"--site_url") | |
site_url=$2 | |
;; | |
*) | |
echo "unknown option" | |
usage $0 | |
exit 1 | |
;; | |
esac | |
shift 2 | |
done | |
check_options_and_generate_variables $0 | |
} | |
ensure_working_dirs() { | |
[ -d "/tmp" ] || mkdir -p "/tmp" | |
[ -d "$PROJECT_BASE" ] || mkdir -p "$PROJECT_BASE" | |
} | |
building_process_not_exist() { | |
local pid_file="$1" | |
[ ! -e "$pid_file" ] || ! ps -eo "pid command" | grep ios-app-build\.sh | grep -q ^\s*$(cat "$pid_file") | |
} | |
unlock_building() { | |
rm -f "$building_pid" | |
rm -f "$building_lock" | |
trap - 0 | |
echo "Unlock $building_lock" | |
} | |
signal_unlock_building() { | |
unlock_building | |
failed "Signal received." | |
} | |
lock_building() { | |
echo "Trying to get building lock $building_lock..." | |
while ! lockfile -1 -r 15 "$building_lock" | |
do | |
if building_process_not_exist "$building_pid" | |
then | |
echo "Locking process does NOT exist... Remove lock." | |
rm -f "$building_lock" | |
rm -f "$building_pid" | |
continue | |
fi | |
echo "Trying to get building lock $building_lock..." | |
sleep 3 | |
done | |
trap signal_unlock_building 0 | |
echo $PID > "$building_pid" | |
echo "Lock $building_lock" | |
} | |
unlock_clean() { | |
rm -f "$CLEAN_UP_LOCK" | |
rm -f "$CLEAN_UP_PID_FILE" | |
} | |
signal_unlock_clean() { | |
unlock_clean | |
failed "Signal received." | |
} | |
clean_up() { | |
local cwd=$(pwd) | |
cd "$PROJECT_BASE" | |
if [ $(ls -1 | grep -c ^com) -gt $MAX_PROJECT_DIR_N ] | |
then | |
echo "Warning: Too many project directories, start to clean..." | |
# lock to make sure that only one process is doing the cleaning work | |
echo "Trying to get clean up lock..." | |
while ! lockfile -1 -r 15 "$CLEAN_UP_LOCK" | |
do | |
if building_process_not_exist "$CLEAN_UP_PID_FILE" | |
then | |
echo "Locking process does NOT exist... Remove lock." | |
rm -f "$CLEAN_UP_LOCK" | |
rm -f "$CLEAN_UP_PID_FILE" | |
continue | |
fi | |
echo "Trying to get clean up lock..." | |
sleep 1 | |
done | |
trap signal_unlock_clean 0 | |
echo $PID > "$CLEAN_UP_PID_FILE" | |
# wait running scripts to finish ... | |
local start_time=$(date +%s) | |
while ls -1 | grep -q ^lock | |
do | |
echo "Waiting running scripts to finish ..." | |
sleep 1 | |
local current_time=$(date +%s) | |
if [ $(( $current_time - $start_time )) -gt 3 ] | |
then | |
for lock_file in lock.* | |
do | |
local pid_file="pid.${lock_file#lock.}" | |
if building_process_not_exist "$pid_file" | |
then | |
echo "Locking process does NOT exist... Remove lock." | |
rm -f "$lock_file" | |
rm -f "$pid_file" | |
fi | |
done | |
fi | |
done | |
# double check is necessary | |
if [ $(ls -1 | grep -c ^com) -gt $MAX_PROJECT_DIR_N ] | |
then | |
local del_project_dir_n=$(( $(ls -1 | grep -c ^com) - $RESERVE_PROJECT_DIR_N )) | |
[ $del_project_dir_n -gt 0 ] || del_project_dir_n=0 | |
# clean up $PROJECT_BASE directory if there are too many projects there | |
ls -1t | grep ^com | tail -n $(( $del_project_dir_n )) | xargs rm -fr | |
# $HOME/Library/Developer/Xcode/DerivedData/ will also be cleaned | |
cd "$HOME/Library/Developer/Xcode/DerivedData/" | |
del_project_dir_n=$(( $(ls -1 | grep -c ^$PROJECT_NAME-) - $RESERVE_PROJECT_DIR_N )) | |
[ $del_project_dir_n -gt 0 ] || del_project_dir_n=0 | |
ls -1t | grep ^$PROJECT_NAME- | tail -n $(( $del_project_dir_n )) | xargs rm -fr | |
fi | |
unlock_clean | |
trap - 0 | |
echo "Finish clean all the project directories." | |
fi | |
cd "$cwd" | |
} | |
validate_keychain() { | |
. "$KEYCHAIN_PASSWORD_CONF" | |
local KeyChain="$HOME/Library/Keychains/$KEYCHAIN.keychain" | |
# unlock the keychain containing the provisioning profile's private key and set it as the default keychain | |
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KeyChain" | |
security default-keychain -s "$KeyChain" | |
# describe the available provisioning profiles | |
echo "Available provisioning profiles" | |
security find-identity -p codesigning -v | |
# verify that the requested provisioning profile can be found | |
(security find-certificate -a -c "$PROVISIONING_PROFILE" -Z | grep ^SHA-1) \ | |
|| failed "Provisioning_profile failed." | |
} | |
validate_image_size() { | |
local func="validate_image_size" | |
local image_path=${1:?"$func: No image path specified."} | |
local required_size=${2:?"$func: No required size specified."} | |
local size=$(identify -format "%Wx%H" "$image_path") | |
[ "$size" = "$required_size" ] || failed "Bad image($image_path) size: $size." | |
} | |
validate_image_format() { | |
local func="validate_image_format" | |
local image_path=${1:?"$func: No image path specified."} | |
local required_format=${2:?"$func: No required format specified."} | |
local format=$(identify -format "%m" "$image_path") | |
shopt -s nocasematch | |
[[ "${format}" = "${required_format}" ]] || failed "Bad image($image_path) format: $format." | |
shopt -u nocasematch | |
} | |
validate_image_convert() { | |
local func="validate_image_convert" | |
local image_path=${1:?"$func: No image path specified."} | |
convert "$image_path" - >/dev/null 2>&1 || failed "Bad image($image_path), can't convert." | |
} | |
generate_icons() { | |
cd "$project_dir/Config/Icon" | |
local origin_icon="icon" | |
wget --max-redirect=0 --timeout=3 -o /dev/null -O "$origin_icon" "$icon_url" \ | |
|| failed "Download icon($icon_url) failed." | |
if [ "$work_mode" = "STRICT" ] | |
then | |
validate_image_size "$origin_icon" "152x152" | |
validate_image_format "$origin_icon" "PNG" | |
validate_image_convert "$origin_icon" | |
else | |
convert "$origin_icon" -resize "152x152^" -gravity center -crop "152x152+0+0" "$origin_icon" \ | |
|| failed "Bad icon($icon_url), can't convert." | |
fi | |
local i=0 | |
for size in ${ICON_SIZE[@]} | |
do | |
convert "$origin_icon" -resize "$size" "icon${ICON_NAME[$i]}.png" & | |
i=$(( $i + 1 )) | |
done | |
wait | |
# if generating icons failed, failed | |
for name in ${ICON_NAME[@]} | |
do | |
[ -e "icon$name.png" ] || failed "Generating icons failed." | |
done | |
} | |
generate_covers() { | |
cd "$project_dir/Config/Loading" | |
local origin_cover="cover@2x" | |
wget --max-redirect=0 --timeout=3 -o /dev/null -O "$origin_cover" "$cover_url" \ | |
|| failed "Download cover($cover_url) failed." | |
if [ "$work_mode" = "STRICT" ] | |
then | |
validate_image_size "$origin_cover" "640x960" | |
validate_image_format "$origin_cover" "PNG" | |
convert "$origin_cover" -resize "320x480" Default.png || failed "Generating covers failed." | |
cp "$origin_cover" [email protected] | |
else | |
convert "$origin_cover" -resize "320x480^" -gravity center -crop "320x480+0+0" Default.png & | |
convert "$origin_cover" -resize "640x960^" -gravity center -crop "640x960+0+0" [email protected] & | |
convert "$origin_cover" -resize "640x1136^" -gravity center -crop "640x1136+0+0" [email protected] & | |
wait | |
[ -e [email protected] ] && [ -e Default.png ] && [ -e [email protected] ] \ | |
|| failed "Generating covers failed." | |
fi | |
} | |
generate_covers_h() { | |
cd "$project_dir/Config/Loading" | |
local origin_cover_h="cover_h" | |
if wget --max-redirect=0 --timeout=3 -o /dev/null -O "$origin_cover_h" "$cover_h_url" | |
then | |
validate_image_size "$origin_cover_h" "640x1136" | |
validate_image_format "$origin_cover_h" "PNG" | |
else | |
failed "Download cover_h($cover_h_url) failed." | |
fi | |
cp "$origin_cover_h" [email protected] | |
} | |
generate_resources() { | |
if [ "$work_mode" = "STRICT" ] | |
then | |
# generating $project_dir/Config/Icon/* | |
generate_icons & local icon_pid=$! | |
# generating $project_dir/Config/Loading/* | |
generate_covers & local cover_pid=$! | |
generate_covers_h & local cover_h_pid=$! | |
wait $icon_pid || failed "Generating icons failed." | |
wait $cover_pid || failed "Generating covers failed." | |
wait $cover_h_pid || failed "Generating covers_h failed." | |
else | |
# generating $project_dir/Config/Icon/* | |
generate_icons & local icon_pid=$! | |
# generating $project_dir/Config/Loading/* | |
generate_covers & local cover_pid=$! | |
wait $icon_pid || failed "Generating icons failed." | |
wait $cover_pid || failed "Generating covers failed." | |
fi | |
} | |
generate_project() { | |
echo "Generating project to: $project_dir" | |
cd "$SCRIPT_DIR" | |
if [ -d "$project_dir" ] | |
then | |
# if the $project_dir exists but it's older than $TEMPLATE_DIR, remove it | |
if [ $(stat -f %m "$TEMPLATE_DIR") -gt $(stat -f %m "$project_dir") ] | |
then | |
echo "Warning: Project dir $project_dir is older, drop it." | |
rm -fr "$project_dir" | |
# if the $project_dir exists but it's not a valid xcode project dir, remove it | |
else | |
cd "$project_dir" | |
if ! xcodebuild -list > /dev/null 2>&1 | |
then | |
echo "Warning: Project dir $project_dir is invalid, drop it." | |
cd "$SCRIPT_DIR" | |
rm -fr "$project_dir" | |
fi | |
cd "$SCRIPT_DIR" | |
fi | |
fi | |
if [[ ! -e "$project_dir" ]] | |
then | |
rm -fr "$project_dir" | |
mkdir -p "$project_dir" | |
cp -R "$TEMPLATE_DIR"/* "$project_dir" | |
fi | |
# generating $project_dir/XXX/XXX-Info.plist | |
"$SCRIPT_DIR/substitute.py" \ | |
DisplayName="$display_name" \ | |
BundleId="$bundle_id" \ | |
BundleName="${sub_domain}" \ | |
< "$TEMPLATE_DIR/XXX/XXX-Info.plist" \ | |
> "$project_dir/XXX/XXX-Info.plist" | |
# generating $project_dir/Config/config.conf | |
"$SCRIPT_DIR/substitute.py" \ | |
SiteURL="$site_url" \ | |
< "$TEMPLATE_DIR/Config/config.conf" \ | |
> "$project_dir/Config/config.conf" | |
# generating image resources in $project_dir/Config/ | |
generate_resources | |
cd "$SCRIPT_DIR" | |
} | |
build_app() { | |
# generating everything... | |
generate_project | |
local devired_data_path="$HOME/Library/Developer/Xcode/DerivedData" | |
mkdir -p "$working_dir" | |
echo "Running xcodebuild > $working_dir/$LOG_FILE_NAME ..." | |
cd "$project_dir" | |
xcodebuild -verbose -scheme "$SCHEME" -sdk iphoneos \ | |
-configuration Release clean build > "$working_dir/$LOG_FILE_NAME" | |
local build_result=$? | |
cd "$SCRIPT_DIR" | |
if [ $build_result -ne 0 ] | |
then | |
if [ "$1" != "last_time" ] | |
then | |
echo "Warning: Building failed, retry once..." | |
rm -fr "$project_dir" | |
build_app last_time | |
else | |
echo "Error: Building failed." | |
tail -n20 "$working_dir/$LOG_FILE_NAME" | |
failed "xcodebuild failed." | |
fi | |
fi | |
# locate this project's DerivedData directory | |
local project_derived_data_directory=$( \ | |
grep -oE "$PROJECT_NAME-([a-zA-Z0-9]+)[/]" "$working_dir/$LOG_FILE_NAME" \ | |
| sed -n "s/\(${PROJECT_NAME//\//\\/}-[a-z]\{1,\}\)\//\1/p" | head -n 1) | |
local project_derived_data_path="$devired_data_path/$project_derived_data_directory" | |
local release_dir="$project_derived_data_path/Build/Products/Release-iphoneos" | |
# locate the .app file | |
# infer app name since it cannot currently be set using the product name, see comment above | |
project_app=$(ls -1 "$release_dir/" | grep ".*\.app$" | head -n1) | |
if [ $(ls -1 "$release_dir/" | grep ".*\.app$" | wc -l) -ne 1 ] | |
then | |
failed "Failed to find a single .app build product." | |
fi | |
echo "Built $project_app in $project_derived_data_path" | |
echo "Retrieving build products..." | |
cp -Rf "$release_dir/$project_app" "$working_dir" | |
rm -rf "$working_dir/$bundle_id.app" | |
mv -f "$working_dir/$project_app" "$working_dir/$bundle_id.app" | |
echo "$working_dir/$bundle_id.app" | |
cp -Rf "$release_dir/$project_app.dSYM" "$working_dir" | |
rm -rf "$working_dir/$bundle_id.app.dSYM" | |
mv -f "$working_dir/$project_app.dSYM" "$working_dir/$bundle_id.app.dSYM" | |
echo "$working_dir/$bundle_id.app.dSYM" | |
project_app="$bundle_id.app" | |
} | |
sign_app() { | |
local mobileprovision="$working_dir/$project_app/embedded.mobileprovision" | |
local output_file="$working_dir/$project_app.ipa" | |
echo "Codesign as \"$PROVISIONING_PROFILE\"" | |
echo "Embedding provisioning profile $mobileprovision" | |
xcrun -sdk iphoneos PackageApplication "$working_dir/$project_app" \ | |
-o "$output_file" \ | |
--sign "$PROVISIONING_PROFILE" \ | |
--embed "$mobileprovision" || failed "Codesign failed." | |
echo "Output File: $output_file" | |
} | |
verify_app() { | |
codesign -d -vvv --file-list - "$working_dir/$project_app" || failed "Verification failed." | |
} | |
echo | |
echo "==================== Start building... ====================" | |
start_time=$(python -c "import time; print time.time()") | |
parse_command_line $@ | |
ensure_working_dirs | |
clean_up | |
echo | |
lock_building | |
echo | |
echo "***** Validate Keychain *****" | |
validate_keychain | |
echo | |
echo "***** Build Project *****" | |
build_app | |
echo | |
echo "***** Package Application *****" | |
sign_app | |
echo | |
echo "***** Verify Application *****" | |
verify_app | |
echo | |
cost_time=$(python -c "import time; print time.time() - $start_time") | |
echo "***** Complete! Cost $cost_time seconds. *****" | |
unlock_building | |
echo "==================== Finish ====================" | |
echo |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment