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 は使ってないようですね。これはよさげ!
コメント
コメントを投稿