发布普通Cocoa Mac程序的脚本

Cocoa Mac应用程序的部署一般包括几步:向备份区提交代码、更新版本号和以光盘镜像方式打包程序。在这篇文章中,我会介绍一个结合了bash/perl/Applescript的脚本的方式来处理这些任务。

部署步骤

标准的Xcode生成模板只会生成代码但不会提供超过其之外的其他的帮助。部署程序一般需要几个额外的步骤。

虽然你可以简单地在发布文件夹中将程序生成ZIP档案,但是当你真正部署程序时需要几个其他的步骤。

1 更新版本号

显然这一步并不困难,但是往往容易忽视的是用户面对的版本号也必须同时更新。

2 向版本控制系统提交代码

这一步也很重要:它通过保证你的代码的安全来节省你的时间。

我曾经见过一些程序员的工作方式,所有的代码都保存在一个文件夹中,版本变更时整个文件夹都要复制。严肃地说,这不应该是管理事物的方式。你可以备份整个硬盘,保存每一个复本,但是这一些仅仅是备份罢了。

在版本1.2我们如何实现这个功能(旧的代码在哪儿?)为什么这段代码改变了而且之前是如何做的?我们有版本3.5的备份么?

不仅你要有代码备份区,而且要对不同的生成版本做好标记才能最好的解决上面的问题。我不想讨论多么频繁的提交更新是好的或者如何标记bug修复和功能变更是正确的,但是至少应该标记每一个发布版本。好的部署脚本应该可以制定相关规则。

新的分布式的备份版本控制程序比如git使得创建备份简单的多。不需要一个集中的保存路径,你可以随意将任何文件夹作为本地备份(例如:运行”git init”),只需在随后关心是否或者在哪儿保存一个官方的或者共享的备份。

现在对Xcode可怕的版本控制系统感到沮丧了么?记不住所有的git命令?不喜欢git命令行的界面?我当然也不喜欢。幸运的是,Mac 程序比如GitX是相当简单、友好而且正常实现该功能。

3 确保你有一个清晰的生成

多少次,你会遇到和生成有关的问题,这些问题只能通过清除和重生成来解决。其中最突出的例子之一就是你故意在生成之后在移除,除非你之前先清除一下,否则它们不会真正移除。不先清楚一次的话,你的生成结果可能跟你预想的不同。

4 在DMG文件中打包程序

在美学观点来看,那些不需要安装的Mac程序是作为DMG光盘镜像来部署的。创建它们是需要一些技巧的。幸运的是,使用一点点Applescripting,我们可以自动完成这一步骤。

一个对象链接与嵌入的Bash脚本

下面是一个bash脚本来处理上面提到的所有步骤。使用其他语言对于一名C/Obj-C/C++的编程者来说有点烦但是一些东西必须以特定的方式完成。

这一种脚本是处理这一类部署的传统方式。然而,这并不是我自己处理调度的方式。下一周,我会介绍我用来部署的代码。

这个脚本中的假设

这里用到了一些假设。虽然它们都是默认的Cocoa Mac程序模板中的正常有效的假设,但是还会有一些情况它们没有涉及到,因而你需要稍微改动一下这个脚本。

  1. 这个脚本需要1个参数:你准备生成的.xcodeproj文件
  2. 你准备生成的目标必须与这个工程有相同的名字(除去.xcodeproj后缀)
  3. 你的程序的Info.plist必须与工程有相同的名字(除去.xcodeproj后缀),具有info.plist的后缀
  4. 程序与工程有相同的名字(除去.xcodeproj后缀)
  5. 调度的生成是”Release”生成,而且生成的工程文件夹就是发布文件夹。
  6. 你使用git来创建备份(这个脚本仍然可以运行即使并没有安装git)
  7. 你的DMG文件夹的后台镜像是一个400*300的PNG文件,该文件命名为background.png,要保存在与.xcodeproj文件相同的文件夹下(虽然如果background.png丢失时,脚本会忽略后台镜像步骤)
  8. 部署DMG文件会在桌面存储一份,名字与工程文件的名字相同,后缀是.dmg(如果这个位置已经有一个文件了,生成会失败)

脚本

#!/bin/bash

if [ ! "${1}" ]; then
echo "usage: $0 xcode_project_path"
exit
fi

XCODE_PROJECT_PATH=$1
XCODE_DIRECTORY="`dirname "$1"`"
XCODE_PROJECT_NAME="`basename -s .xcodeproj "${XCODE_PROJECT_PATH}"`"

# xcodebuild needs to run from the project's directory so we'll move there
# and stay for the duration of this script
cd "${XCODE_DIRECTORY}"

# Check if git is installed
if [ `which git` ]; then
echo "git is installed on this computer"

# If git is installed, then require that the code be committed
if [ "`git status -s 2>&1 | egrep '^\?\?|^ M|^A |^ D|^fatal:'`" ] ; then
echo "Code is not committed into git. Commit into git before deployment."
exit
fi
echo "Repository up-to-date."
fi

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the perl to Objective-C bridge to get the version from the Info.plist
# You could easily use the python or ruby bridges to do the same thing
#CURRENT_VERSION="`echo 'use Foundation;
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$value = $plist->objectForKey_("CFBundleVersion");
#print $value->description()->UTF8String() . "\n";' | perl`"

# Use PlistBuddy instead of the perl to Cocoa bridge
CURRENT_VERSION="`/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' \
"${XCODE_PROJECT_NAME}-Info.plist"`"

# Report the current version
echo "Current version is ${CURRENT_VERSION}"

# Prompt for a new version
read -p "Please enter the new version:
" NEW_VERSION

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the bridge again to write the updated version back to the Info.plist
#echo 'use Foundation;
#$version = "'$NEW_VERSION'";
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$plist->setObject_forKey_($version, "CFBundleVersion");
#$plist->writeToFile_atomically_($file, "YES");' | perl

# Use PlistBuddy instead of the perl to Cocoa bridge
/usr/libexec/PlistBuddy -c "Set CFBundleVersion ${NEW_VERSION}" \
"${XCODE_PROJECT_NAME}-Info.plist"

# Commit the updated Info.plist
if [ `which git` ]; then
git commit -m "Updated Info.plist to version ${NEW_VERSION}" \
"${XCODE_PROJECT_NAME}-Info.plist"
fi

# Clean the Release build
xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" clean

# Build the Release build
if [ "`xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" build \
| egrep ' error:'`" ] ; then
echo "Build failed."
exit
fi

# Tag the repository now that we have a successful build
git tag "version-${NEW_VERSION}"

#########
# From this point onwards, the script is all about DMG packaging
#########

# Create a temporary directory to work in
TEMP_DIR="`mktemp -d "${TMPDIR}${XCODE_PROJECT_NAME}.XXXXX"`"

# Create the folder from which we'll make the disk image
DISK_IMAGE_SOURCE_PATH="${TEMP_DIR}/${XCODE_PROJECT_NAME}"
mkdir "${DISK_IMAGE_SOURCE_PATH}"

# Copy the application into the folder
cp -R "build/Release/${XCODE_PROJECT_NAME}.app" \
"${DISK_IMAGE_SOURCE_PATH}/${XCODE_PROJECT_NAME}.app"

# Make a symlink to the Applications folder
# (so we can prompt the user to install the application)
ln -s "/Applications" "${DISK_IMAGE_SOURCE_PATH}/Applications"

# If a "background.png" file is present in the Xcode project directory,
# we'll use that for the background of the folder.
# An assumption is made in this script that the background image is 400x300px
# If you are using a different sized image, you'll need to adjust the
# placement and sizing parameters in the Applescript below
if [ -e "background.png" ]; then
cp "background.png" \
"${DISK_IMAGE_SOURCE_PATH}/background.png"
fi

# Create the read-write version of the disk image from the folder
# Also note the path at which the disk is mounted so we can open the disk
# to adjust its attributes
DISK_IMAGE_READWRITE_PATH="${DISK_IMAGE_SOURCE_PATH}-rw.dmg"
VOLUME_MOUNT_PATH="`hdiutil create -srcfolder "${DISK_IMAGE_SOURCE_PATH}" \
-format UDRW -attach "${DISK_IMAGE_READWRITE_PATH}" | \
sed -n 's/.*\(\/Volumes\/.*\)/\1/p'`"

# Now we use Applescript to tell the Finder to open the disk image,
# set the view options to a bare, icon arranged view
# set the background image (if present)
# and set the icon placements
if [ -e "background.png" ]; then
echo '
tell application "Finder"
open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
set statusbar visible of front window to false
set toolbar visible of front window to false
set view_options to the icon view options of front window
set icon size of view_options to 96
set arrangement of view_options to not arranged
set the bounds of front window to {100, 100, 500, 400}
set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
set app_folder to item "Applications" of front window
set background_image to item "background.png" of front window
set background picture of view_options to item "background.png" of front window
set position of background_image to {200, 200}
set position of app_icon to {120, 100}
set position of app_folder to {280, 100}
set current view of front window to icon view
end tell' | osascript
else
echo '
tell application "Finder"
open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
set statusbar visible of front window to false
set toolbar visible of front window to false
set view_options to the icon view options of front window
set icon size of view_options to 96
set arrangement of view_options to not arranged
set the bounds of front window to {100, 100, 500, 400}
set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
set app_folder to item "Applications" of front window
set position of app_icon to {120, 100}
set position of app_folder to {280, 100}
set current view of front window to icon view
end tell' | osascript
fi

# Make the background.png file invisible
SetFile -a V "${VOLUME_MOUNT_PATH}/background.png"

# Eject the disk image so that we can convert it to a compressed format
hdiutil eject "${VOLUME_MOUNT_PATH}"

# Create the final, compressed disk image
hdiutil convert "${DISK_IMAGE_READWRITE_PATH}" -format UDBZ \
-o "${HOME}/Desktop/${XCODE_PROJECT_NAME}.dmg"

# Remove the temp directory
rm -Rf "${TEMP_DIR}"

结论

Ergh:这是一个即不是C也不是Objective-C的代码规模大的文章

下一周,我会介绍如何以相同的步骤来实现Cocoa程序上的登陆,报道和错误处理。

原文作者:Matt Gallagher

原文链接:http://cocoawithlove.com/2010/11/deployment-script-for-generic-cocoa-mac.html

原文:

A deployment script for a generic Cocoa Mac application

Deployment for a Cocoa Mac application normally involves a few common steps: committing code into a repository, updating version numbers and packaging the application as a DMG disk image. In this post, I’ll show you a combination bash/perl/Applescript to handle all these tasks in a single script.

Deployment steps

Standard Xcode build templates will build your code but don’t really offer any help beyond that point. Deploying your application normally requires a few extra steps beyond what the standard templates provide.

While you can simply make a ZIP archive out of the application in your Release directory and call it done, there’s normally a few more steps that you should take when deploying an application.

1. Update your version number

Obviously, this isn’t a hard step but it is easy to forget since the user-facing version number (build numbers not included) must be deliberately updated when you decide a build is ready.

2. Commit your code into a version control system

This step should be obvious: it protects your time investment by ensuring that your code doesn’t disappear.

And yet I still visit small or single-programmer offices where all the code is just kept in a directory and the whole directory is duplicated from version to version or to play with features. Seriously, that is not how you should manage things. You may have your disk backed up, you may keep copies of your builds but these things are only complimentary to maintaining your repository.

How did we implement this feature in version 1.2 (and where’s the old code)? Why was this code changed and what did it do before? Do we have a copy of version 3.5 of the application anywhere?

These are the questions that are best answered by not only having a code repository but having it properly tagged for every build. I won’t get into philosophies about how frequently you should commit between releases or how you should tag bug fixes and feature changes but the absolute minimum for any system should be that you tag your releases. Your deployment script should make this mandatory.

Incidentally, newer distributed repository version control programs like git make creating a repository for your code so much simpler. Instead of needing to set up a centralized location, you can casually make any folder its own local repository (i.e. just run “git init” in the directory) and worry about whether and where to locate an official or shared repository later.

Frustrated at Xcode’s horrible version control system support? Can’t remember all the git commands? Don’t like git’s command-line interface? I certainly don’t. Fortunately, Mac apps like GitX are simple, pretty and work well.

3. Make certain you have a clean build

From time-to-time, you will encounter problems with builds that are only fixed by cleaning and rebuilding. One of the biggest examples of this are assets that you deliberately remove from the build — these never get removed from the build directory unless you clean first, then build. Without a step to clean the directory first, your build may not be exactly what you think it should be.

4. Package the application in a DMG file

Mostly for reasons of aesthetic presentation, Mac applications that don’t require an installer are normally deployed as DMG disk images. These can be a little fiddly to create, adjust aesthetically and then create a compressed version for distribution.

Fortunately, with a little Applescripting, we can automate this process too.

A big ole Bash script

Here then is a bash script to handle all of the above steps. It’s an annoying diversion into another language for a C/Obj-C/C++ programmer but some things (especially setting folder view options) need to be done a specific way.

A script of this sort is the traditional way that this type of deployment is handled. However, it is actually not how I handle my deployments (but I’m a little weird in this respect). Next week, I’ll show you the code I use for deployment.

Assumptions in this script

There’s a few assumptions here. While they are normally valid assumptions if you create your project using default Cocoa Mac Application template, there are certainly cases where they won’t apply and you’ll need to tweak the script a little.

  1. This script requires 1 parameter: the .xcodeproj file you want to build.
  2. The target you want to build must have the same name as the project (minus the .xcodeproj extension).
  3. The Info.plist for the application must have the same name as the project (minus the .xcodeproj extension) with the suffix “-Info.plist”.
  4. The application build has the same name as the project (minus the .xcodeproj extension).
  5. The deployment build is the “Release” build and the build project directory is the build/Release directory.
  6. You use git for your repository (although this script will continue if git is not installed).
  7. The background image for your DMG folder is a 400x300px PNG named background.png in the same folder as the .xcodeproj file (although this script will skip background image steps if the background.png is missing).
  8. The deployment DMG file will be saved to the Desktop with the same name as the project (minus the .xcodeproj extension) with the suffix “.dmg” (build will fail if there’s already something at this location).
The script
#!/bin/bash

if [ ! "${1}" ]; then
    echo "usage: $0 xcode_project_path"
    exit
fi

XCODE_PROJECT_PATH=$1
XCODE_DIRECTORY="`dirname "$1"`"
XCODE_PROJECT_NAME="`basename -s .xcodeproj "${XCODE_PROJECT_PATH}"`"

# xcodebuild needs to run from the project's directory so we'll move there
# and stay for the duration of this script
cd "${XCODE_DIRECTORY}"

# Check if git is installed
if [ `which git` ]; then
    echo "git is installed on this computer"

    # If git is installed, then require that the code be committed
    if [ "`git status -s 2>&1 | egrep '^\?\?|^ M|^A |^ D|^fatal:'`" ] ; then
        echo "Code is not committed into git. Commit into git before deployment."
        exit
    fi
    echo "Repository up-to-date."
fi

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the perl to Objective-C bridge to get the version from the Info.plist
# You could easily use the python or ruby bridges to do the same thing
#CURRENT_VERSION="`echo 'use Foundation;
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$value = $plist->objectForKey_("CFBundleVersion");
#print $value->description()->UTF8String() . "\n";' | perl`"

# Use PlistBuddy instead of the perl to Cocoa bridge
CURRENT_VERSION="`/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' \
    "${XCODE_PROJECT_NAME}-Info.plist"`"

# Report the current version
echo "Current version is ${CURRENT_VERSION}"

# Prompt for a new version
read -p "Please enter the new version:
" NEW_VERSION

# !! Update: Changed from using the Perl Cocoa bridge to using PlistBuddy !!
# Use the bridge again to write the updated version back to the Info.plist
#echo 'use Foundation;
#$version = "'$NEW_VERSION'";
#$file = "'"${XCODE_PROJECT_NAME}"'-Info.plist";
#$plist = NSDictionary->dictionaryWithContentsOfFile_($file);
#$plist->setObject_forKey_($version, "CFBundleVersion");
#$plist->writeToFile_atomically_($file, "YES");' | perl

# Use PlistBuddy instead of the perl to Cocoa bridge
/usr/libexec/PlistBuddy -c "Set CFBundleVersion ${NEW_VERSION}" \
    "${XCODE_PROJECT_NAME}-Info.plist"

# Commit the updated Info.plist
if [ `which git` ]; then
    git commit -m "Updated Info.plist to version ${NEW_VERSION}" \
        "${XCODE_PROJECT_NAME}-Info.plist"
fi

# Clean the Release build
xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" clean

# Build the Release build
if [ "`xcodebuild -configuration Release -target "${XCODE_PROJECT_NAME}" build \
     | egrep ' error:'`" ] ; then
    echo "Build failed."
    exit
fi

# Tag the repository now that we have a successful build
git tag "version-${NEW_VERSION}"

#########
# From this point onwards, the script is all about DMG packaging
#########

# Create a temporary directory to work in
TEMP_DIR="`mktemp -d "${TMPDIR}${XCODE_PROJECT_NAME}.XXXXX"`"

# Create the folder from which we'll make the disk image
DISK_IMAGE_SOURCE_PATH="${TEMP_DIR}/${XCODE_PROJECT_NAME}"
mkdir "${DISK_IMAGE_SOURCE_PATH}"

# Copy the application into the folder
cp -R "build/Release/${XCODE_PROJECT_NAME}.app" \
    "${DISK_IMAGE_SOURCE_PATH}/${XCODE_PROJECT_NAME}.app"

# Make a symlink to the Applications folder
# (so we can prompt the user to install the application)
ln -s "/Applications" "${DISK_IMAGE_SOURCE_PATH}/Applications"

# If a "background.png" file is present in the Xcode project directory,
# we'll use that for the background of the folder.
# An assumption is made in this script that the background image is 400x300px
# If you are using a different sized image, you'll need to adjust the
# placement and sizing parameters in the Applescript below
if [ -e "background.png" ]; then
    cp "background.png" \
        "${DISK_IMAGE_SOURCE_PATH}/background.png"
fi

# Create the read-write version of the disk image from the folder
# Also note the path at which the disk is mounted so we can open the disk
# to adjust its attributes
DISK_IMAGE_READWRITE_PATH="${DISK_IMAGE_SOURCE_PATH}-rw.dmg"
VOLUME_MOUNT_PATH="`hdiutil create -srcfolder "${DISK_IMAGE_SOURCE_PATH}" \
    -format UDRW -attach "${DISK_IMAGE_READWRITE_PATH}" | \
    sed -n 's/.*\(\/Volumes\/.*\)/\1/p'`"

# Now we use Applescript to tell the Finder to open the disk image,
# set the view options to a bare, icon arranged view
# set the background image (if present)
# and set the icon placements
if [ -e "background.png" ]; then
    echo '
    tell application "Finder"
        open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
        set statusbar visible of front window to false
        set toolbar visible of front window to false
        set view_options to the icon view options of front window
        set icon size of view_options to 96
        set arrangement of view_options to not arranged
        set the bounds of front window to {100, 100, 500, 400}
        set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
        set app_folder to item "Applications" of front window
        set background_image to item "background.png" of front window
        set background picture of view_options to item "background.png" of front window
        set position of background_image to {200, 200}
        set position of app_icon to {120, 100}
        set position of app_folder to {280, 100}
        set current view of front window to icon view
    end tell' | osascript
else
    echo '
    tell application "Finder"
        open ("'"${VOLUME_MOUNT_PATH}"'" as POSIX file)
        set statusbar visible of front window to false
        set toolbar visible of front window to false
        set view_options to the icon view options of front window
        set icon size of view_options to 96
        set arrangement of view_options to not arranged
        set the bounds of front window to {100, 100, 500, 400}
        set app_icon to item "'"${XCODE_PROJECT_NAME}"'" of front window
        set app_folder to item "Applications" of front window
        set position of app_icon to {120, 100}
        set position of app_folder to {280, 100}
        set current view of front window to icon view
    end tell' | osascript
fi

# Make the background.png file invisible
SetFile -a V "${VOLUME_MOUNT_PATH}/background.png"

# Eject the disk image so that we can convert it to a compressed format
hdiutil eject "${VOLUME_MOUNT_PATH}"

# Create the final, compressed disk image
hdiutil convert "${DISK_IMAGE_READWRITE_PATH}" -format UDBZ \
    -o "${HOME}/Desktop/${XCODE_PROJECT_NAME}.dmg"

# Remove the temp directory
rm -Rf "${TEMP_DIR}"

Conclusion

Ergh: a code-heavy post with neither C nor Objective-C.

Next week, I’ll show you how to perform the same steps in a logging, reporting, error-handling Cocoa application.