SyntaxHighlighter

2018年11月24日土曜日

1バイナリの Python と exepy

1バイナリの Python と exepy

1バイナリにした Stackless Python の更新もそろそろ終わりなので、その中に含まれている exepy ライブラリについてまとめておこうと思います。

1バイナリの Python の仕組み

この Python は、 外部 DLL や *.py ファイルが不要になっています。
DLL については、ビルド時に tcl/tk, OpenSSL などを含め、静的リンクすることで解決しています。
*.py ファイルについては、 embeddedimport というモジュールを新規に開発することで対応しています。

これらについては Single Binary 版の Stackless Python 3.6.4Single Binary StacklessPython 3.6.4 L8 リリースに簡単にまとめてあります。

exepy の仕組み

2.0.0 以降の exepy では、以下のようにして exe を作ることができます。

1
python.exe -m exepy create sample.exe input.py

上記で作られた exe は、一時フォルダにファイルを展開したりすることなく、単一の exe ファイルだけで input.py の処理を実行することができます。

さて、このとき、 sample.exe は python.exe とほぼ同じバイナリになっています。
唯一の違いは、 input.py に相当する内容が sample.exe のリソースとして組み込まれているところです。

embeddedimport は、実行している exe ファイル (sample.exe) のリソースに特定の ID のリソースが含まれている場合、その内容から *.py のデータをデコードしロードします。
つまり、 import input が実行された場合、 embeddedimport は以下の順で *.py を探します。

  1. 現在実行中の exe ファイルのリソース内
  2. exe 内に静的に持っている *.py のデータ (具体的には embeddedimport_data.c の内容)
そのため、元々の python.exe にリソースを埋め込むだけで、 *.py を含んだ exe ファイルを作ることができます。

exepy のサンプル

いくつかサンプルを示しておきます。

複数ファイルからなるサンプル

main.py と sub.py があり、 main.py が sub.py を import しているとします。

1
2
3
4
5
6
# main.py
 
import sub
 
sub.func()
print("main")
1
2
3
4
# sub.py
 
def func():
    print("sub.func")

上記のファイルを下記のように exe 化します。

1
> python.exe -m exepy create sample.exe main.py sub.py

上記で作られた sample.exe の実行は、実質的に下のコマンドを実行したときとほぼ同等の動きになります。

1
2
3
4
> python.exe -m main
sub.func
main
>

画像ファイルなどの抱き込み

Single Binary StacklessPython 3.6.6 L16 に含まれる exepy 2.1.0 では、画像ファイルなどの抱き込みができるようになっています。
例えば、 picture.png を tkinter で表示する例を考えます。

以下のような main.py を用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py
 
import tkinter as tk
import os
 
root = tk.Tk()
 
path = os.path.join(os.path.dirname(__file__), "picture.png")
img_data = __loader__.get_data(path)
img = tk.PhotoImage(data=img_data)
 
label = tk.Label(image=img)
label.pack()
root.mainloop()

こちらを以下のコマンドで exe 化します。

1
> python.exe -m exepy create sample.exe main.py picture.png

これで picture.png も組み込まれた exe ができあがります。

__loader__.get_data(...) で、通常のファイルのようにデータを取得することができます。
あとはこれを用いて tkinter の Label, Widget などで表示すればよいです。

今回の変更で embeddedimporter が get_data をサポートしました。
これにより、任意のバイナリデータを組み込めるようになっています。

あとは、アイコンの変更機能ぐらいを追加して開発も終了です。

2018年6月26日火曜日

Single Binary StacklessPython 3.6.4 L8 リリース

Single Binary StacklessPython 3.6.4 L8 リリース

業務の効率化や Python インタプリタの勉強も兼ねて、 StacklessPython を 1 バイナリ化していましたが、バージョン L8 まできました。

GitHub のリリースページからダウンロードできます。

今回は、主に以下の変更をしました。

  • tkinter のサポート
  • EXE 化の実験的サポート
  • Windows Subsystem 版のサポート

tkinter のサポート

これまでに Python の標準ライブラリは、ほぼすべてサポートしていましたが、 tkinter はいくつかの理由でサポートしていませんでした。

tkinter をサポートし、1 バイナリで実行するには、 tcl/tk のライブラリを静的にリンクする必要があります。
また、 tcl/tk は、いくつかの *.tcl ファイルを必要とするので、こちらについても対応する必要があります。

静的リンクについては、 nmake -f makefile.vc core OPTS=static のように OPTS として static を指定すれば、静的リンク版をビルドできるので、こちらを使うことになります。

一方で、*.tcl ファイルの抱き込みについては、やや面倒です。

*.tcl ファイルの抱き込み

tcl では、Tcl_Filesystem という構造体で、複数のファイルシステムを持つことができます。
これは一言で言うと Python の path_hooks に相当するものです。
今回は embeddedFilesystem というファイルシステムを作成し、 embeddedfs:/tcl8.6/init.tcl のように embeddedfs:/ で始まる仮想的なパスが指定された場合に、バイナリの中に保持しているファイルの中身を返すようにしています。

詳しくは tclEmbeddedFilesystem.c を参照してください。

EXE 化の実験的サポート

せっかくなので、おまけの機能として EXE 化の機能を付けてみました。
なお、元々、標準ライブラリ外の機能は考慮していないので、py2exe や PyInstaller の代わりには使えません。あくまで、標準ライブラリだけを使って書けるスクリプトの EXE 化ができます。

例えば下記のように、 tk でウィンドウを作成し、クリックでカウントアップするだけのプログラムを EXE 化してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# coding: utf-8
 
# counter.py
 
import tkinter as tk
import tkinter.ttk as ttk
 
 
class Counter(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        var = tk.StringVar()
        var.set("0")
        label = ttk.Label(self, textvariable=var, anchor=tk.CENTER, font=("", 30))
        label.pack(expand=True, fill=tk.BOTH)
        label.bind("<Button-1>", self.increment)
        label.bind("<Button-3>", self.decrement)
        self.var = var
        self.count = 0
 
    def increment(self, event):
        self.count += 1
        self.var.set("%d" % self.count)
 
    def decrement(self, event):
        self.count -= 1
        self.var.set("%d" % self.count)
 
 
root = tk.Tk()
a = Counter(root)
a.pack(expand=True, fill=tk.BOTH)
root.mainloop()

今回の Single Binary StacklessPython では、上記のように tkinter を使うような場合も、実行することができます。

これを、単一の EXE にするには、下記のようにします。
ただし、これはまだ実験的な機能なので、将来は仕様を変更するかもしれません。

1
python.exe -m exepy sample.exe counter.py

上記を実行すると sample.exe が生成されます。

サイズは 14MB ぐらいになりますが、テンポラリディレクトリにファイルを展開したりはしないので、かなり高速に実行されます。

Windows Subsystem 版のサポート

今回から pythonw.exe のように、コンソール画面が表示されないバイナリを作っておきました。

こちらは、主に GUI を使うスクリプトを EXE 化する際に使うと便利です。

Python, tcl/tk の実装の勉強にもなり、 AppVeyor の利用の勉強にもなりましたが、すべての標準ライブラリを 1 バイナリに収めたので、そろそろ終了にする予定です。

2018年6月13日水曜日

Single Binary 版の Stackless Python 3.6.4

Single Binary 版の Stackless Python 3.6.4

Stackless Python が 3.6.4 対応されていたので、 1 バイナリ化した Stackless Python も対応してみました。

GitHub のページから、ダウンロードしてご利用ください。

1 バイナリ化した Stackless Python

以前に Stackless Python 2.7 を 1 バイナリ化しましたがその当時の変更を Python 3.6 に対応させることになります。
今回は、その際の対応と、そもそも 1 バイナリ化した Stackless Python はどういうものかについて、まとめようと思います。

1 バイナリ化した Stackless Python 作成の目的

業務の都合上、「種々の PC 実機上で簡単に Python を実行したい」という動機がありました。
それに加えて、 pausable unittest を実行したかったので、通常の Python ではなく Stackless Python を使うことにしました。

また、極力、通常の Python と比べて、実行速度が遅くならないようにしたいということも考えていました。
前に Ruby を OCRA で固めたときは、実行前の展開にやや時間がかかるのがストレスだったので、今回はそういったことがないようにしたいと思っていました。

1 バイナリ化した Stackless Python の変更点

デフォルトの Stackless Python から以下の変更を加えました。

  • プログラム引数の変更
  • embeddedimport の実装
  • 3rd party ライブラリの同梱
  • その他の小変更

プログラム引数の変更

1 バイナリ化した Stackless Python は、引数の意味を二つだけ変更してあります。

  • -B: この引数は意味が逆になっており、-B が存在しないときに、 *.pyc ファイルを作成しないようになっています。
  • -E: この引数も意味が逆になっており、-E が存在しないときに、環境変数を参照しないようになっています。

これらの変更は、いずれも未知の環境で動作しやすいようにしておくためです。
時刻が正しくない複数の実機で実行した場合や、他に Python の処理系がインストールされていた場合にも、正しく動作させるためです。

embeddedimport の実装

通常、 Python の import 文は、 sys.path に登録されたパスを順に探し、その中から外部モジュールをロードします。
また、 sys.path には python.zipも 登録されており、 ZIP ファイルの中もサーチの対象になっています。
この ZIP ファイルからのロードは、 zipimport というモジュールが担っています。

今回は、「一つのバイナリにしたい」という目標があったので、 zipimport を参考にして、 embeddedimport を作り、実装しています。

挙動の概要としては、以下のようになります。

  1. ビルド時に、 Lib 以下の *.py ファイルをすべて C 言語の文字列に変換して、 python のバイナリに組み込んでおきます。
  2. 実行時には、 sys.pathC:/path_to/python.exe のように python.exe のフルパスを登録しておきます。また、 sys.path_hooksembeddedimporter モジュールを登録しておきます。
  3. これにより、 import pickle などが処理される際に embeddedimporter によってモジュールのインポートが試みられます。
  4. embeddedimporter では、自身が保持している *.py ファイルの中から指定されたファイルを探し、それを実行します。

3rd party ライブラリの同梱

快適に使えるように、いくつかのライブラリを同梱してあります。

  • comtypes
  • pycodestyle
  • pyflakes
  • pyreadline
  • PyYAML with libyaml

PyYAML に関しては、 libyaml をビルドしておき、 C 言語のサポートがあった方がずっと速いので、そのようにしてみました。

一点、注意としては、 libyaml とのブリッジライブラリである _yaml は、 Cython 形式になっており、 C 言語としてビルドするには前処理が必要でした。
Stackless Python の場合、 Fast Calll 時の関数呼び出しの処理が変更されているので、 CYTHON_FAST_PYCALL を 0 に設定し、 _yaml から Fast Call させないようにすることが必要でした。

その他の小変更

プログラムの引数を変更した関係で、いくつかのテストを変更しました。 -E を使っているテストは非常に多かったので、その辺りを中心に変更しました。

また、 C 言語で書かれた拡張ライブラリ *.pyd を、静的に Python にリンクするために、ビルド方法や config.c でのビルトインモジュールの定義部分を変更しました。

私の業務の都合上、 XML の属性をソートしたくなかったので、 xml.etree.ElementTree の出力部分に少し手を加えました。
具体的には ElementTree.writesort_attrib=True というキーワード引数を追加し、ソートを制御できるようにしておきました。

これらの対応を行い、テストがほぼ通ったので、ついにリリースになりました。
Windpws PE 上での作業や、多数のテスト実機上で Python を走らせる必要がある場合などに、ご利用いただければと思います。

2018年3月8日木曜日

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

Windows 上で一時的に Python を使って作業する際に使っているバッチファイル

私は、仕事でクリーンな (OS 展開直後の) Windows 実機や Windows PE 上で、Python を用いて作業をすることがしばしばあります。
今回は、その際によく使っているスクリプトを備忘録としてまとめておきます。

いずれも、 Python の -x オプションを使って、バッチファイル先頭行の goto 文をスキップしています。

1バイナリの Python

前提として、1バイナリ化した Python を使います。

以前に書いたように、ほぼすべての標準ライブラリを含んだ StacklessPython を、単一の EXE ファイルをコピーするだけで動作するようにしてあります。
こういった用途のために作ったので、インストールが不要かつコピーするだけで使えるようにしました。

管理者権限で実行する Python スクリプト

下記のようなバッチファイルを用意し、print("hoge") 部分を好きな Python スクリプトに変更すれば、ダブルクリックするだけで、管理者権限の Python スクリプトとして実行することができます。
なお、同じディレクトリに python.exe が必要です。

@goto BEGINNING_OF_CMD
# -*- coding: UTF-8 -*-
"""
:BEGINNING_OF_CMD
@echo off
cd /d "%~dp0"
set PYTHON=python.exe
%PYTHON% -c "import ctypes, sys, ctypes.wintypes; ctypes.windll.shell32.IsUserAnAdmin.restype = ctypes.wintypes.BOOL; sys.exit(ctypes.windll.shell32.IsUserAnAdmin())"
if ERRORLEVEL 2 goto :ERROR
if ERRORLEVEL 1 goto :ADMIN
goto :NON_ADMIN
:ADMIN
%PYTHON% -x "%~nx0"
exit /b %ERRORLEVEL%
:NON_ADMIN
powershell Start-Process "%~0" -Verb runas
exit /b 0
:ERROR
echo IsUserAnAdmin failed.
exit /b 1
"""
print("hoge")

readline を有効にした状態で Python インタプリタを立ち上げるスクリプト

こちらも、下記のようなバッチファイルを用意し、ダブルクリックすれば、 Python のインタプリタが立ち上がります。
pyreadline を使っているので、 Ctrl-f などのキーバインドや、補完、ヒストリ保存もできます。

@goto RUN_PYTHON
# -*- coding: UTF-8 -*-
"""
:RUN_PYTHON
@echo off
cd /d "%~dp0"
python -x -i "%~nx0"
exit /b %ERRORLEVEL%
"""
# from __future__ import print_function, unicode_literals, absolute_import
def main():
import os.path
_history_file_name = os.path.join(os.path.abspath(os.path.dirname(__file__)), "history.txt")
_history_length = 2048
def at_exit_callback():
import readline
try:
readline.write_history_file(_history_file_name)
except IOError:
print("Failed to save history in %s." % _history_file_name)
try:
import pyreadline.rlmain
#pyreadline.rlmain.config_path=r"c:\xxx\pyreadlineconfig.ini"
import readline, atexit
import pyreadline.unicode_helper
#
#
#Normally the codepage for pyreadline is set to be sys.stdout.encoding
#if you need to change this uncomment the following line
#pyreadline.unicode_helper.pyreadline_codepage="utf8"
except ImportError:
print("Module readline not available.")
else:
#import tab completion functionality
import rlcompleter
#Override completer from rlcompleter to disable automatic ( on callable
completer_obj = rlcompleter.Completer()
def nop(val, word):
return word
completer_obj._callable_postfix = nop
readline.set_completer(completer_obj.complete)
#activate tab completion
readline.parse_and_bind("tab: complete")
readline.set_history_length(_history_length)
readline.read_history_file(_history_file_name)
atexit.register(at_exit_callback)
import sys
print("Python " + sys.version + " on " + sys.platform)
main()
del main
view raw interactive.cmd hosted with ❤ by GitHub

作業の内容によっては使えると思いますので、置いておきます。