Windows10 + pyinstallerで、Djangoをexe化して配布可能にしてみた

最近、Windows10 + pyinstallerで、

をしました。

pyinstallerのWikiに方法が記載されていますが、いろいろとハマったため、メモを残します。
Recipe Executable From Django · pyinstaller/pyinstaller Wiki · GitHub

 
目次

 

環境

 
なお、最新のPythonは3.7.3です。

ただ、pyinstallerでexe化する際、Python3.7ではpyinstallerにパッチを当てる必要があります。

 
現時点では上記の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

 
仮想環境を有効化し、 djangopyinstaller を 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の下に、 builddist の2つができます。

今回の myprojct.exedist の中に入ります。

また、rootディレクトリの中に、 myproject.spec ファイルが自動生成されます。

これが今回exe化した時の設定内容になります。exe化する時の設定を変更する場合は、このファイルを修正します。

 
続いて、runserverすると、Congratulations!が表示されます。

(env36)>dist\myproject.exe runserver

f:id:thinkAmi:20190421182558j:plain:w450

 

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'

 
そこで、 myapphiddenimports に追加します。

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.spechiddenimports は以下の通りです。

hiddenimports=[
    'myapp.apps',
    'myapp.urls',
],

 
続いて、runserverして localhost:8000 にアクセスすると、Hello worldが表示されます。

(env36)>dist\myproject.exe runserver

  f:id:thinkAmi:20190421175503j:plain:w300

 

静的ファイル・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を赤字で表示します。

static_files/css/myapp.css

.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も動作しています。

f:id:thinkAmi:20190421190400p:plain

なお、 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

 
意図した通りに表示されました。

f:id:thinkAmi:20190421190340p:plain

 
また、 %USERPROFILE%\db.sqlite3 も作成されています。

>dir %USERPROFILE%\db*
...
2019/04/21  15:18           135,168 db.sqlite3

 
以上で、Djangoアプリをexe化できました。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_pyinstaller-sample