PyQt でクロスプラットフォームなデスクトップアプリケーションを
ここ何ヶ月かデスクトップアプリケーションにどっぷりな感じです。パッケージングをもっと簡単にしたい!ということで色々と試行錯誤しておりました。linux, mac はいい感じですが、Windows は・・・ py2exe でフリージングのみしかしていませんでした。配布とインストールは自動解凍書庫、アップデート、アンインストールは・・・。そこで今回 (やっと) 覚えたのが Inno Setup や WiX といった Windows 用のパッケージビルダです。備忘録がてら、Python でのパッケージングをまとめてみました。
パッケージングについて
大きく 2 つのフェーズに分かれています。
- フリージング: Python バンドルや他の必要なライブラリーを寄せ集め、実行可能形式にまとめます。
Windows と OS X については以下のライブラリでフリージングします。
Linux を含むクロスプラットフォームなフリージングができる cx_Freeze というのもあります。 - ディストリビューションのビルド
-
- Windows: Microsoft Windows Installer はアップグレード、アンインストール、トランザクション処理を使った複数パッケージの管理とかの便利機能を持っています。
- 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 についてはディストリビューションのビルドがかなりお手軽になります。
環境の構築
- Visual C++ (Windows Platform SDK): py2exe にも必要
- Inno Setup QuickStart Pack: IStools Script Editor 等のサードパーティアドオンなどが含まれます
- innosetup Pythono ライブラリ: Inno Setup 用ライブラリ
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 = "mail@example.com" # 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 は使ってないようですね。これはよさげ!
コメント
コメントを投稿