最近、Windows10 + pyinstallerで、
をしました。
pyinstallerのWikiに方法が記載されていますが、いろいろとハマったため、メモを残します。
Recipe Executable From Django · pyinstaller/pyinstaller Wiki · GitHub
目次
- 環境
- Congratulations!を表示するDjangoアプリのexe化
- HttpResposeを返すDjangoアプリをexe化
- 静的ファイル・Model・TemplateViewを使うDjangoアプリのexe化
- 一時ディレクトリ外のSQLiteを使用するDjangoアプリをexe化
- ソースコード
環境
なお、最新のPythonは3.7.3です。
ただ、pyinstallerでexe化する際、Python3.7ではpyinstallerにパッチを当てる必要があります。
- 参考
- Pythonメモ-97 (python 3.7 + pyinstaller 3.4 + venv で TypeError が出る件)(expected str, bytes or os.PathLike object, not NoneType) - いろいろ備忘録日記
- depend: Allow pylib detection when not a dependency of python.exe by Loran425 · Pull Request #3956 · pyinstaller/pyinstaller
- depend: Allow pylib detection when not a dependency of python.exe · Loran425/pyinstaller@14b6e65
現時点では上記のPRがマージされていないため、今回はPython3.6.8を使いました。
Congratulations!を表示するDjangoアプリのexe化
まずは、Congratulations!を表示するDjangoアプリをexe化します。
Python3.6.8をダウンロードします。
Python Release Python 3.6.8 | Python.org
次に、Windowsのpyランチャーを使い、仮想環境を作成します。
Pythonの実行方法 - python.jp
# pyランチャーで仮想環境作成 >py -3.6 -m venv env36
仮想環境を有効化し、 django
と pyinstaller
を pip install します。
# 仮想環境を有効化 >env36\Scripts\activate # インストール (env36)>pip install django pyinstaller
Djangoプロジェクトを作成します。
(env36)>django-admin startproject myproject
開発サーバを起動し、Congratulations! が表示されることを確認します。
(env36)>python myprojct\manage.py runserver
続いて、pyinstallerを使い、exe化します。
現在のディレクトリ構成です。
(root) +---env36 +---myproject | +---myapp | +---myproject
上記の (root)
ディレクトリでpyinstallerを実行します。
exe化では、manage.pyを実行できるように myproject/manage.py
を指定します。
また、出力はexeファイル1つにしたいため、オプション onefile
(もしくは F
) を指定します。
他に、exeファイルの名前は name
オプションで指定します。今回は myproject.exe
です。
(env36)>pyinstaller --name=myproject myproject/manage.py --onefile
実行すると、rootの下に、 build
と dist
の2つができます。
今回の myprojct.exe
は dist
の中に入ります。
また、rootディレクトリの中に、 myproject.spec
ファイルが自動生成されます。
これが今回exe化した時の設定内容になります。exe化する時の設定を変更する場合は、このファイルを修正します。
続いて、runserverすると、Congratulations!が表示されます。
(env36)>dist\myproject.exe runserver
HttpResposeを返すDjangoアプリをexe化
続いて、自作のDjangoアプリをexe化してみます。
まずはHttpResponseを返すViewのDjangoアプリを作成します。
(env36)>myproject\manage.py startapp myapp
続いて、HttpResponseを返すDjangoアプリを作成します。
settings.py
INSTALLED_APPS = [ 'myapp.apps.MyappConfig', # 追加 'django.contrib.admin', ... ]
myapp.views.py
from django.http import HttpResponse def hello(request): return HttpResponse('Hello, world')
myproject/urls.py_
from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('myapp.urls')), # 追加 ]
myapp/urls.py
from django.urls import path from myapp import views urlpatterns = [ path('', views.hello, name='hello'), ]
続いて、pyinstallerを実行します。
Congratulations!のときと異なり、自動生成された myprojct.spec
ファイルを使ってexeを作成します。
なお、 myproject/manage.py
を指定すると、上記で変更した myproject.spec
の内容が修正されてしまうことに注意します。
(env36)>pyinstaller myproject.spec --onefile
ただ、現時点では runserver するといくつかエラーが出ます。
対応方法としては、
- exe化する
- runserverして、エラーを出す
- myproject.specで
hiddenimports
を修正する
を繰り返すしか方法がないようです。
Pyinstaller によるPython 3.6スクリプトのexeファイル化 - Qiita
そのため、今回対応した内容を残しておきます。
初回の runserver のエラーです。
(env36)>dist\myproject.exe runserver Watching for file changes with StatReloader Exception in thread Thread-1: Traceback (most recent call last): ... File "lib\site-packages\django\apps\registry.py", line 91, in populate File "lib\site-packages\django\apps\config.py", line 116, in create File "importlib\__init__.py", line 126, in import_module File "<frozen importlib._bootstrap>", line 994, in _gcd_import File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 941, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed File "<frozen importlib._bootstrap>", line 994, in _gcd_import File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked ModuleNotFoundError: No module named 'myapp'
そこで、 myapp
を hiddenimports
に追加します。
hiddenimports=[
'myapp',
],
2回目は、 myapp.apps
でエラーになったため、 myapp.apps
を追加します。
(env36)>dist\myproject.exe runserver Watching for file changes with StatReloader Exception in thread Thread-1: Traceback (most recent call last): ... File "lib\site-packages\django\apps\registry.py", line 91, in populate File "lib\site-packages\django\apps\config.py", line 116, in create File "importlib\__init__.py", line 126, in import_module File "<frozen importlib._bootstrap>", line 994, in _gcd_import File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked ModuleNotFoundError: No module named 'myapp.apps'
3回目は、 myapp.urls
のエラーです。これも追加します。
(env36)>dist\myproject.exe runserver Watching for file changes with StatReloader Performing system checks... Traceback (most recent call last): ... File "myproject\urls.py", line 21, in <module> File "lib\site-packages\django\urls\conf.py", line 34, in include File "importlib\__init__.py", line 126, in import_module File "<frozen importlib._bootstrap>", line 994, in _gcd_import File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked ModuleNotFoundError: No module named 'myapp.urls' [2244] Failed to execute script manage
ここまで対応すると
(env36)>pyinstaller myproject.spec --onefile 73962 INFO: Appending archive to EXE path\to\django_pyinstaller_sample\dist\myproject.exe 73993 INFO: Building EXE from EXE-00.toc completed successfully.
と、exe化が成功します。
なお、この時の myprojct.spec
の hiddenimports
は以下の通りです。
hiddenimports=[ 'myapp.apps', 'myapp.urls', ],
続いて、runserverして localhost:8000
にアクセスすると、Hello worldが表示されます。
(env36)>dist\myproject.exe runserver
静的ファイル・Model・TemplateViewを使うDjangoアプリのexe化
次に、本格的なDjangoアプリで使われる
- 静的ファイル
- Model
- TemplateView
を使うDjangoアプリをexe化してみます。
まずはModelを作ります。
myapp/models.py
from django.db import models class Fruit(models.Model): name = models.CharField('名前', max_length=50) def __str__(self): return self.name
また、Modelの初期データを投入するfixtureを追加します。
myapp/fixtures/initial_data.json
[ { "model": "myapp.Fruit", "pk": 1, "fields": { "name": "りんご" } }, { "model": "myapp.Fruit", "pk": 2, "fields": { "name": "みかん" } } ]
ViewであるTemplateViewには、exe化した時の環境を確認するため、
- Pythonのバージョン
- settings.BASE_DIR
を表示してみます。
myapp/views.py
from django.views.generic import TemplateView from django.conf import settings import platform from myapp.models import Fruit class FruitTemplateView(TemplateView): template_name = 'myapp/fruit.html' extra_context = { 'base_dir': settings.BASE_DIR, 'python_version': platform.python_version(), 'fruits': Fruit.objects.all(), }
templates/myapp/fruit.html
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> <link rel="stylesheet" type="text/css" href="{% static 'css/myapp.css' %}"> </head> <body> <p class="version">Python version: {{ python_version }}</p> <p>BASE_DIR: {{ base_dir }}</p> <div> Fruit Model: <ul> {% for fruit in fruits %} <li>{{ fruit }}</li> {% endfor %} </ul> </div> <button id="show">say</button> <script src="{% static "js/myapp.js" %}"></script> </body> </html>
versionを赤字で表示します。
.version { color: red; }
ボタンをクリックしたときにalertを出すJavaScriptを作成します。
static_files/js/myapp.js
const btn = document.getElementById('show'); btn.addEventListener('click', () => { alert('hello'); });
プロジェクトのURLディスパッチャに、静的ファイル分を追加します。
myproject/urls.py
from django.contrib import admin from django.urls import path, include from django.contrib.staticfiles.urls import staticfiles_urlpatterns # 追加 urlpatterns = [ path('admin/', admin.site.urls), path('', include('myapp.urls')), ] urlpatterns += staticfiles_urlpatterns() # 追加
アプリのURLディスパッチャには、今回作成したViewを追加します。
myapp.urls.py
urlpatterns = [ path('', views.hello, name='hello'), path('fruit', views.FruitTemplateView.as_view(), name='fruit'), # 追加 ]
settins.pyには、静的ファイルの設定を追加します。
Pyinstaller compiles but 404 error on Django Static javascript files - Stack Overflow
myproject/settings.py
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 変更 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ # 追加 STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static_files'), )
あとはマイグレーション & fixtureによる初期データ投入を行います。
# マイグレーションファイルを作成する (env36)>python myproject\manage.py makemigrations # マイグレーション (env36)>python myproject\manage.py migrate # データ投入 (env36)>python myproject\manage.py loaddata initial_data Installed 2 object(s) from 1 fixture(s)
ここまででDjangoアプリが作成できました。
続いてexe化の設定ファイルを修正します。
datas
に、テンプレートと静的ファイルのパスをタプルで指定します。
myproject.spec
datas=[ # (<myproject.specのディレクトリから見た各パス>, <exe化後に、Djangoアプリのrootから見た各パス>) ('myproject/templates', 'templates'), # テンプレート ('myproject/static_files', 'static_files'), # 静的ファイル ],
設定も終わったため、exe化と runserverします。
# exe化 (env36)>pyinstaller myproject.spec --onefile # runserver (env36)>dist\myproject.exe runserver
localhost:8000/fruit
にアクセスすると
- Pythonのバージョン
- settings.BASE_DIR
- Modelの内容
が表示されました。また、JavaScriptも動作しています。
なお、 settings.BASE_DIR
は一時ディレクトリのため、起動ごとに変更されることに注意します。
一時ディレクトリ外のSQLiteを使用するDjangoアプリをexe化
ここまでの方法では、SQLiteが一時ディレクトリにあるため、runserverするたびに初期化されてしまいます。
そのため、 %USERPROFILE%
直下にSQLiteを移動して使うよう変更します。
%USERPROFILE% env variable for python - Stack Overflow
myproject/settings.py
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(os.environ['USERPROFILE'], 'db.sqlite3'), } }
また、fixtureを適用できるよう、exe化の設定ファイルを修正します。
myproject.spec
datas=[ ('myproject/templates', 'templates'), ('myproject/static_files', 'static_files'), ('myproject/myapp/fixtures', 'myapp/fixtures'), # 追加 ],
他に、現在のSQLiteとは別のものを使っていることを確認するため、fixtureに追加します。
myproject/myapp/fixtures/initial_data.json
{ "model": "myapp.Fruit", "pk": 3, "fields": { "name": "ぶどう" } }
準備ができたので、exe化とlodadataとrunserverをして結果を確認します。
# exe化 (env36)>pyinstaller myproject.spec --onefile # loaddata (env36)>dist\myproject.exe loaddata initial_data Installed 3 object(s) from 1 fixture(s) # runserver (env36)>dist\myproject.exe runserver
意図した通りに表示されました。
また、 %USERPROFILE%\db.sqlite3
も作成されています。
>dir %USERPROFILE%\db* ... 2019/04/21 15:18 135,168 db.sqlite3
以上で、Djangoアプリをexe化できました。