Skip to content

Instantly share code, notes, and snippets.

@nicky-zs
Created November 7, 2014 03:32
Show Gist options
  • Save nicky-zs/ae5e96b51550123d99ec to your computer and use it in GitHub Desktop.
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.
#!/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" Default@2x.png
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" Default@2x.png &
convert "$origin_cover" -resize "640x1136^" -gravity center -crop "640x1136+0+0" Default-568h@2x.png &
wait
[ -e Default@2x.png ] && [ -e Default.png ] && [ -e Default-568h@2x.png ] \
|| 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" Default-568h@2x.png
}
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