コンテンツへスキップ

Python デスクトップアプリを Windows と Mac の二刀流で配布する — リリースフローの全工程

WP メンテナンスマネージャは、同じ Python コードベースで Windows と macOS の両方に配布しています。「Python なら write once でクロスプラットフォーム」と言われがちですが、配布までのフローは OS ごとに完全に別で、それぞれに固有のハマりどころがあります。

PyInstaller / Inno Setup / Apple Notarization / eSigner — 各 OS ごとに必要なツールチェーンを組み合わせて 1 つのリリースサイクルを回しているので、その全体像と注意点を残しておきます。アプリの内部構造(Flask + ブラウザ UI)を選んだ理由は 別記事 を参照。本稿はその構造を 2 OS で配布する側の話です。

OS 別フローの全体像

段階 Mac Windows
ビルド PyInstaller(--target-arch x86_64 PyInstaller
配布フォーマット .app バンドル → .dmg フォルダ → .exe インストーラー
インストーラー作成 hdiutil / create_dmg.sh Inno Setup(.iss スクリプト)
コード署名 codesign + Developer ID 証明書 eSigner CSC(クラウド署名)
配布前の検証 Apple Notarization(公証) SmartScreen 評価ビルドの蓄積
最終成果物 WP_Maintenance_Pro_X.X.X.dmg WP_Maintenance_Pro_Setup_X.X.X.exe

両 OS とも PyInstaller を使う共通点はあるものの、その後の流れは別物。Mac は Apple の審査プロセス、Windows は Microsoft の評価ビルド経由、という根本的に違うエコシステムに乗ります。

Mac 側 — PyInstaller → 署名 → Notarization → DMG

Intel/Apple Silicon 両対応の罠

Mac の PyInstaller ビルドで最初に踏むのは アーキテクチャ問題。Apple Silicon Mac で素直に pip install + python build_app.py すると、cffi などのネイティブバイナリが arm64 だけで含まれて、Intel Mac で動かなくなります。

解決策は、ビルド全体を arch -x86_64 経由で実行する:

arch -x86_64 pip3 install -r requirements.txt
arch -x86_64 python3 build_app.py

これで x86_64 バイナリだけが入った .app が出力され、Intel Mac はネイティブ、Apple Silicon Mac は Rosetta 2 経由で動く形に統一できます。

署名は内側→外側の順で

PyInstaller が出力した .app は、内部に多数の Mach-O バイナリ(cryptography_cffi_backend.so 等)を含みます。codesign --deep で一括署名する素直なやり方は Hardened Runtime と相性が悪いケースが報告されているので、file コマンドで Mach-O を検出して内側から個別署名する方式を採用しています:

find "${APP_BUNDLE}" -type f -exec file {} \; \
  | grep "Mach-O" | cut -d: -f1 \
  | while read bin; do
    codesign --force --options=runtime \
      --entitlements "${ENTITLEMENTS}" \
      --sign "${APP_CERT}" "${bin}"
  done

# 最後に .app 全体を署名
codesign --force --options=runtime \
  --entitlements "${ENTITLEMENTS}" \
  --sign "${APP_CERT}" "${APP_BUNDLE}"

Notarization は ZIP → DMG の順

公証申請は DMG をそのまま投げるより ZIP のほうが速い経験則があります。Apple のバックエンドで DMG 解析が複雑な経路を通るのに対し、ZIP は単純なスキャンパスに乗るためです。

ditto -c -k --keepParent "${APP_BUNDLE}" "${ZIP_PATH}"
xcrun notarytool submit "${ZIP_PATH}" \
  --keychain-profile "wpmm-notary" --wait
xcrun stapler staple "${APP_BUNDLE}"
# DMG は staple 済みの .app を封入して作成

公証が In Progress のまま動かないケースもあって、Apple Developer Program の Tax Form / 銀行口座情報の未提出が原因だったり、サポートに問い合わせると複数の保留申請が一気に Accepted になったりします。技術的設定ではなく契約系の不備で詰まることが意外と多い領域です。

Windows 側 — PyInstaller → Inno Setup → eSigner CSC

Inno Setup スクリプト

Windows は WP_Maintenance_Pro.iss という Inno Setup スクリプトでインストーラーを組み立てます。管理者権限を要求しない %APPDATA% インストール、起動時のショートカット、アンインストール時の残留ファイル削除、などをここで定義します:

[Setup]
AppId={{B3A7F2C1-4E8D-4A9F-B2C3-D5E6F7A8B9C0}
DefaultDirName={userappdata}\{#MyAppName}
PrivilegesRequired=lowest

PrivilegesRequired=lowest でユーザーモードインストールにしているのは、企業環境で管理者権限を持たないユーザーでも導入できるようにするため。UAC ダイアログが出ないだけで、サポート問い合わせが目に見えて減る効果がありました。

eSigner CSC でクラウド署名

Windows のコード署名は、従来は物理 USB トークンに証明書を入れる方式が主流でしたが、eSigner CSC(SSL.com の提供するクラウド署名サービス)を使うと、トークンを刺さずに自動化スクリプトから署名できます。

& "C:\esigner\CodeSignTool.bat" sign `
  -input_file_path "WP_Maintenance_Pro_Setup.exe" `
  -output_dir_path "signed/" `
  -credential_id "${CRED_ID}" `
  -username "${USERNAME}" -password "${PASSWORD}" `
  -totp_secret "${TOTP_SECRET}"

OV/EV 証明書のグレード差や、SmartScreen への評価ビルド蓄積(配布数が積み上がるまで一定期間「Unknown publisher」警告が出る)など、Windows 固有の事情はそれぞれ別記事級ですが、まず動かすところまでなら eSigner CSC の自動化が現実的です。

共通の課題 — バージョン同期と再現性

両 OS のリリースで毎回踏むのが バージョン番号の同期version.pyWP_Maintenance_Pro.issMyAppVersionserver/wpmm-web/version.json・LP のダウンロードリンク — リリースのたびに 4 ファイル以上を一致させる必要があり、片方ずれると配信側で「最新版が出てるのにアプリ内アップデートで気づかない」のような事故になります。

release.py で一括更新するスクリプトを用意していますが、後付けでファイルが増えるたびに追従が要るので、リリース時のチェックリストは必須です。

振り返り — 「同じ Python」なのに別物の配布

「クロスプラットフォーム」と一言で言っても、配布側の手間は OS で完全に分かれます。バイナリビルドは PyInstaller で共通化できても、インストーラー・コード署名・OS 側の検証は別エコシステムなので、それぞれの作法に従うしかない。

それでも一度フローを固めてしまえば、新しいリリースは python build_app.py + bash sign_and_notarize.sh + Inno Setup F9 で 30 分程度で出せるようになります。初回の構築コストは高いが、固めれば回しやすい、というのが二刀流リリースの実感です。