根据苹果开发者网站上的消息,2020年2月3日之后,macOS Catalina在默认设置下,只能运行经过苹果公证的应用程序。碰巧公司macOS版本的客户端也因为其他原因,需要升级Python和PyQt5的版本。磕磕碰碰弄了一周,终于完成升级并通过了苹果的公证。在此记录一下过程和遇到的一些坑。
相关版本
Python: 3.6.8
PyQt5: 5.12.3(对应Qt5版本5.12.6)
PyInstaller: 3.5
Packages: 1.2.7
对APP结构的改动
目前苹果要求App/Contents/MacOS下的所有文件都具有签名。但截止到1.2.7版本,Packages对保留文件的扩展属性这个功能的支持依然存在问题。而苹果对非可执行文件的签名是存放在文件的扩展属性中的。这就导致已经被签名的非可执行文件,在Packages打包后,会丢失签名信息。
目前的解决方案是,想办法将非可执行文件移动到App/Contents/Resources目录下。
PyInstaller需要移动的文件
移动App/Contents/MacOS/base_library.zip
到App/Contents/Resources
下,在原路径下创建名为base_library.zip
,指向App/Contents/Resources/base_library.zip
的链接文件
PyQt5需要移动的文件
- 使用Recipe-OSX-Code-Signing-Qt中的fix_app_qt_folder_names_for_codesign.py脚本,并对脚本做一些修改。
将开始迭代的路径从App/Contents/MacOS
变为App/Contents/MacOS/PyQt5/Qt
, 将需要移动的判断标准从带.的文件夹
变为所有文件夹
。
运行后将App/Contents/MacOS/PyQt5/Qt
下的文件夹被移动App/Contents/Resources/PyQt5/Qt
下,在原路径留下一个链接文件,且移动后所有可执行文件的动态库查找路径均被修复。 - 遍历
App/Contents/Resources/PyQt5/Qt/lib
目录下所有Qt*.framework
目录,将在App/Contents/MacOS
下创建名称为Qt*
, 指向App/Contents/Resources/PyQt5/Qt/lib/Qt*.framework/Versions/5/Qt*
的链接文件,替换原有的实体文件。
例如:删除App/Contents/MacOS/QtCore
,创建同名链接文件,指向App/Contents/Resources/PyQt5/Qt/lib/QtCore.framework/Versions/5/QtCore
。
这么做可以显著减少安装包体积。 - 如果项目中使用了QtWebEngine,此时运行app,Qt会提示错误
Could not find QtWebEngineProcess
。因为QtWebEngineProcess在1中被移动了。需要在App启动时修改环境变量QTWEBENGINEPROCESS_PATH
,指向App/Contents/Resources/PyQt5/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/Resources/QtWebEngineProcess
,让Qt可以正确的找到QtWebEngineProcess。1
2
3
4
5
6
7
8os.environ['QTWEBENGINEPROCESS_PATH'] = os.path.abspath(
os.path.join(
os.path.dirname(sys.executable),
'..',
'Resources/PyQt5/Qt/lib/QtWebEngineCore.framework',
'Helpers/QtWebEngineProcess.app/Contents/Resources/QtWebEngineProcess',
)
)
签名
授权文件
参照notarizing_macos_software_before_distribution和
Enable hardened runtime (macOS),被公证的app需要开启hardened runtime。通过Xcode维护的工程,可以按照Add a capability to a target,添加相关能力即可。但我们的工程并没有Xcode来管理,需要自己创建一个授权文件。
1 |
|
在文件内添加项目工程需要使用到的能力。相关能力可以在hardened_runtime_entitlements中查看
PyInstaller在运行时,会解压一部分可执行文件到临时目录,再执行这些可执行文件。如果不修改源代码,这些文件是没有签名的。运行时会报Memory Error。需要在授权文件中增加Allow Unsigned Executable Memory Entitlement的能力。
签名
签名时需要增加的选项:--options runtime
用于标识开启Hardened runtime--timestamp
显式的增加时间戳。否则后续公证时会报The signature does not include a secure timestamp.
的错误--entitlements entitlement_file_path
用于标识需要开启的能力
一开始我们直接使用如下命令签名
1 | codesign -s cert_id \ |
但在使用时发现有几个问题:
- 经常因为网络原因导致获取时间戳失败,最终导致签名失败。如果重试,则会重签整个App。
- –deep在分析依赖时经常会出现问题,导致漏签某个文件,或者某个文件依赖的文件没有签名导致签名中断。
最终的解决方案是:
使用
1
2
3
4
5
6codesign -s cert_id \
-f \
--options runtime \
--timestamp \
--entitlements entitlement_file_path \
file_path对单个文件签名
遍历
App/Contents/MacOS
下的文件,对主程序以外的文件进行签名遍历
App/Contents/Resources
下的文件,对.dylib
后缀的文件进行签名对
App/Contents/Resources/PyQt5/Qt/lib/Qt*.framework/Versions/5/Qt*
(即上文提到的需要软链到App/Contents/MacOS的Qt动态库)进行签名如果需要使用QtWebEngine,也需要对
App/Contents/Resources/PyQt5/Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/Resources/QtWebEngineProcess
进行签名
打包
到这一步时,App/Contents/MacOS
下只有可执行文件,且都进过了签名。App/Contents/Resources
下的所有可执行文件也都经过了签名。之后就可以和平常一样,使用Packages生成pkg文件。
公证
这一步相对来说比较简单,根据customizing_the_notarization_workflow的介绍一步一步来即可。
上传
1 | xcrun altool --notarize-app \ |
上传成功后会输出类似于
1 | RequestUUID: 12345678-abcd-1234-abcd-123456789abc |
的结果。uuid在后续查询公证状态时会用到
查看公证结果
1 | xcrun altool --notarization-info 12345678-abcd-1234-abcd-123456789abc \ |
输出结果的Status
字段会显示本次公证的状态。LogFileURL
字段则会提供指向一个json文件的网址,记录公证的文件内容和发现的问题。需要重点关注json文件中的issues
字段。
将公证结果附加到安装包上
1 | xcrun stapler staple pkg_path |
检查附加结果是否有效
1 | xcrun stapler validate pkg_path |
其他
- 在新部署的环境上执行上传app操作时,会下载和更新相关的jar包。这个过程可能会比较漫长。最好在第一次执行时,增加
--verbose
参数,查看jar包的下载进度。如果发现很长时间没有动静的话,去活动监视器中退出altool创建的java进程,等待
命令行自行退出后再重试。 - 处理公证结果中LogFileURL指出的问题时,按照resolving_common_notarization_issues中提到的进行处理。如果提示主进程
The signature of the binary is invalid.
,有可能是App/Contents/MacOS
中的文件,在签名时被遗漏了,或是打包时被移除了签名。建议执行查看是哪个文件没有签名。1
codesign -vvv -deep app_path