PySide6 実践講座 #8 配布の仕上げ — アイコン・バージョン情報、署名、GitHub Actions
機能が終わったアプリと、配布できるアプリの間には最後の区間があります。アイコン、バージョン情報、署名、そして「自分のパソコンでしかビルドできない」状態からの脱出です。入門 #7 で PyInstaller の基本を扱ったので、今回はその上に載せる仕上げの作業です。
spec ファイル — ビルド設定もコードです #
入門ではコマンドラインのオプションでビルドしましたが、オプションが増えたら spec ファイルに移すのが正解です。pyinstaller コマンドの初回実行時に作られる daily.spec をリポジトリにコミットし、直接管理します。
# daily.spec(核心のみ)
a = Analysis(
["src/daily/__main__.py"],
datas=[("assets/icons", "icons")], # リソースを同梱
)
pyz = PYZ(a.pure)
exe = EXE(
pyz, a.scripts, a.binaries, a.datas,
name="Daily",
icon="assets/daily.ico", # Windows 用アイコン
console=False, # ターミナルウィンドウなし
)
app = BUNDLE( # macOS の .app バンドル
exe,
name="Daily.app",
icon="assets/daily.icns",
bundle_identifier="com.example.daily",
)ビルドは pyinstaller daily.spec の 1 行に固定されます。押さえる細部が 2 つあります。第一に、アイコンはプラットフォームごとに形式が違います(Windows は .ico、macOS は .icns)。元の PNG ひとつから変換ツールで両形式を作って assets に置きます。第二に、spec に入れた datas のリソースは実行時に一時フォルダに展開されるので、コードからリソースのパスを探すときは、開発実行とバンドル実行を区別するヘルパーが必要です。
# src/daily/resources.py
import sys
from pathlib import Path
def resource_path(relative: str) -> Path:
if getattr(sys, "_MEIPASS", None): # PyInstaller バンドルの中
return Path(sys._MEIPASS) / relative
return Path(__file__).parent.parent.parent / "assets" / relativeバージョン — 一か所で定義し、全員が読む #
バージョン文字列があちこちに散らばると、リリースのたびにひとつずつ更新漏れが出ます。源泉を pyproject.toml の version ひとつに定め、残りはすべて読みに行きます。
# src/daily/__init__.py
from importlib.metadata import version
__version__ = version("daily")アプリの「情報」ダイアログ、ログの 1 行目、そして下の CI のリリースタグまで、この値ひとつを使います。リリース手順は「pyproject.toml のバージョンを上げる → タグをプッシュ」の 2 動作に圧縮されます。
署名 — 配布の最後の関門 #
ビルドしたアプリを別のパソコンに送ると、最初に出会うのは警告画面です。macOS は「開発元を確認できません」と実行を止め、Windows の SmartScreen は青い警告を出します。署名のない実行ファイルに対する OS のデフォルトの防御で、原理と手順は Wails 実践 #5 署名と公証で詳しく扱いました。Python アプリだからといって違いはないので、ここでは PySide6・PyInstaller 観点の要点だけ整理します。
- macOS: Apple Developer Program に加入 →
codesignで .app に署名 →notarytoolで公証を申請 → ステープル。PyInstaller の成果物に特有の注意は、バンドル内のすべてのバイナリ(Qt のフレームワークを含む)が署名対象であることと、hardened runtime オプションが必要なことです。 - Windows: コード署名証明書で
signtool署名。個人開発者には証明書のコストが負担なので、初期は署名なしで配布して SmartScreen の警告を案内ドキュメントで対応する選択も現実的です。 - 共通の原則: 署名は「誰が作ったのかの証明」です。個人の趣味の配布段階なら省略できますが、不特定多数に配布する瞬間からは通過儀礼として受け入れるのが正しいです。
GitHub Actions — 3 つの OS を一度にビルド #
「自分の Mac では macOS 用しかビルドできない」状態を抜け出す標準解が、CI のマトリックスビルドです。
# .github/workflows/build.yml
name: build
on:
push:
tags: ["v*"]
jobs:
test:
runs-on: ubuntu-latest
env:
QT_QPA_PLATFORM: offscreen # 第 7 回: 画面なしの Qt テスト
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync
- run: uv run pytest
build:
needs: test
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync
- run: uv run pyinstaller daily.spec
- uses: actions/upload-artifact@v4
with:
name: daily-${{ matrix.os }}
path: dist/構造は単純です。タグがプッシュされると、まずテストジョブが第 7 回のテストを offscreen で回し、通れば 3 つの OS のランナーがそれぞれ自分のプラットフォーム用の実行ファイルをビルドしてアーティファクトに上げます。PyInstaller はクロスコンパイルをしないので、各 OS でそれぞれビルドするマトリックスが唯一の正解です。ここにリリース作成のアクションをつなげば「タグプッシュ = 3 プラットフォームのリリース」が完成します。署名の工程を CI に入れるときは、証明書をリポジトリのシークレットで管理します。
シリーズを終えて #
8 回の旅路をひと段落で振り返ります。画面リストで要件を定義して 3 層構造を立て(第 1 回)、SQL をリポジトリに閉じ込めて user_version でマイグレーションに備え(第 2 回)、カスタムモデルでデータと画面をつなぎ(第 3 回)、既製品のない画面は QPainter で自作し(第 4 回)、トレイ常駐とリマインダーでデスクトップらしさをまとい(第 5 回)、QSettings とテーマで決定権をユーザーに返し(第 6 回)、層別テストで完成状態を固定し(第 7 回)、最後にアイコン・バージョン・署名・CI で人に渡せる品物に仕上げました(第 8 回)。
この順序自体が再利用できるテンプレートです。次に作るアプリがメモでも、時間トラッキングでも、写真整理でも、同じ骨組み(構造 → データ → 画面 → 常駐 → 設定 → テスト → 配布)に肉を差し替えればいいのです。Go 側の同じ旅路が気になるなら、Wails 実践講座と比べながら読むのも面白いはずです。
まとめ #
- ビルド設定は spec ファイルとしてリポジトリにコミットします。アイコンはプラットフォーム別形式(.ico/.icns)を用意し、リソースのパスはバンドル実行を区別するヘルパーで探します。
- バージョンは pyproject.toml の一か所で定義し、importlib.metadata で読みます。リリースはバージョンアップ + タグプッシュに圧縮されます。
- 署名・公証は不特定多数への配布の通過儀礼です。macOS は codesign + 公証、Windows は signtool で、原理は Wails 実践 #5 と同じです。
- CI はテスト(offscreen)→ 3 OS のマトリックスビルド → アーティファクトの流れです。PyInstaller にクロスコンパイルはないので、マトリックスが正解です。
- このシリーズの順序(構造 → データ → 画面 → 常駐 → 設定 → テスト → 配布)は、次のアプリにもそのまま使えるテンプレートです。