みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

Requestsをcx_freezeするときのポイント

Requestsという便利なモジュールを使ってちゃちゃっとPythonのソフトウェアが完成したのはいいが,いざcx_freezeでフリーズしようとすると以下のようなエラーが出てなんじゃこりゃ?ってなった時のメモ.

Traceback (most recent call last): 
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages/requests/packages/urllib3/util/ssl_.py", line 116, in ssl_wrap_socket 
    try: 
NotADirectoryError: [Errno 20] Not a directory

ディレクトリじゃないって言うのだから,パスがなんか違うのだろうと思って,ssl_.pyを開いて確認すると,以下のようなコードであった.

if SSLContext is not None:  # Python 3.2+ 
    def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, 
                        ca_certs=None, server_hostname=None, 
                        ssl_version=None): 
 
        if ca_certs: 
            try: 
                context.load_verify_locations(ca_certs) 

load_verify_locations(ca_certs) の引数が怪しそうなので,cx_freezeした後のこの値をチェックしてみたところ,

ca_certs: /.../build/exe.macosx-10.9-x86_64-3.3/library.zip/requests/cacert.pem

ってなってた.library.zipってディレクトリじゃないよねーと納得.

こことかこっちには,request.certにあれこれしてからrequest.getにverify=pathとすればいいよーみたいなことが書いてあるが,cx_freezeした後はこのオプションが何故か効かなくなることが判明したので,ssl_.pyのssl_wrap_socket()のca_certsの値を直前で修正する,という強行手段をとることにした.以下のようにreplace()でlibrary.zip/requestsの部分を切除する.

if SSLContext is not None:  # Python 3.2+ 
    def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, 
                        ca_certs=None, server_hostname=None, 
                        ssl_version=None): 
        """ 
        All arguments except `server_hostname` have the same meaning as for 
        :func:`ssl.wrap_socket` 
 
        :param server_hostname: 
            Hostname of the expected certificate 
        """ 
        context = SSLContext(ssl_version) 
        context.verify_mode = cert_reqs 
 
        # Disable TLS compression to migitate CRIME attack (issue #309) 
        OP_NO_COMPRESSION = 0x20000 
        context.options |= OP_NO_COMPRESSION 
        ca_certs = ca_certs.replace("library.zip/requests/","/") 
        print("ca_certs: {0}".format(ca_certs)) 
 
        if ca_certs: 
            try: 
                context.load_verify_locations(ca_certs)

この方法のよいところは,フリーズする前だとlibrary.zipなんていう文字列は存在するはずがないので,replaceはされず正常に動作し,フリーズした後はreplaceされるので正常に動作する点である.

あとはcacert.pemもフリーズされるように,以下のようにsetup.pyに追記する.

setup( 
    options = { 
        "build_exe": { 
            "include_files":[(requests.certs.where(),"cacert.pem")]

python setup.py buildした後に,buildしたディレクトリにcacert.pemが入っていることを確認して完了.

cx_freezeは素晴らしいけれども,スクリプトでない依存ファイルがあると何かと工夫する必要があるのが大変である.