0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【脱・属人化】Python製ツールの配布戦略:exeのバージョン管理からリリース自動化まで

Posted at

はじめに

京セラコミュニケーションシステム株式会社 ICT事業本部 先端技術統括部の中橋(なかはし)です。
私たちの組織では、社内の開発プロセス改善を推進しており、特に業務効率化ツールの開発や、開発プロセスの自動化に取り組んでいます。今回はその取り組みの中で得た自動化の知見を、皆さんが一度は感じたことがあるであろう「あるある」を解決する方法としてご紹介します。

Pythonツール配布に潜む3つの課題

Pythonで作った便利なツールを他部署のメンバーに配布したい─。そんなとき、こんな課題に直面しませんか?

課題 解決策 方法
実行環境の問題
例:
相手のPCにPythonがない。あってもバージョンが違って動かない
Python環境に依存しないツールを配布する PyInstallerによるexe化
バージョン管理の問題
例:
このexe、どの機能まで入れた最新版だっけ…?
ファイルのプロパティから一目でバージョンを確認できるようにする バージョン情報をexeに埋め込む
属人化の問題
例:
ビルド手順が複雑で、自分しかリリースできない…
誰でも簡単にリリースできる仕組みを構築する GitHub Actionsによる自動化

本記事では、これらの課題を解決し、誰でも安心して使え、継続的にメンテナンスできるPython製ツールの配布戦略を考え、具体的な手順を交えて紹介します。

本記事で学べること

  • Pythonのパッケージ管理とexe化の基本
  • バージョン情報をexeファイルへ反映させる方法
  • GitHub Actionsによるビルド&リリースの自動化

想定読者

  • Pythonでツールを作成し、チーム内外に配布したい方
  • CI/CDによる自動化に興味がある方
  • 属人化を解消し、チーム開発を改善したい方

環境構築・前提条件

環境

まず、開発環境については以下のような構成になっています。

項目 バージョン
OS Windows11
Python 3.12.4
pyinstaller 6.15.0

環境について
最終的にGitHub ActionsのWindowsランナーを使用するため、ローカル開発環境はWindowsでなくても問題ありません。ただし、Linux等で開発している場合、ローカルでの動作確認が一部できませんので、その点はご注意ください。

注意
開発環境構築時には、Pythonの仮想環境(venv) を使用することをおすすめします。

プロジェクト構成

プロジェクトは以下のような構成で考えます。

project/                          # プロジェクトのルートディレクトリ
├── mypackage                     # 自作パッケージ
│   ├── __init__.py
│   ├── __main__.py
│   ├── ...
│   └── subpackage                # サブパッケージ
│       ├── __init__.py
│       ├── ...
├── tests/                        # テストコード
│   └── ...
├── main.py                       # exe化の対象となるメインスクリプト
├── setup.py or pyproject.toml    # パッケージ管理
├── VERSION.txt                   # バージョン情報
└── requirements.txt              # 依存パッケージ一覧

この構成では、mypackageとして実装した機能を、main.pyから呼び出して実行する形式を取ります。setup.pypyproject.tomlは、パッケージ管理とメタデータ定義に使用します。

Pythonパッケージ管理の基礎知識

Pythonでは、プロジェクトの依存関係やメタデータを管理するためのさまざまなツールやファイルが存在します。ここでは、setup.pypyproject.tomlそれぞれのパッケージ管理方法について紹介します。

バージョン管理

まず、VERSION.txtには、以下のようにバージョン情報を記述します。

1.0.0.0

setup.pyを使用する方法

setup.pyは、プロジェクトのビルドやインストールを定義するPythonスクリプトです。以下のように、setuptoolsライブラリを使用して記述します。この例では、requirements.txtVERSION.txtから依存関係とバージョン情報を読み込んでいます。

from setuptools import setup, find_packages


with open(file='requirements.txt', mode='r', encoding='utf-8') as require_file:
    requires = require_file.read().splitlines()


with open(file='VERSION.txt', mode='r', encoding='utf-8') as vf:
    version = vf.read().strip()


setup(
    name='MyApp',
    version=version,
    description='自作アプリ',
    packages=find_packages(exclude=['tests', 'tests.*']),
    package_data={
        'mypackage': ['*.*', '**/*.*']
    },
    include_package_data=True,
    requires=requires
)

pyproject.tomlを使用する方法

pyproject.tomlは、PEP 518/621で導入された新しい標準的な設定ファイルです。ビルドシステムの要件やプロジェクトのメタデータを一元管理できるため、近年主流になりつつあります。ここでは、setuptoolsを使用する設定例を紹介します。

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "MyApp"
description = "自作アプリ"
requires-python = ">=3.11"
dynamic = ["version", "dependencies"]

[tool.setuptools]
packages = ["mypackage"]
include-package-data = true

[tool.setuptools.package-data]
"project" = ["*.*", "**/*.*"]

[tool.setuptools.dynamic]
version = {file = "VERSION.txt"}
dependencies = {file = ["requirements.txt"]}

dependenciesについての補足
本記事では、既存のrequirements.txtを活かして移行する方法を紹介しています。将来的には、依存関係をpyproject.tomlに直接記述して管理することが推奨されます。

[project]
name = "MyApp"
description = "自作アプリ"
requires-python = ">=3.11"
dependencies = ["requests"]  # pyproject.tomlに記述する
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {file = "VERSION.txt"}

自作パッケージのインストール方法

以下のコマンドで自作パッケージをローカルにインストールできます。

pip install .

PyInstallerでexe化とバージョン情報の埋め込み

このセクションでは、PythonスクリプトをWindows上で実行可能なexeファイルに変換するPyInstallerの使い方と、exeにバージョン情報を埋め込む方法について解説します。

基本

PyInstallerのインストール

まず、pipを使ってPyInstallerをインストールします。

pip install pyinstaller

Pythonファイルをexe化する

例えば、main.pyというPythonファイルをexe化するには、以下のようなコマンドを実行します。

pyinstaller main.py

上記のコマンドを実行すると、distフォルダにmain.exeというexeファイルが生成されます。しかし、このままでは関連ファイルが多数生成されたり、実行時にコンソールが表示されたりするため、より実用的なオプションを組み合わせるのが一般的です。

ここで、私がよく使うオプションについて簡単にまとめます。

オプション 説明 用途
--clean ビルド前に PyInstaller のキャッシュと一時ファイルを削除 古いビルドファイルやキャッシュが原因の不具合を防ぐために使用
-F, --onefile 関連ファイルを1つのファイルにまとめた実行ファイルを作成 単一の実行ファイルとして配布したい場合に使用
-n NAME, --name NAME 出力ファイル名を指定(デフォルト: 最初のスクリプトのベース名) 出力ファイル名を任意に設定したい場合に使用
--collect-all MODULENAME 指定したパッケージまたはモジュールの全リソースを収集する 特定のパッケージの依存関係やリソースをすべて含めたい場合に使用
-w, --windowed, --noconsole コンソール非表示で実行する GUIアプリケーションを作成し、コンソールウィンドウを表示したくない場合に使用
--version-file FILE 指定ファイルからバージョンリソースを埋め込む .exe のプロパティにバージョン/説明等を付与

上記のオプションを使用したより実用的な例です。mypackage--collect-allで収集し、myapp.versionというファイルからバージョン情報を埋め込みます。

pyinstaller main.py --clean --onefile -n MyApp --collect-all mypackage --windowed --version-file myapp.version

exeへのバージョン埋め込み

PyInstallerで作成したexeファイルにバージョン情報を埋め込むと、ファイルのプロパティからバージョンを確認できるようになります。これにより、ユーザーが使っているバージョンを簡単に把握できます。

exeProperty1000_dl.png

exeへバージョンを埋め込む場合は、先に述べた--version-fileオプションを使用します。しかし、バージョンファイルを手動で記述するのは手間がかかるため、サードパーティpyinstaller-versionfileを使用します。

pyinstaller-versionfileのインストール

pyinstaller-versionfileをインストールするには、以下のいずれかのコマンドを実行します。

  • PyPIからインストールする場合
pip install pyinstaller_versionfile
  • GitHubリポジトリから直接インストールする場合
pip install git+https://github.com/DudeNr33/pyinstaller-versionfile.git@7e133fc9ffb999d0b4d265038520525f02154126

注意
※2025年8月5日時点においてPyPI経由のインストールでは特定のバグが修正されていないため、GitHubリポジトリからインストールすることをお勧めします。

バージョンファイルの作成

pyinstaller-versionfileでは、バージョンファイルを自動で生成できます。ここでは、ローカルにインストール済みのPythonパッケージのメタ情報からバージョンファイルを作成する方法を紹介します。

まず、setup.pypyproject.tomlを使って、作成した自作パッケージをローカルにインストールします。

pip install .

次に、以下のPythonスクリプト(ファイル名はcreate_version_file.pyとします)を実行します。これにより、Pythonパッケージのメタデータからバージョン情報が自動生成され、myapp.versionというファイルが作成されます。

# create_version_file.py
from pyinstaller_versionfile import create_versionfile_from_distribution


create_versionfile_from_distribution(
    output_file='myapp.version',
    distname='MyApp'
)

もし、自動生成された情報の一部を上書きしたい場合は、create_versionfile_from_distribution()の引数に辞書形式で値を渡すことで、内容をカスタマイズできます。

data: dict[str, str | list[int]] = {
    'company_name': '京セラコミュニケーションシステム株式会社',  # 会社名
    'legal_copyright': '© Kyocera Communication Systems Co., Ltd All rights reserved.',  # コピーライト
    'original_filename': 'MyApp.exe',  # ファイル名
    'product_name': 'MyProduct',  # プロダクト名
    'translations': [0x0411, 1200]  # 言語設定
}


create_versionfile_from_distribution(
    output_file='myapp.version',
    distname='MyApp',
    **data
)

生成されたバージョンファイル(myapp.version

# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx

VSVersionInfo(
  ffi=FixedFileInfo(
    # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
    # Set not needed items to zero 0. Must always contain 4 elements.
    filevers=(1,0,0,0),
    prodvers=(1,0,0,0),
    # Contains a bitmask that specifies the valid bits 'flags'r
    mask=0x3f,
    # Contains a bitmask that specifies the Boolean attributes of the file.
    flags=0x0,
    # The operating system for which this file was designed.
    # 0x4 - NT and there is no need to change it.
    OS=0x40004,
    # The general type of file.
    # 0x1 - the file is an application.
    fileType=0x1,
    # The function of the file.
    # 0x0 - the function is not defined for this fileType
    subtype=0x0,
    # Creation date and time stamp.
    date=(0, 0)
    ),
  kids=[
    StringFileInfo(
      [
      StringTable(
        u'040904B0',
        [StringStruct(u'CompanyName', u'京セラコミュニケーションシステム株式会社'),
        StringStruct(u'FileDescription', u'自作アプリ'),
        StringStruct(u'FileVersion', u'1.0.0.0'),
        StringStruct(u'InternalName', u'MyApp'),
        StringStruct(u'LegalCopyright', u'© Kyocera Communication Systems Co., Ltd All rights reserved.'),
        StringStruct(u'OriginalFilename', u'MyApp.exe'),
        StringStruct(u'ProductName', u'MyProduct'),
        StringStruct(u'ProductVersion', u'1.0.0.0')])
      ]), 
    VarFileInfo([VarStruct(u'Translation', [1041, 1200])])
  ]
)

このmyapp.versionファイルを、pyinstallerコマンドの--version-fileオプションで指定することで、exeにバージョン情報を埋め込むことができます。

言語設定
translationsの値については、pyinstaller-versionfileのドキュメントおよびMSのドキュメントをご参照ください。本記事では、日本語(0x0411)とUnicode(1200)を指定しています。

パッケージ管理とexeバージョン埋め込みの連携

個人的に、設定値をスクリプト内に直接記述(ハードコーディング)するのは避けたいと考えています。そのため、これらの設定値を外部ファイルで管理する方法をいくつか紹介します。

例えば、以下のようにversion_info.ymlというYAMLファイルを作成します。

company_name: 京セラコミュニケーションシステム株式会社
legal_copyright: © Kyocera Communication Systems Co., Ltd All rights reserved.
original_filename: MyApp.exe
product_name: MyProduct
translations:
  - 0x0411
  - 1200

このYAMLファイルを読み込むようにcreate_version_file.pyを以下のように修正します。

import yaml
from pyinstaller_versionfile import create_versionfile_from_distribution


# 直接記述せず、設定ファイルから読み込む
with open(file='version_info.yml', mode='r', encoding='utf-8') as f:
    version_info: dict[str, str | list[int]] = yaml.safe_load(f)


create_versionfile_from_distribution(
    output_file='myapp.version',
    distname='MyApp',
    **version_info
)

パッケージ管理において、pyproject.tomlを使用している場合は、設定をこのファイルに統合することで、バージョン情報を一元管理できます。

# pyinstaller用のカスタムメタデータ
[tool.pyinstaller-versionfile]
company_name = "京セラコミュニケーションシステム株式会社"
legal_copyright = "© Kyocera Communication Systems Co., Ltd All rights reserved."
original_filename = "MyApp.exe"
product_name = "MyProduct"
translations = [
    0x0411, 1200
]

この場合、create_version_file.pypyproject.tomlを読み込むように、以下のように修正します。

import tomllib

from pyinstaller_versionfile import create_versionfile_from_distribution


with open('pyproject.toml', 'rb') as f:
    pyproject_data = tomllib.load(f)


version_info: dict[str, str | list[int]] = pyproject_data.get('tool', {}).get('pyinstaller-versionfile', {})
    

create_versionfile_from_distribution(
    output_file='myapp.version',
    distname='MyApp',
    **version_info
)

GitHub Actionsでビルドとリリースを自動化

これまでの手順をGitHub Actionsを使って自動化することで、誰でも簡単にツールをリリースできる仕組みを構築します。

ワークフロー設計

ワークフローは、以下のようなシナリオで設計します。

  1. 開発者がmainブランチに対してPull Requestを作成・マージする。
  2. mainブランチへのマージをトリガーに、GitHub Actionsが自動で起動する。
  3. GitHub Actionsが、pyinstallerを使ってexeファイルをビルドする。
  4. ビルドされたexeファイルを、GitHubのリリースページにアップロードする。

このワークフローを構築することで、開発者はコードをマージするだけでリリースが完了し、リリース作業の属人化を防ぐことができます。

ワークフローの構築

それでは、このワークフローを具体的に構築していきます。

まず、プロジェクトルート直下に.github/workflowsディレクトリを作成し、その中にrelease.ymlファイルを作成します。

1. ワークフロー名とトリガーの設定

最初に、ワークフローの名前と、ワークフローを実行するタイミング(トリガー)を定義します。ここでは、mainブランチへのプルリクエストがマージされたときに起動するように設定します。

name: Release  # ワークフローの名前

on:
  pull_request:
    branches:
      - "main"  # 監視するブランチ
    types:
      - "closed"  # プルリクエストが閉じられたときにトリガー

pull_requestイベントのclosedタイプを使用することで、プルリクエストがマージされた後(またはクローズされた後)にのみ、ワークフローが実行されます。

2. ジョブ名・ランナーOS・権限の設定

次に、ワークフローの具体的な処理を定義するジョブを作成します。

jobs:
  release:  # ジョブの名前
    if: github.event.pull_request.merged == true  # プルリクエストがマージされた場合のみ実行
    runs-on: windows-latest  # Windows環境で実行
    permissions:
      contents: "write"  # リリース作成に必要な権限
      pull-requests: "read"  # Pull Requestの本文の読み取り権限
  • if条件: github.event.pull_request.merged == trueとすることで、プルリクエストがマージされた場合のみジョブが実行されます。

  • runs-on: windows-latestを指定することで、最新のWindows環境でビルドを実行します。

  • permissions: リリースを作成するためにcontents: write権限が必要です。

3. ビルドからリリースノート作成までのステップ

ビルドからリリースまでの具体的なステップを定義します。

    steps:

      - name: Checkout Repository  # リポジトリのチェックアウト
        uses: actions/checkout@v4
      
      - name: Set up Python  # Python環境のセットアップ
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      
      - name: Install dependencies  # 必要なライブラリのインストール
        run: |
          python -m pip install --upgrade pip
          pip install .
          pip install pyinstaller==6.15.0
          pip install git+https://github.com/DudeNr33/pyinstaller-versionfile.git@7e133fc9ffb999d0b4d265038520525f02154126
        shell: pwsh
      
      - name: Set VERSION environment variable from VERSION.txt  # バージョン情報の環境変数へのセット
        run: |
          if (-not (Test-Path VERSION.txt)) {
            Write-Error "VERSION.txt not found."
            exit 1
          }
          $raw = Get-Content VERSION.txt -Raw
          $version = $raw.Trim()
          echo "VERSION=$version" >> $env:GITHUB_ENV
        shell: pwsh
      
      - name: Display MyApp version
        run: |
          Write-Host "MyApp VERSION: ${{ env.VERSION }}"
        shell: pwsh
      
      - name: Create version file  # PyInstaller用のバージョンファイルを作成
        run: | 
          python create_version_file.py
        shell: pwsh
      
      - name: Build with PyInstaller  # PyInstallerでexeをビルド
        run: |
          pyinstaller main.py --clean --onefile -n MyApp --collect-all mypackage --windowed --version-file myapp.version
        shell: pwsh
      
      - name: Create Release with gh CLI  # ビルドされたexeファイルをリリースに添付
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create v${{ env.VERSION }} .\dist\MyApp.exe `
          --title "Release v${{ env.VERSION }}" `
          --notes "${{ github.event.pull_request.body }}" `
          --target main
        shell: pwsh
  • Checkout Repository: actions/checkout@v4を使って、リポジトリの最新のコードをチェックアウトします。

  • Set up Python: actions/setup-python@v5を使って、Python環境をセットアップします。

  • Install dependencies(※): PowerShell(shell: pwsh)を使って必要なライブラリをインストールします。ここではpip install .で自作パッケージを、pip install pyinstallerでPyInstallerをインストールしています。

  • Set VERSION environment variable: VERSION.txtからバージョン情報を読み込み、GitHub Actionsの環境変数VERSIONに設定します。このバージョンは、リリース名やタグとして使用します。

  • Create version file: 前述のpyinstaller-versionfileを使って、exeに埋め込むためのバージョンファイルを生成します。

  • Build with PyInstaller: pyinstallerコマンドを実行して、exeファイルをビルドします。--version-fileオプションで先ほど作成したバージョンファイルを指定しています。

  • Create Release with gh CLI: ghコマンド(GitHub CLI) を使用して、GitHubのリリースを作成します。

    • gh release create: リリースを作成するコマンドです。

    • gh release create ${{ env.VERSION }} ".\dist\MyApp.exe": タグ名と添付するファイル(ビルドしたexe)を指定します。

    • --title "Release ${{ env.VERSION }}": リリースのタイトルをバージョン情報から生成します。

    • --notes "${{ github.event.pull_request.body }}": Pull Requestの本文をリリースノートに自動で記載します。これにより、リリースごとの変更点を自動的に記載できます。

    • --target main: リリースが紐づくブランチを指定します。

このワークフローを実行することで、mainブランチへマージされるたびに、自動的にビルドとリリースが行われるようになります。

※補足
上記ワークフローで使用している pyinstaller および pyinstaller-versionfile は、アプリ実行時の依存関係ではなくビルド用ライブラリです。そのため、通常の依存関係(requirements.txt)には含めず、個別にインストールしています。
なお、依存関係をより厳密に管理したい場合には、実行用とビルド用を分離する方法があります(例:requirements-dev.txt、extra_requires/optional-dependencies、pip install .[dev])。
ただしこれは、本記事の主題からは外れるため、ここでは詳細を扱いません。

参考サイト

以下、本記事を書くにあたって参考にしたサイトです。

おわりに

本記事では、GitHub Actionsを用いたPythonプログラムのリリース自動化について解説しました。
ここで重要なのはPythonでも実行ファイルにバージョンを付与する方法でもありません。

私がこの取り組みで最も大切だと感じているのは、「誰でも、安心して、継続的に」という3つのキーワードです。

  • 誰でも: 属人化をなくし、チームの誰もがツールをメンテナンスできること
  • 安心して: バージョン情報が明確で、安定したツールを配布できること
  • 継続的に: 開発・配布プロセスを自動化し、メンテナンスコストを下げて改善を続けられること

これらの観点を重視することで、チーム開発の質を向上させ、効率的で信頼性の高いソフトウェア開発が可能になると考えています。

本記事で紹介した方法が、皆さんのプロジェクトやチーム開発の一助となれば幸いです。

所属部署について

ITエンジニアが活躍する地方拠点「長崎 Innovation Lab」

長崎 Innovation Labでは、IT技術を活用して、工場などものづくりの現場で活用できるシステムの開発に取り組んでいます。自由な発想でこれまでにない社会に役立つ製品・サービスを生み出し、長崎から発信していくことを目指しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?