PyQt でクロスプラットフォームなデスクトップアプリケーションを

ここ何ヶ月かデスクトップアプリケーションにどっぷりな感じです。パッケージングをもっと簡単にしたい!ということで色々と試行錯誤しておりました。linux, mac はいい感じですが、Windows は・・・ py2exe でフリージングのみしかしていませんでした。配布とインストールは自動解凍書庫、アップデート、アンインストールは・・・。そこで今回 (やっと) 覚えたのが Inno Setup や WiX といった Windows 用のパッケージビルダです。備忘録がてら、Python でのパッケージングをまとめてみました。

パッケージングについて

大きく 2 つのフェーズに分かれています。

  1. フリージング: Python バンドルや他の必要なライブラリーを寄せ集め、実行可能形式にまとめます。
    Windows と OS X については以下のライブラリでフリージングします。

    Linux を含むクロスプラットフォームなフリージングができる cx_Freeze というのもあります。
  2. ディストリビューションのビルド
    • Windows: Microsoft Windows Installer はアップグレード、アンインストール、トランザクション処理を使った複数パッケージの管理とかの便利機能を持っています。
      • WiX (Wikipedia): MSI 形式でビルドするための MS 製のオープンソースツールです。WXS という XML ファイルを作成し、それを元にビルドします。
      • Inno Setup (日本ドキュメント): Windows インストーラ (Setup.exe) を作成するオープンソースツールです。Delphi で書かれています。
      • NSIS (Wikipedia): 多機能。スクリプト駆動型のWindows用インストールシステム。Mozilla, Google, BitTorrent などにも使われています。
      • 他にもあるかもです。
    • OS X: hdiutil: py2app でできた *.app を *.dmg にしてくれるらしいです (使ったことありません)。

クロスプラットフォームな setup.py の書き方

Windows と OS X のフリージングは、py2exe と py2app があるので簡単です。プラットフォーム情報を取得し、各プラットフォームに対してフリージングします。いろんなコードを見ましたが、切り分けるコードは大きく 2 種類ありました。私は簡単なので前者を使っています。

import sys

if sys.platform == 'win32':
    u"""Windows 用のフリージング"""

    import py2exe

    # 処理

if sys.platform == 'darwin':
    u"""OS X 用のフリージング"""

    import py2app

    # 処理

if sys.platform == 'linux2':
    u"""Linux 用のビルド"""

    import subprocess

    u"""
    makeself 等でインストーラを作成するコマンドを実行::
    
        ret = subprocess.Popen("makeself ...")
        ret.wait()
    
    cx_Freeze でもいいと思います。
    """
import platform

if platform.system() in ['Windows', 'Microsoft']:
    u"""Windows 用のフリージング"""

    # 省略

if platform.system() == 'Darwin':
    u"""OS X 用のフリージング"""

    # 省略

if platform.system() == 'Linux':
    u"""Linux 用のビルド"""

    # 省略

Inno Setup によるディストリビューションのビルド

Inno Setup は distutils の拡張モジュールが pypi に提供されています。なので、上記の setup.py にちょっと追加すれば、Windows についてはディストリビューションのビルドがかなりお手軽になります。

環境の構築
setup.py の書き方

PyQt アプリケーションのディレクトリ構成は以下のようにしました。

  • path/to/project: プロジェクトディレクトリ
    • dist: パッケージが保存されるディレクトリ (自動的に作成されます)
    • test: PyQt アプリケーションディレクトリ
      • media: アイコンファイルなどのリソースを保存するディレクトリ
        • test.ico: アイコンファイル
      • main.py: メインスクリプト
    • setup.py: パッケージング用スクリプト
    • README: アプリケーション概要を記載したファイル (Windows の場合は cp932 に保存)
    • LICENSE: ライセンスを記載したファイル (cp932)
#!/usr/bin/env python2.6
# -*- coding: utf-8 -*-

import os
import sys, subprocess
from distutils.core import setup

########################################
# コンパイルオプション設定             #
########################################

# バイナリ名
NAME = u"test"

# バージョン情報
VERSION = "1.0.0"

# 著作権
AUTHOR = "Kosei Kitahara"

# メール
EMAIL = "[email protected]"

# URL
u"""
AppID を生成するように用いられるため、アプリケーション毎にユニークにする
"""
URL = "http://example.com/test"

# パッケージ 概要
DESCRIPTION = u"テスト用アプリケーション"

# Python バイトコードの最適化オプション (0: None, 1: -O, 2: -OO)
OPTIMIZE = 2

# 圧縮オプション (0: 圧縮しない, 1: 圧縮する)
COMPRESSED = 1

# バンドルオプション (1: 単独, 3: 個別)
BUNDLE_FILES = 3

# 依存ライブラリの解決
INCLUDES = ["sip", "ctypes", ]
EXCLUDES = ["_ssl", "tcl", "tkinter", "Tkconstants", "Tkinter", ]
DLL_EXCLUDES = ["tcl84.dll", "tk84.dll", ]

# ディレクトリ, ファイルなど
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
APPLICATION_DIR = os.path.join(BASE_DIR, "test")
DIST_DIR = os.path.join(BASE_DIR, "dist")

MAIN_SCRIPT_NAME = os.path.join(APPLICATION_DIR, "main.py")
ICON_FILE_NAME = os.path.join(APPLICATION_DIR, "media", "test.ico")
LICENSE_FILE_NAME = os.path.join(BASE_DIR, "LICENSE")

########################################
# ユーティリティ関数                   #
########################################

def get_win32ui_files():
    u"""win32ui 依存 dll の取得"""

    import win32ui
    win32ui_dir = os.path.dirname(win32ui.__file__)
    return [os.path.join(win32ui_dir, i) for i in [
        "mfc90.dll", 
        "mfc90u.dll", 
        "mfcm90.dll", 
        "mfcm90u.dll", 
        "Microsoft.VC90.MFC.manifest", ]]
WIN32UI_MFCFILES = get_win32ui_files()

def get_pyqt4_imageformats_plugin_files():
    u"""PyQt4 の画像コーデック依存 dll の取得"""

    import PyQt4
    pyqt4_dir = os.path.dirname(PyQt4.__file__)
    return [os.path.join(pyqt4_dir, "plugins", "imageformats", i) for i in [
        "qgif4.dll",
        "qico4.dll",
        "qjpeg4.dll",
        "qmng4.dll",
        "qsvg4.dll",
        "qtiff4.dll", ]]
PYQT4_IMAGEFORMATS = get_pyqt4_imageformats_plugin_files()

########################################
# ビルドスクリプト                     #
########################################

# Linux, OS X は省略
if sys.platform == 'win32':
    u"""Windows 用のフリージング"""

    import py2exe, innosetup

    # py2exe, innosetup 共通ビルドオプション
    DATA_FILES = [
            ("Microsoft.VC90.MFC", WIN32UI_MFCFILES), 
            ("imageformats", PYQT4_IMAGEFORMATS), ]
    ICON_RESOURCES = [(1, ICON_FILE_NAME), ]
    PY2EXE_OPTIONS = {
            "includes": INCLUDES,
            "excludes": EXCLUDES,
            "dll_excludes": DLL_EXCLUDES,
            "compressed": COMPRESSED,
            "optimize": OPTIMIZE,
            "bundle_files": BUNDLE_FILES, }

    # innosetup ビルドオプション
    u"""
    'inno_script' は、.iss ファイル名を指定することもできますが、
    デフォルトでも用意されているので、大変便利です。
    """
    INNOSETUP_OPTIONS = {
            "inno_script": innosetup.DEFAULT_ISS, 
            "bundle_vcr": True, 
            "zip": False, }

    setup(
            name=NAME,
            version=VERSION,
            license=LICENSE_FILE_NAME,
            author=AUTHOR,
            author_email=EMAIL,
            description=DESCRIPTION,
            url=URL,
            data_files=DATA_FILES,
            options={
                    "py2exe" : PY2EXE_OPTIONS,
                    "innosetup": INNOSETUP_OPTIONS},
            windows=[{
                    "script" : MAIN_SCRIPT_NAME, 
                    "icon_resources": ICON_RESOURCES}], 
            zipfile="test.lib", )

py2exe の設定を継承してくれるので、大変便利ですね。

パッケージング処理の実行

以下のコマンドを実行すると、パッケージングしてくれます。

python setup.py innosetup

もちろん setup.py py2exeも動作します。innosetup の run メソッドは py2exe を継承しており、dist ディレクトリに作成するライブラリや実行ファイルは py2exe と同様です。それとは別に以下のファイルが作成されます。

  • Microsoft.VC90.CRT.manifest
  • distutils.iss: インストーラ作成用 ISS ファイル
  • test-1.0.0-setup.exe: インストーラファイル。ファイル名は settings.py で設定した [アプリケーション名]-[バージョン]-setup.exe になります。

実行すると Inno Setup Compiler が自動的に起動し、作成した distutils.iss ファイルを元にインストーラーが作成されます。途中でエラーが出たら、innosetup.py を修正し、出力する ISS ファイルを変更しましょうw

ちなみに、私の環境では以下を修正しました。

218c218
<             return win32api.LoadResource(handle, restype, name).decode('utf_8')
---
>             return win32api.LoadResource(handle, restype, name).decode('utf-8')
525c525
<             iss_metadata['MinVersion'] = '5.0,4.0'
---
>             iss_metadata['MinVersion'] = '0,5.0'
546c546
<             fp.write('%s=%s\n' % (k, iss_metadata[k], ))
---
>             fp.write(unicode('%s=%s\n' % (k, iss_metadata[k], )).encode("utf-8"))
779c779
<             fp.write('#define %s "%s"\n' % (k, consts[k], ))
---
>             fp.write(unicode('#define %s "%s"\n' % (k, consts[k], )).encode("utf-8"))

これでデスクトップアプリケーションの配布がかなり楽になりましたよ!ユニコードインストーラなどについてはもうちょっと調べる必要がありますね・・・。

追記 (2010-08-18)

試してないですが、bdist_nsi という NSS I 用の distutils 拡張モジュールが pypi に登録されていました。ソースを見ると、Python 3 にも対応しており (Python 2 は 3 へ変換)、py2exe は使ってないようですね。これはよさげ!

コメント

このブログの人気の投稿

Python から Win32 API 経由で印刷する

Disqus のスケール - Django 編

#PySpa アドベント (23 日目)