发布Unity项目到macOS AppStore和Steam

中文 | English


这篇blog记录下如何将Unity导出的macOS应用提交到Steam和AppStore。通常情况下我们在macOS系统上的开发依赖于Xcode,如果在Unity的PlayerSettings里面没有设置签名和BundleId,那么只能使用相应的命令行工具对二进制的Game.app进行签名和处理。

首先会介绍一些签名相关的基本概念和命令,或者你可以直接跳到:
提交到Steam和第三方直接发布
提交到macOS-App-Store

包体配置和签名

基本概念

Apple提供了签名工具用于给应用文件进行签名,通常macOS系统的应用都是一个.app的Bundle,不像Windows系统中是一个.exe的文件。所以macOS的codesign 是对于整个bundle的所有文件的。

Designated Requirement(DR)

Apple会给每一个要签名的应用创建一个Designated Requirement(DR),这个DR通常由应用的BundleID进行标识。

Resource envelope

签名会对Game.app下的所有代码和非代码的资源文件以及第三方的插件进行签名,计算出每个文件的hash,同时再合并所有资源文件的hash生成最终的一个Hash值。

Resource envelope 有两个版本,最新的 v2版本会对所有文件进行签名,OSX Mavericks 10.9之后都是使用 v2的版本

只有有任何记录的文件有改动,都会通过hash校验来被检测出来。所以即使没有修改Game.app下的文件,在签名完成之后添加新的文件,整个Budnle也是校验不过的。无法通过GateKeeper的验证。

应用Bundle处理

修改BundleId

直接打开Game.app/Contents/Info.plist 修改CFBundleIdentifier

移除不需要的文件

使用Unity编辑器时,如果导入macOS或者iOS的frameworbundle文件,Unity会自动对文件夹下的所有文件和文件夹创建一个对应的.meta文件。
这个文件存在的路径在Bundle中不是标准的路径,同时会导致codesign时失败,我们需要移除它。

1
2
#remove all remove meta files
find "./Game.app" -name "*.meta" -type f -exec rm {} \;

CodeSign签名

第三方库签名

Apple要求所有提交给macOS AppStore和经Notarization认证后直接分发的应用中,所有的第三方库dylib,framework,bundle都需要被签名

同时需要注意:提交到acOS AppStore的应用中,所有的第三方库和插件都需要使用Apple开发者的签名。

也就是说你如果提交给macOS Store的话,还是需要给类似libssl.dylib这类的库重新签名。

Entitlements文件

在使用CodeSign签名之前我们需要创建.entitlements文件。如果使用xcode开发应用,xcode会帮我们创建好。但我们只有一个.app的Bundle文件,那么只能手动创建然后,使用命令行进行签名。

macOS AppStore需要的.entitlements文件和第三方发布提交给Steam的.entitlements文件是不同的.

下面是一个最简的配置,如果应用有其他的capabilities配置,需要再额外配置其他的key/value。

For macOS AppStore

1
2
3
4
5
6
7
8
9
10
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist

For Steam

1
2
3
4
5
6
7
8
9
<?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>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>

CodeSign 命令

codesign命令

1
codesign --deep --force --verify --verbose=4 --timestamp --options runtime --entitlements  $EntitlementsFile --sign "$CertName" $FileName

参数sign填写的证书的名称 类似于Mac Developer:XXXXX(TEAMID)之类的。这个参数会有自动匹配只要不出现冲突就可以。

如何验证codesign是否正确

1
codesign --verify --deep --strict --verbose=4 Game.app

验证通过则会输出

1
2
Game.app: valid on disk
Game.app: satisfies its Designated Requirement

提交到Steam和第三方直接发布

提交到Steam直接分发给用户的macOS程序需要通过Apple的认证流程,详细的说明 Notarizing macOS Software Before Distribution

证书创建

首先是证书创建,需要在Apple Developer后台的证书中创建一个 Developer ID Application 证书。

Developer ID Application Certificate 只能由Apple Developer的account holder创建,开发者账号下即使是有admin权限的子账号也是无法创建的。

和创建普通的开发者证书一样,提交CSR文件,创建完成后下载双击导入到keychain中。

包体配置和准备

现在我们有一个Game.app的二进制包,以及一个Developer ID Application Certificate

按照第一部分的应用Bundle处理中的说明

进行下面的操作

  1. 删除Game.app下面的所有.meta文件
  2. Game.app同级目录创建Game.entitlements
  3. 修改Game.app/Contents/Info.plist中的信息

修复SteamLib

通常Unity使用的Steamworks Lib是第三方开发的rlabrecque/Steamworks.NET。在codesign之前我们要对他进行处理。

rlabrecque/Steamworks.NET的库文件为Steam_api.bundle

  1. Steam_api.bundle的 CFBundleIdentifier为com.rileylabrecque.steam_api,不是标准的BundleId,我们需要将它改成com.rileylabrecque.steamapi去掉下划线。 修改的文件地址在steam_api.bundle/Contents/Info.plist中。
  2. libsteam_api.dylib包含32位的symbol。提交到Apple的bundle必须支持64位的文件,32位的symbol需要删除。
    lipo -remove x86_64 libsteam_api.dylib -o libsteam_api.dylib

这样我们对SteamworkLibrary的处理就完成了。如果有其他的第三方库有类似的问题,也需要进行相同的操作。

Codesign签名

Apple文档上建议首先要先对所有的lib,framework以及bundle进行签名,最后再对Game.app进行签名。使用--deep参数进行签名不保证能够正确处理嵌套的lib的正确签名。只能用于文件修改后的codesign patch。

  1. 对所有的library签名

$BINARY = ‘Game.app’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
libfile=`find ./$BINARY -name '*.dylib'`
while read -r libname; do
echo "Sign:" $libname
codesign --deep --force --verify -vvvv --timestamp --options runtime --entitlements $ENTITLEMENTS --sign "$SIGNCERT" $libname
done <<< "$libfile"

bundlefile=`find ./$BINARY -name '*.bundle'`
while read -r bundlename; do
echo "Sign:" $bundlename
codesign --deep --force --verify -vvvv --timestamp --options runtime --entitlements $ENTITLEMENTS --sign "$SIGNCERT" $bundlename
done <<< "$bundlefile"

frameworkfile=`find ./$BINARY -name '*.framework'`
while read -r frameworkname; do
echo "Sign:" $frameworkname
codesign --deep --force --verify -vvvv --timestamp --options runtime --entitlements $ENTITLEMENTS --sign "$SIGNCERT" $frameworkname
done <<< "$frameworkfile"
  1. 在所有的lib签名成功后对整体的包进行签名
1
2
3
codesign --deep --force --verify --verbose --timestamp --options runtime --entitlements  $ENTITLEMENTS --sign "$SIGNCERT" $BINARY -i $BUNDLE_ID
# verification
codesign --verify --deep -vvvv --strict Game.app

需要注意的是--sign参数输入”Developer ID Application:”就可以了,codesign会从keychain中自动匹配

提交苹果Notarization验证

1.提交到Notarization需要将Game.app打包为一个zip包。

需要注意的是我们要使用apple提供的ditto命令进行打包,直接使用zip命令会丢失签名信息其他额外信息

1
ditto -c -k --sequesterRsrc --keepParent "Game.app" "Game.app.zip"

2.提交认证请求

1
2
3
4
xcrun altool --notarize-app --primary-bundle-id "${Bundle_Identifier}" --username "${APPLE_ID_ACCOUNT}" --password "${APPLE_ID_PWD}" --file Game.app.zip --asc-provider "${ASC_PROVIDER}"

#sample
xcrun altool --notarize-app --primary-bundle-id "com.xxxx.game" --username "yourid@gmail.com" --password "password" --file Game.app.zip --asc-provider "ABCDEFGH"

asc-providerProviderShortname, 通常是开发者证书末尾的那个Team ID,在Apple Developer/Membership/Team ID中可以找到

比较老的apple developer账号ProviderShortnameTeam ID不一致,我们可以使用命令来查询ProviderShortName

1
$ xcrun altool --list-providers -u "AC_USERNAME" -p "PWD"

提交成功之后会显示一个RequestId 如1809fca2-XXXX-XXXX-XXXX-14aa62f6XXXX

1
2
altool[16765:378423] No errors uploading 'Game.app.zip'.
RequestUUID = 1809fca2-XXXX-XXXX-XXXX-14aa62f6XXXX

根据RequestID 我们可以查询notarization的结果,通常需要等待5分钟左右。

1
2
#查询结果
$ xcrun altool --notarization-info "1809fca2-XXXX-XXXX-XXXX-14aa62f6XXXX" --username "${APPLE_ID_ACCOUNT}" --password "${APPLE_ID_PWD}"

提交成功后AppleID邮箱会收到一个来自Apple的邮件。
如果失败可以根据Request Identifier查询相应的log。

  1. 在提交成功之后我们就可以给应用盖章了。
1
2
$ xcrun stapler staple "Game.app"
$ zip -r -q Game.dist.app Game.app

这里注意的是不能直接给Game.app.zip staple。
对于Staple完成的.appBundle,再zip之后就可以直接分发了。

提交到macOS App Store

证书创建

提交到macOS AppStore需要两个证书

  1. Mac App Distribution Certificate
  2. Mac Installer Distribution Certificate

Mac App Distribution Certificate用于给bundle CodeSign

Mac Installer Distribution Certificate用于pack bundle 之后提交到itunes server审核

包体配置和准备

提交到Steam和第三方直接发布中的流程一致

  1. 删除Game.app下面的所有.meta文件
  2. Game.app同级目录创建Game.entitlements
  3. 修改
    Game.app/Contents/Info.plist
    中的信息
  4. 处理第三方lib,修改bundleId或删除symbol
  5. 对所有lib签名
  6. 对Game.app签名,这里使用Mac App Distribution证书进行签名

生成pkg文件

签名完成后需要将 Game.app pack成 Game.app.pkg文件。

1
2
3
4
productbuild --component ${BINARY} /Applications --sign "3rd Party Mac Developer Installer:" "${PKG_FILE}"

#sample
productbuild --component Game.app /Applications --sign "3rd Party Mac Developer Installer:" Game.app.pkg

pack成功之后会得到一个Game.app.pkg

使用AppLoader提交到itune服务器

和iOS开发相同,使用application loader提交的itunes服务器进行验证。由于最新的xcode 11不再提供GUI版本的的 application loader。我们只能使用命令行xcrun alttol进行处理

  1. 首再提交到itunes之前先验证。
1
xcrun altool --validate-app -f Game.app.pkg -u "${USER}" -p "${PWD}"

等到处理结果,如果提示not error则我们可以提交应用。

需要注意的是xcrun altool --validate-app并不会查出所有的错误,再提交到itunes之后,apple还会对包体进行验证。可能本地validation没有错误,但是还是会被apple拒绝。

  1. 上传pkg到itune服务器
1
$ xcrun altool --upload-app -f Game.app.pkg -u "${USER}" -p "${PWD}"

提示no error uploading package,就表示上传成功了。我们只需要等待itunes后台处理。

如果收到了Apple的邮件:
App Store Connect: Your app "XXXXX" (Apple ID: 123456789 Version: 1.0 Build: 0) has one or more issues
就表示本次提交的包里面还有其他错误。

当这个版本的提交itunes服务器通过了验证,则不会收到邮件,直接在itunes connect后台可以看到提交的版本了。

在iTunes服务器验证失败后,就需要重新走一遍codesign->pkg->upload的流程。和ios开发相同,对于同一个版本每次提交必须要有不同的build,手动修改Game.app/Contents/Info.plist的build version重新操作即可。


FAQ

ITMS-90303: Unable to Sign
code object is not signed at all In subcomponent: 有额外的文件没有删除,删除文件后再签名。

ITMS-90238: Invalid Signature

某些lib文件没有签名,或者有额外的文件没有删除

codesign 时提示CSSMERR_TP_CERT_REVOKED

证书过期需要重新从apple developer后台下载证书

codesign 提示 errSecInternalComponent

需要解锁Security的keychain读取证书进行签名


Reference

Unity Manual: Delivering your application to the Mac App Store
gist:how-to-notarize-unity-for-macos.md
Notarizing macOS Software Before Distribution
Technical Note TN2206:macOS Code Signing In Depth


感谢阅读,如果有任何错误或者问题请下下面留言。