preface

Any company with a desire for efficiency should have an automated build system.

The current iOS build process has basically stabilized after two years of use.

The main purpose of this article is to document the Jenkins package đŸ“Ļ script currently used by 📝.

There are many tools for packaging to do similar things, but more importantly why use automated builds:

  1. In terms of efficiency, it frees up developers’ time. It is also more convenient for other colleagues to use.
  2. Ensure standard packaging to avoid failure due to configuration or environment problems. Doing things right is more important than doing them fast.
  3. Permission security, centralized management through the build system, is a black box for users.
  4. In the project process, it is easy to do Daily builds or automated tests when required.

Basic configuration such as how to install Jenkins or Jenkins parameter configuration is not covered.

There are many detailed articles on the Internet. For example, hand in hand with Jenkins continuous integration iOS projects.

The general iOS build process

The overall build process is introduced, which will be described in the following steps.

The corresponding Ruby script used below has been uploaded to the Github repository. Note that the variables are desensitized and modified according to your needs

Before building

  • Setting the build name
  • Configure app icon watermark (build number, branch)
  • Ruby script according to the parameters, modify the project bundleID, macro, etc
  • Install third-party dependencies, Pod Update

Execute a build

  • xcodebuild clean
  • xcodebuild archive
  • xcodebuild exportArchive

Build a complete

  • Upload distribution platform: Dandelion/FIR/Appstore (Historical version record: Git Tag)
  • Symbol table processing: upload bugly
  • Archive product: Upload to FTP server
  • Clearing: Delete the IPA
  • Setting the Build description
  • Notification: Enterprise wechat Webhook robot push

Before building

Setting the build name

First set the name of our build. I use a few parameters here:

  • BUILD_NUMBER, an argument that Jenkins uses to indicate the number of builds
  • BetaPlatform, the option parameter set to represent the distribution platform. My values here are:fir.pgyer.appstore
  • Mode, the option parameter set, represents the Xcode built environment setting, isSnapshot 和 Release
  • Branch,Jenkins parameter, stands for Git Branch name

Configuring APP ICONS

In order to package the tested APP and locate problems conveniently, the APP Logo can be watermarked and key information such as git branch name, Jenkins build number and APP version number can be added.

The process for configuring icon watermarking is as follows:

  • Determine whether this is the appstore distribution platform. If it’s the Appstore, clean up the old icon directory and copy the icon to the directory you’re using.
  • If it is not appstore, it will be distributed for test platform and watermarked.

Replace resources before packaging

Note:

In the past, there were two ways to handle icon substitution. One was to go into the app’s resources after the build was done (this is no longer possible). The other is to directly modify the resources in the project. The current method is to directly modify the icon source file in the project directory. Replace the Logo with a watermark before the build.

Because you will be using the alternative resource approach, prepare two directories.

One directory acts as the source directory for the unprocessed images. A directory serves as the target directory to store the images used by the App Logo.

Why use two picture directories for storage? Suppose only one image is used, and the original image is A. When the image is processed for the first time, it is A A1 watermark image. When the image is obtained for the second time, it is already A processed A1 watermark image, not the original image A.

Note that icons_path is the address where the original image is stored, and icons_dest_path is the destination path to be modified. I’ll call it appicon-internal.

Refer to iOS APP icon versioning

To obtain the version, use Ruby because the current version has changed, and the script will provide a link in the following:

version=$(ruby ./ToolChain/ruby/dy_build_version.rb ${Mode})
Copy the code

There is also a temporary directory, which should be created in advance:

tmp_path="/Users/${sys_username}/Desktop/iOS_IPA/IconVersioning
Copy the code

ImageMagick

Adding watermarks mainly uses the command line tool ImageMagick, so first install:


brew install imagemagick
Install Ghostscript, which provides imagemagick-enabled fonts.
brew install ghostscript

Copy the code

The script content

The specific script is as follows:


#! /bin/bash -l

echo "🐛 ------------- Configure app icon --------------------"

# User name for the local Mac
sys_username="$USER" 
The name of the task built by Jenkins
jenkinsName=${JOB_NAME}
# project name
APP_NAME="your app name"
# Project repo directory
Workspace="${WORKSPACE}"


project_infoplist_path=". /${APP_NAME}/Info.plist"
# Temporary image storage path
tmp_path="/Users/${sys_username}/Desktop/iOS_IPA/IconVersioning"

# If the platform is appstore
if [ "$BetaPlatform" = "appstore" ];then
   echo "🍃🍃🍃 upload platform from appstore 🍃🍃"
   echo "icons_path: ${icons_path}"
   echo "icons_dest_path: ${icons_dest_path}"

#1. Clear the original PNG file
find "${icons_dest_path}" -type f -name "*.png" -print0 |
while IFS= read -r -d ' ' file; do
echo "rm file $file"
rm -rf $file
done

#2. Copy icons_PATH to iconS_dest_path
find "${icons_path}" -type f -name "*.png" -print0 |
while IFS= read -r -d ' ' file; do
echo "file: ${file}"
image_name=$(basename $file)
echo "copy image: ${image_name}"
cp $file ${icons_dest_path}/${image_name}
done


else
# If the platform is another beta distribution platform
   echo "🍃🍃🍃 upload platform for pagyer/fir, watermark 🍃🍃."
   
   convertPath=`which convert`
   echo ${convertPath}
   if [[ ! -f ${convertPath} || -z ${convertPath}]].then
      echo "warning: Skipping Icon versioning, you need to install ImageMagick and ghostscript (fonts) first, you can use brew to simplify process: brew install imagemagick brew install ghostscript"
      exit- 1;fi

    # description
    # version app- Version number
    # build_num app- Build version number.
    version=$(ruby ./ToolChain/ruby/dy_build_version.rb ${Mode})
    build_num=${BUILD_NUMBER}

    Check your current Git branch
    cut="$Branch"
	 echo ${cut#*/}
	#shell intercepts string
	branch=${cut#*/}

	shopt -s extglob
	build_num="${build_num##*( )}"
	shopt -u extglob

	# Pictures show text content
	if [ "${isBeta}" = "YES" ];then
  	   echo "🍜🍜🍜 is in Beta"
 	   caption="${version}($build_num)\n${branch}(Beta)"
	else
 	  caption="${version}($build_num)\n${branch}"
	fi

	echo $caption

function abspath() { pushd . > /dev/null; if [ -d "The $1" ]; then cd "The $1"; dirs -l +0; else cd "`dirname \"The $1\ "`"; cur_dir=`dirs -l +0`; if [ "$cur_dir"= ="/" ]; then echo "$cur_dir`basename \"The $1\ "`"; else echo "$cur_dir/`basename \"The $1\ "`"; fi; fi; popd > /dev/null; }


function processIcon() {
    base_file=The $1
    temp_path=$2
    dest_path=$3
    
    if [[ ! -e $base_file]].then
    echo "error: file does not exist: ${base_file}"
    exit- 1;fi
    
    if [[ -z $temp_path]].then
    echo "error: temp_path does not exist: ${temp_path}"
    exit- 1;fi
    
    if [[ -z $dest_path]].then
    echo "error: dest_path does not exist: ${dest_path}"
    exit- 1;fi
    
    file_name=$(basename "$base_file")
    final_file_path="${dest_path}/${file_name}"
    
    base_tmp_normalizedFileName="${file_name%.*}-normalized.${file_name##*.}"
    base_tmp_normalizedFilePath="${temp_path}/${base_tmp_normalizedFileName}"
    
# Normalize
    echo "Reverting optimized PNG to normal"
    echo "xcrun -sdk iphoneos pngcrush -revert-iphone-optimizations -q '${base_file}' '${base_tmp_normalizedFilePath}'"
    xcrun -sdk iphoneos pngcrush -revert-iphone-optimizations -q "${base_file}" "${base_tmp_normalizedFilePath}"
    
    width=`identify -format %w "${base_tmp_normalizedFilePath}"`
    height=`identify -format %h "${base_tmp_normalizedFilePath}"`
    
    band_height=$((($height * 50) / 100))
    band_position=$(($height - $band_height))
    text_position=$(($band_position - 8))
    point_size=$(((15 * $width) / 100))
    
    echo "Image dimensions ($width x $height) - band height $band_height @ $band_position - point size $point_size"
    
#
# blur band and text
#
    convert "${base_tmp_normalizedFilePath}" -blur 10x8 /tmp/blurred.png
    convert /tmp/blurred.png -gamma 0 -fill white -draw "rectangle 0,$band_position.$width.$height" /tmp/mask.png
    convert -size ${width}x${band_height} xc:none -fill 'rgba (0,0,0,0.2)' -draw "A rectangle 0, 0,$width.$band_height" /tmp/labels-base.png
    convert -background none -size ${width}x${band_height} -pointsize $point_size -fill white -gravity center -gravity South caption:"$caption" /tmp/labels.png
    
    convert "${base_tmp_normalizedFilePath}" /tmp/blurred.png /tmp/mask.png -composite /tmp/temp.png
    
    rm /tmp/blurred.png
    rm /tmp/mask.png
    
#
# compose final image
#
    filename=New"${base_file}"
    convert /tmp/temp.png /tmp/labels-base.png -geometry +0+$band_position -composite /tmp/labels.png -geometry +0+$text_position -geometry +${w}-${h} -composite -alpha remove "${final_file_path}"
    
# clean up
    rm /tmp/temp.png
    rm /tmp/labels-base.png
    rm /tmp/labels.png
    rm "${base_tmp_normalizedFilePath}"
    
    echo "Overlayed ${final_file_path}"
}



# Copy appIcon images to appicon-internal
icons_path="${Workspace}/${APP_NAME}/Resources/Assets.xcassets/AppIcon.appiconset"
icons_dest_path="${Workspace}/${APP_NAME}/Resources/Assets.xcassets/AppIcon-Internal.appiconset"

icons_set=`basename "${icons_path}"`

echo "icons_path: ${icons_path}"
echo "icons_dest_path: ${icons_dest_path}"

	mkdir -p "${tmp_path}"

	if [[ $icons_dest_path= ="\ \"]].then
		echo "error: destination file path can't be the root directory"
		exit- 1;fi

	rm -rf "${icons_dest_path}"
	cp -rf "${icons_path}" "${icons_dest_path}"

	# Reference: https://askubuntu.com/a/343753
	find "${icons_path}" -type f -name "*.png" -print0 |
	while IFS= read -r -d ' ' file; do
		echo "$file"
		processIcon "${file}" "${tmp_path}" "${icons_dest_path}"
	done

fi
Copy the code

Ruby modifs project parameters

Here you can use Ruby to implement parameter modification (of course, you can also use python and other languages, if you are convenient).

According to their own scene to distinguish, some parameters can not do not do. This paper mainly records the methods of modifying parameters and adding parameter markers used by the author

Current operations:

  • Distinguish between beta and non-beta – modify the definitionbetaThe true and false values of macros
  • Different BundleIDS for different distribution platforms — Modify the bundleID

#! /bin/bash -l

export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8

echo ${isBeta}
echo ${channel}

if [ "${isBeta}" = "YES" ];then
  echo "🍜🍜🍜 is in Beta"
  ruby ./ToolChain/ruby/dy_build_global.rb -isbeta-BETA -channel-${channel}
else
  echo "🍜🍜🍜 is not a Beta"
  ruby ./ToolChain/ruby/dy_build_global.rb -channel-${channel}
fi


if [ "$BetaPlatform" = "pgyer" ];then
  echo "Pgyer 🌹 modify bundleID com.xx.yy. Test, profile" 
  ruby ./ToolChain/ruby/dy_edit_profile.rb
fi
  
if [ "$BetaPlatform" = "appstore" ];then
  echo "Appstore 🚀 keep bundleID,profile"
fi
  
if [ "$BetaPlatform" = "fir" ];then
   echo "Fir 🚀 keep bundleID,profile"
fi


Copy the code

Script, depended on Xcodeproj CocoaPods open source for the project name. The Xcodeproj/project pbxproj files for configuration changes.

For Python, you can use the project mod-pbxproj

Pod operation

Install/update third-party libraries, using Cocoapods in this case, but other package managers can use other methods.

echo "🌲 ------------- Pod operation --------------------"

pod update --verbose --no-repo-update

echo "🌲 ------------- Pod complete --------------------"
Copy the code

Execute a build

The preparatory work

Before we get started, we need to do some preparatory work, like setting the variables, constants that we’re going to use.

It needs to be written in advance to avoid falling apart.

echo "🌰 ------------- Obtain materials --------------"

# User name for the local Mac
sys_username="$USER"
The name of the task built by Jenkins
jenkinsName=${JOB_NAME}
# project name
APP_NAME=""
# scheme of
SCHEME_NAME=""

# Project absolute path
project_path="${WORKSPACE}"
# time
DATE="$(date +%Y-%m-%d-%H-%M-)"
# info. Plist path
project_infoplist_path=". /${APP_NAME}/Info.plist"

#buglys Command line tool path
buglyPath=/Users/${sys_username}/Desktop/buglySymboliOS


Copy the code

The Build number related

The old way was to get it directly from info.plist:


#version
bundleVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${project_infoplist_path}")

#bundleID
BundleID=$(/usr/libexec/PlistBuddy -c "print CFBundleIdentifier" "${project_infoplist_path}")

Copy the code

However, in the new Xcode, the way to get the version number and bundleID has changed, Now the value in info.plist is the variable name, with version number $(MARKETING_VERSION) and bundleID $(PRODUCT_BUNDLE_IDENTIFIER).

The ending idea is to get it through the script to the project configuration. The following uses Ruby to achieve both purposes.

We set the build number of App and Jenkins to be the same, so that we can check the parameters and symbol table of the build when needed:


# Obtain version number X.X.X through script
bundleShortVersion=$(ruby ./ToolChain/ruby/dy_build_version.rb "${Mode}")

# Get bundleID from script
BundleID=$(ruby ./ToolChain/ruby/dy_build_bundIeID.rb "${Mode}")

Change the build number of the IPA to be the same as the Jenkins build number
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "${project_infoplist_path}"

# build value
bundleVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${project_infoplist_path}")

# bundleVersion is normally the same as BUILD_NUMBER
echo "BundleID:${BundleID} Verision:${bundleVersion} Jenkins Build: $BUILD_NUMBER "

Copy the code

usesecurityUnlock the keychain.

The reason for adding the security unlock operation is that the keychain is not unlocked after the child node is logged in to over SSH. The package fails.

The solution is to run the security unlock-keychain command to unlock the certificate.


The default password for login keychain is the user's login password
security -v unlock-keychain -p "password"

Copy the code

In addition, you can run commands to view the details of the description file, including the UUID

/usr/bin/security CMS -d -i File pathCopy the code

Xcodebuild

The main way to build a project is to use Xcodebuild.

Divided into three stages:

  • Clean
  • Archive
  • Export

If you do not like the log output during the command execution, you can add it at the end of the command line

-quiet    # Only WARN and error will be printed
Copy the code

Clean up the project

At each build, clean the project to ensure that there are no other factors affecting it.

Use xcodeBuild Clean [-optionName]… To clear the compile process generation file, use the following:

#// The following is the use of Cocopods integrated
echo "🏎 ī¸ 🏎 ī¸ = = = = = = = = = = = = = = = = = the clean = = = = = = = = = = = = = = = = = 🏎 ī¸ 🏎 ī¸"
 
xcodebuild clean -workspace "${APP_NAME}.xcworkspace" -scheme "${APP_NAME}"  -configuration ${development_mode} -UseModernBuildSystem=YES

Copy the code

-project ${APP_NAME}.xcodeProj ${APP_NAME}.xcodeproj ${APP_NAME}.xcodeproj ${APP_NAME}.xcodeproj

The new version of Xcode has a new build system that uses -usemodernBuildSystem =

to separate the old from the new.

The command instructions
-workspace NAME Specify the workspace file xxx.xcworkspace
-scheme NAME Specify the name of the build project
-configuration [Debug/Release] Select Debug or Release build
-sdk NAME Specifies the SDK to be used at compile time

Build the archive package

Xcodebuild archive

echo "🚗🚗🚗 *** compiling project For${development_mode}🚗 🚗 🚗"

xcworkspace=${project_path}/${APP_NAME}.xcworkspace
echo "acrhivie xcworkspace : ${xcworkspace}"

xcodebuild \
archive -workspace  ${xcworkspace} \
-scheme ${SCHEME_NAME} \
-configuration ${development_mode} \
-archivePath ${build_path}/${APP_NAME}.xcarchive \
-quiet 


echo '✅ *** compile complete ***'
Copy the code

The export of IPA package


security -v unlock-keychain -p "yourpassword"

echo '🚄 * * * * * * * * * * * * * * * * * are packaged * * * * * * * * * * * * * * * * * 🚄'


xcodebuild -exportArchive -archivePath ${build_path}/${APP_NAME}.xcarchive \
-exportPath ${exportFilePath} \
-exportOptionsPlist ${exportOptionsPlist_path} \
-allowProvisioningUpdates \
-quiet

Copy the code

After the update to Xcode9.0, the auto-packaging script written earlier is not available.

You need to add -allowprovisioningUpdates to get the key to access the keystring. Setting this field will ask for the keystring content in the packaging process pop-up.

ExportOptionsPlist set

In particular, exportOptionsPlist must be checked for different environments and distribution platforms.

The easiest way is to manually archive,export and use the exportOptionsPlist file in the product after setting the required environment.

Check the ipa

Check whether the **. Ipa file exists in the corresponding path:


if [ -e ${exportFilePath}/${APP_NAME}.ipa ]; then
echo "✅ ***. Ipa file exported ***"
echo $exportFilePath

else
echo "❌ *** Failed to create. Ipa file ***"
exit 1
fi

echo 'đŸ“Ļ *** Packaging completed ***'

Copy the code

Build a complete

Upload distribution Platform

Here is divided into dandelion, FIR, Appstore three platforms, upload IPA.

If it is appstore, there is an extra Git tag related operation, marking the submission of the current version, so as to directly roll back the code for viewing when necessary.

The following three upload commands are best tested on the machine in advance and can be used normally before building.


if [ "$BetaPlatform" = "pgyer" ];then
     
      echo 🚀 upload dandelion ++++++++++++++upload+++++++++++++
      #User Key
      uKey="User Key"
      #API Key
      apiKey="API Key"
      Execute the upload to dandelion command
      curl -F "file=@${IPA_PATH}" -F "uKey=${uKey}" -F "_api_key=${apiKey}" -F "buildPassword=yourpassword" -F "buildInstallType=2" http://www.pgyer.com/apiv2/app/upload
      echo "✅ Finsh - Dandelion upload completed"
fi
    
    
if [ "$BetaPlatform" = "fir" ];then
      echo "🚀 upload Fir ++++++++++++++upload+++++++++++++"
      
      fir p ${IPA_PATH} -T your_token
      
      echo "✅ finsh-fir upload completed"
fi
 
 
 
if [ "$BetaPlatform" = "appstore" ];then

   echo "🏠 -- -- -- -- -- -- -- -- -- -- -- -- appstore xcrun uploaded to the appstore -- -- -- -- -- -- -- -- -- --"
 
   xcrun altool --upload-app -f ${IPA_PATH} -u your_account -p your_app_password  --verbose
   
   echo "Add Git Tag ---------- to 📝 ------------ Appstore"

	echo "--------- Current Tag -----------"
	git tag

	echo "-- -- -- -- -- -- -- -- -- play Tag -- -- -- -- -- -- -- -- -- -- -- --"
	GitTag=V${bundleShortVersion}_${bundleVersion}

	git tag -a ${GitTag} -m "Tag:${GitTag} "
	echo "Tag ${GitTag}"

   # Push tag
	git push origin ${GitTag}

	echo "✅ ----------- Git Tag push completed ----------"
fi

Copy the code

Symbol table processing

Upload bugly

echo "đŸ“Ļ ------ Start symbol table related work ------"

echo "Šī¸ ----- Upload symbol table ------- Šī¸"

if [ "$BetaPlatform" = "appstore" ];then
   echo "🚀 Bugly Official symbol Table"
   buglyID= your_product_buglyID
   buglyKey= your_product_buglyKey
 
else
   echo "🚀 Bugly test version symbol Table"
   buglyID= your_dev_buglyID
   buglyKey=your_dev_buglyKey
fi

dSYMPath=$exportFilePath/${APP_NAME}.xcarchive/dSYMs/
cd $buglyPath 

echo "----- starts uploading symbol table ----------"
java -jar buglySymboliOS.jar \
-i ${dSYMPath}/${APP_NAME}.app.dSYM \
-u -id ${buglyID} \
-key  ${buglyKey} \
-package ${BundleID} \
-version ${bundleShortVersion}

echo "✅ ---------- Symbol table upload completed ------ ✅"

Copy the code

Archive product

After all operations are performed, the product is saved for use as needed.

The compression

First compress the file



echo "đŸ“Ļ ---------- compressed file ------ đŸ“Ļ"

# Open directory
cd $exportFilePath
zip -r ./${JOB_NAME}_${BUILD_NUMBER}.zip ./* 

**.xcarchive
rm -rf ${APP_NAME}.xcarchive
Copy the code

Uploading an FTP Server

You can use the FTP plug-in to upload the ZIP file to the archive directory

Product to clean up

Delete IPA and other files. Note that the files will be cleared only when the status is SUCCESS, so as to avoid sometimes outgoing problems. You can manually upload them.

Build description

Setting the Build description

To inform

After completion, enterprise wechat Webhook robot push, the effect is as follows:

This is optional to avoid frequent interruptions to other colleagues. The script is as follows:

if [ "${BotPush}" = "YES" ];then
 
version=$(ruby ./ToolChain/ruby/dy_build_version.rb ${Mode})
downUrl="pgyer url"

if [ "$BetaPlatform" = "fir" ];then
  downUrl="fir url"
fi
 
# Robot address in the group
ROBOT=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=yourkey

curl ' '${ROBOT}' ' \
   -H 'Content-Type: application/json' \
   -d '{" msgType ": "markdown", "markdown": {"content": "### iOS build \n version '${version}'</font> <font color=\"info\">#'${BUILD_NUMBER}'\n '${Mode}'\n '${BetaPlatform}'\n>[download address]('${downUrl}') "}} '

fi


Copy the code

Refer to the article

  • Xcode10 new features
  • Xcodebuild command is used
  • Xcodebuild command official description
  • Use the Xcodebuild command to automate the packaging
  • Xcode packages those things automatically