2025-10-29 2025-11-02

概要

前回(PythonのWebアプリのデスクトップアプリ化)、pywebview と pyinstaller を使用して Webアプリ(streamlit)をWindows用のGUIアプリとしてビルドした。
・・が、exeサイズが大きい場合、しばらくは何も表示されない(Loading画面が表示されるまでの時間が長すぎる)為(※1)、これを改善(※2)する為の改修を行う。
※1 ... 一時ディレクトリへの展開や、アンチマルウェア等によるファイルチェックに時間がかかる。
※2 ... exeをダブルクリックしてから何も表示されない時間を出来るだけ短くする。

今回は、最初のLoading画面を出来るだけ早く表示出来るように改修を行う。

目次

改修内容

webview用の exe と webアプリ(streamlit)の exe を分割する事により、
最初に起動する画面(webview)のexeサイズを小さくして、最初のLoading画面が出来るだけ早く表示されるようにする。

webview側のexeで行う事は以下の通り。

  • 「起動中」ページを表示する
  • 起動するサーバexeが信頼出来るものかどうかチェックする
    ※ exeにコードサイニング証明書が付与されている事が前提(関連: ビルドしたexeへのデジタル署名
  • サーバ(exe)を別プロセスとして起動する
  • サーバの起動が確認できたタイミングで対象のURL(※)をWebviewに表示する
    ※ デフォルト: http://localhost:8501
    ※ ただしポート番号はstreamlit設定ファイル内容から自動判定
exe_image.png

ファイル構成

前回作成した myapp.py を myapp_webview と myapp_server の 2つに分割する事以外は前回と同じ。
ただし、メインとなるexe(前回)から別のexe(myapp_server)を起動する際に証明書をチェックする処理を追加。

+ myapp_webview.py    ... 画面(pywebview)
+ myapp_server.py     ... Webアプリ(Streamlit)
+ check_sign.py       ... 共通処理(コードサイニング証明書関連)
+ common.py           ... 共通処理
+ pages               ... streamlit の各ページ
    + top.py
    + xxxxxx.py
    + xxxxxx.py
+ .streamlit
    + config.toml             ... streamlit 設定ファイル
+ .github
    + workflows
        + build-windows.yml   ... github actions ワークフロー
+ requirements.txt            ... pythonの依存ライブラリを記載

webview と アプリの分離

前回作成した myapp.py を myapp_webview と myapp_server の 2つに分割する。

streamlit==1.50.0
pywebview==6.0

plotly
pyvista[all]
stpyvista
pydeck

scipy
rfc3987

pyinstaller

# 署名検証用
pefile
cryptography
myapp_webview.py
import multiprocessing
import os
import queue
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import threading
import time
import traceback

from common import check_edge_installed
from common import get_initial_content
from common import init_platform
from common import install_webview2
from common import load_server_env
from common import get_server_port
from common import print_log


# アプリケーション名
APP_NAME = "Sample App"

# アプリケーションサーバ実行ファイル名(拡張子は自動補完)
APP_SERVER_EXE = "myapp_server"

# streamlitの設定ファイル
APP_CONFIG_FILE = os.path.join(".streamlit", "config.toml")

# 許可するサーバー実行ファイルの証明書サムプリント(空の時はチェックしない)
# TODO: 外部化
ALLOWED_THUMB_PRINTS = "81CD829A58A13480C5......"


class MyParameters():
    def __init__(self):
        self.server_proc = None
        self.child_pid_value = None
        self.log_thread = None
        self.stop_event = None
        self.server_port = None
        self.server_exe_path = None
        self.server_tmp_base = None
        self.server_tmp_dir = None

    def set_value(self, server_proc, child_pid_value, log_thread, stop_event):
        self.server_proc = server_proc
        self.child_pid_value = child_pid_value
        self.log_thread = log_thread
        self.stop_event = stop_event


def start_server_process(app_path, log_queue, pid_value, server_tmp_base, errs, force):
    """アプリケーションサーバを起動する."""

    try:
        if not app_path:
            raise ValueError("app_path is empty")

        exe_path = os.path.abspath(app_path)
        if not os.path.exists(exe_path):
            raise FileNotFoundError(f"Executable not found: {exe_path}")

        # 証明書チェック
        if not force:
            is_certified = check_server_certificate(app_path)
            if not is_certified:
                print_log("[ERROR] Server certificate check failed.")
                errs["CERT_ERROR"] = "証明書エラー"
                print_log("cert_error(p): " + errs["CERT_ERROR"])
                return
                # raise RuntimeError("Server certificate check failed.")

        pid_value.value = os.getpid()

        if sys.platform == "win32":
            # 子プロセスの展開先ベースを指定(PyInstaller は TEMP/TMP 配下に _MEIxxxxx を作る)
            env = os.environ.copy()
            if server_tmp_base:
                try:
                    os.makedirs(server_tmp_base, exist_ok=True)
                except Exception:
                    pass
                env["TEMP"] = server_tmp_base
                env["TMP"] = server_tmp_base
                env["TMPDIR"] = server_tmp_base
            popen_kwargs = {
                "stdout": subprocess.PIPE,
                "stderr": subprocess.STDOUT,
                "encoding": "utf-8",                                         # 標準入出力のエンコーディング
                "errors": "namereplace",                                     # 標準入出力の文字化け部分はUnicode名に変換
                "creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),  # コンソールなしで実行(環境によっては未定義の可能性がある為、getattr使用)
                "universal_newlines": True,                                   # 標準入出力は文字列
                "env": env,
            }

            print_log(f"[INFO] start server process: {exe_path}")
            proc = subprocess.Popen([exe_path], **popen_kwargs)
            pid_value.value = proc.pid

            try:
                if proc.stdout is not None:
                    for line in iter(proc.stdout.readline, ""):
                        if not line:
                            break
                        log_queue.put(line)
            finally:
                if proc.stdout is not None:
                    proc.stdout.close()
                proc.wait()
        else:
            # Unix系: TMPDIR を指定して execv
            if server_tmp_base:
                try:
                    os.makedirs(server_tmp_base, exist_ok=True)
                except Exception:
                    pass
                os.environ["TEMP"] = server_tmp_base
                os.environ["TMP"] = server_tmp_base
                os.environ["TMPDIR"] = server_tmp_base
            os.execv(exe_path, [exe_path])

    except SystemExit:
        pass
    except Exception:
        log_queue.put("[STREAMLIT ERROR] Failed to launch executable:\\n" + traceback.format_exc())


def check_server_certificate(exe_path: str) -> bool:
    """サーバー実行ファイルの署名をチェックする."""
    if sys.platform != "win32":
        # Windows以外はチェックしない
        return True

    if not ALLOWED_THUMB_PRINTS:
        # 許可する証明書サムプリントが空の時はチェックしない
        print_log("[INFO] SKIP check certificate")
        return True

    try:
        from check_sign_win import get_exe_thumbprint
    except ImportError:
        print_log("[WARN] Unable to import check_sign module; skipping certificate check.")
        return False

    try:
        print_log("[INFO] START check certificate")
        thumbprint = get_exe_thumbprint(exe_path)
        print_log(f"[INFO] Executable thumbprint: {thumbprint}")
        if thumbprint.upper() == ALLOWED_THUMB_PRINTS.upper():
            print_log("[INFO] Server certificate OK.")
            return True
        else:
            print_log("[ERROR] Server certificate thumbprint does not match expected value.")
            return False
    except Exception as e:
        print_log(f"[ERROR] Failed to verify server certificate: {e}")
        return False
    finally:
        print_log("[INFO] END check certificate")


def start_app_server(params, errs, force=False):
    """Streamlitサーバ起動."""
    child_pid_value = multiprocessing.Value("i", -1)
    stop_event = threading.Event()
    server_proc = multiprocessing.Process(
        target=start_server_process,
        args=(params.server_exe_path, params.log_queue, child_pid_value, params.server_tmp_dir, errs, force),
    )
    server_proc.start()

    def print_logs(q):
        while True:
            if stop_event.is_set():
                break
            try:
                msg = q.get(timeout=0.5)
            except queue.Empty:
                if stop_event.is_set() or not server_proc.is_alive():
                    break
                continue
            except Exception:
                if stop_event.is_set() or not server_proc.is_alive():
                    break
                continue

            if msg:
                print_log(f"[STREAMLIT] {msg}", end="")

    log_thread = threading.Thread(target=print_logs, args=(params.log_queue,), daemon=False)
    log_thread.start()

    params.set_value(server_proc, child_pid_value, log_thread, stop_event)


def wait_server_starting(window, params, force=False):
    """アプリケーションサーバが起動するまで待機する."""
    print_log("[INFO] Waiting for app server port...")

    # プロセス跨ぎでエラー情報を共有する為、Manager を使用
    with multiprocessing.Manager() as manager:

        # サーバ起動
        errs = manager.dict()
        start_app_server(params, errs, force)

        def force_start(e):
            """証明書エラー時の強制起動ボタン押下処理."""
            print_log("[INFO] clicked force start")
            window.load_html(get_initial_content())
            wait_server_starting(window, params, force=True)

        def wait_for_port(host, port, timeout=60):
            """指定ポートが開くまで待機する."""
            start = time.time()
            while time.time() - start < timeout:
                if "CERT_ERROR" in errs:
                    print_log("cert_error(w): " + errs["CERT_ERROR"])
                    window.load_html("<h1 style='color: red'>アプリケーションの起動に失敗しました</h1><div><span>内部サーバ証明書が不正です。このまま起動しますか?</span><button id='force_start'>起動する</button></div>")
                    button = window.dom.get_element('#force_start')
                    button.events.click += force_start
                    return False
                try:
                    with socket.create_connection((host, port), timeout=1):
                        return True
                except OSError:
                    time.sleep(0.5)
            return False

        if wait_for_port("127.0.0.1", params.server_port, timeout=120):
            print_log("[INFO] app server is ready.")
            window.load_url(f"http://localhost:{params.server_port}")
        else:
            if "CERT_ERROR" not in errs:
                server_proc = params.server_proc
                errors = ["[ERROR] app server did not start within timeout."]
                if server_proc is not None and server_proc.exitcode is not None:
                    errors.append(f"[ERROR] Streamlit process exit code: {server_proc.exitcode}")
                while params.log_queue is not None and not params.log_queue.empty():
                    errors.append(f"[STREAMLIT] {params.log_queue.get()}")
                for err in errors:
                    print_log(err)
                window.load_html("<h1 style='color: red'>アプリケーションの起動に失敗しました</h1><pre>" + "\r\n".join(errors) + "</pre>")


def get_server_exe_path(filename):
    """実行ファイルのパスを取得する"""
    ext = ".exe" if sys.platform == "win32" else ""
    candidates = []
    try:
        # この exe と同じディレクトリにある場合
        exec_dir = os.path.dirname(sys.executable)
        candidates.append(os.path.join(exec_dir, f"{filename}{ext}"))
    except Exception:
        pass
    # ./dist/{filename}[.exe] にある場合(開発環境)
    candidates.append(os.path.abspath(os.path.join("dist", f"{filename}{ext}")))
    # ./{filename}[.exe] にある場合(開発環境)
    candidates.append(os.path.abspath(f"{filename}{ext}"))

    exe_path = ""
    for path in candidates:
        if os.path.exists(path) and os.access(path, os.X_OK):
            exe_path = path
            print_log(f"[INFO] found server exe: {path}")
            break
        else:
            print_log(f"[INFO] Checked and not found: {path}")

    return exe_path


def terminate_server_process(params, timeout=5):
    """サーバプロセスを停止する."""
    print_log("[INFO] Shutting down Streamlit...")
    log_queue = params.log_queue
    proc = params.server_proc
    child_pid_value = params.child_pid_value
    log_thread = params.log_thread
    stop_event = params.stop_event

    if stop_event is not None:
        stop_event.set()

    if log_queue is not None:
        try:
            log_queue.put_nowait("")
        except Exception:
            pass

    launcher_pid = getattr(proc, "pid", None)
    child_pid = -1
    if child_pid_value is not None:
        try:
            child_pid = child_pid_value.value
        except Exception as exc:
            print_log(f"[WARN] Unable to read child process pid: {exc}")

    if proc is not None:
        try:
            proc.terminate()
            proc.join(timeout)
        except Exception as exc:
            print_log(f"[WARN] Failed to terminate Streamlit launcher gracefully: {exc}")

        if proc.is_alive():
            print_log("[WARN] Streamlit launcher still running; forcing termination.")
            if sys.platform == "win32":
                try:
                    subprocess.run(
                        ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
                        check=False,
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL,
                    )
                except Exception as exc:
                    print(f"[ERROR] taskkill failed: {exc}")
            else:
                try:
                    proc.kill()
                except Exception as exc:
                    print(f"[ERROR] Failed to force kill Streamlit launcher: {exc}")
            try:
                proc.join(timeout)
            except Exception:
                pass

    target_pid = child_pid if child_pid > 0 else launcher_pid
    if target_pid and (launcher_pid is None or target_pid != launcher_pid or (proc is not None and not proc.is_alive())):
        if sys.platform == "win32":
            try:
                subprocess.run(
                    ["taskkill", "/PID", str(target_pid), "/T", "/F"],
                    check=False,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
            except Exception as exc:
                print_log(f"[ERROR] taskkill (child) failed: {exc}")
        else:
            try:
                os.kill(target_pid, signal.SIGTERM)
            except ProcessLookupError:
                pass
            except Exception as exc:
                print(f"[ERROR] Failed to signal Streamlit process: {exc}")

    if log_thread is not None and log_thread.is_alive():
        try:
            log_thread.join(timeout)
        except Exception:
            pass

    if log_queue is not None:
        try:
            log_queue.cancel_join_thread()
        except Exception:
            pass
        try:
            log_queue.close()
        except Exception:
            pass

    if proc is not None and proc.is_alive():
        print_log(f"[ERROR] Streamlit launcher is still alive (pid={proc.pid}).")

    try:
        if proc is not None:
            proc.close()
    except Exception:
        pass


def cleanup_server_tmp(params):
    """サーバー用一時ディレクトリを削除する."""

    parent_dir = params.server_tmp_base
    tmp_dir = params.server_tmp_dir

    try:
        print_log(f"[INFO] Cleaning up server temp directory: {tmp_dir}, (_MEIPASS parent: {parent_dir})")
        if not tmp_dir or not os.path.isdir(tmp_dir):
            return
        # 親の _MEIPASS そのものは消さない(pyinstaller実装に影響するため)
        if parent_dir and os.path.abspath(tmp_dir) == os.path.abspath(parent_dir):
            return
        # ハンドル解放のタイムラグがある場合に備えてリトライ
        retries = 5
        for i in range(retries):
            try:
                shutil.rmtree(tmp_dir)
                break
            except Exception:
                if i == retries - 1:
                    raise
                time.sleep(0.6)
    except Exception as e:
        print_log(f"[WARN] Failed to cleanup server temp '{tmp_dir}': {e}")


def create_tmp_dir():
    """サーバー用一時ディレクトリを作成する."""
    parent_mei = getattr(sys, "_MEIPASS", None)
    if parent_mei:
        tmp_dir = parent_mei + "_s"
    else:
        tmp_dir = os.path.join(tempfile.gettempdir(), "myapp_server_s")
    try:
        os.makedirs(tmp_dir, exist_ok=True)
    except Exception:
        pass
    return parent_mei, tmp_dir


# -----------------------
# メイン処理
# -----------------------
def main():
    # 初期設定
    init_platform()

    # 環境確認など
    if not check_edge_installed() and sys.platform == "win32":
        install_webview2()
        sys.exit(1)

    # パラメータ初期化
    params = MyParameters()
    params.log_queue = multiprocessing.Queue()

    # アプリケーションサーバの path、ポート取得
    envs = load_server_env(APP_CONFIG_FILE)
    params.server_port = get_server_port(envs)
    params.server_exe_path = get_server_exe_path(APP_SERVER_EXE)

    # サーバー用一時ベースディレクトリを決定
    tmp_base_dir, tmp_dir = create_tmp_dir()
    params.server_tmp_base = tmp_base_dir
    params.server_tmp_dir = tmp_dir

    # Webviewの表示
    try:
        import webview
        content = get_initial_content()
        window = webview.create_window(APP_NAME, html=content, width=1000, height=700, text_select=True)
        if params.server_exe_path:
            webview.start(wait_server_starting, (window, params), gui=os.environ["PYWEBVIEW_GUI"], debug=False)
        else:
            window.load_html("<h1 style='color: red'>アプリケーションの起動に失敗しました</h1><div>(内部サーバが存在しません)</div>")
    except Exception as e:
        print_log("[ERROR] WebView failed to start:")
        print_log(f"Exception: {e}")
        traceback.print_exc()
    finally:
        # サーバ停止
        terminate_server_process(params)
        # 一時ディレクトリを削除
        cleanup_server_tmp(params)


if __name__ == "__main__":
    main()
myapp_server.py
from streamlit.web import cli as stcli
import os
import sys

from common import get_server_port, load_server_env, resource_path


# streamlitの設定ファイル
APP_CONFIG_FILE = os.path.join(".streamlit", "config.toml")

# TOPページのPATH
TOP_PAGE = os.path.join("pages", "top.py")


if __name__ == "__main__":

    # アプリケーションサーバのpath、ポート取得
    host = os.environ.get("STREAMLIT_SERVER_ADDRESS") or os.environ.get("STREAMLIT_HOST") or "localhost"
    streamlit_env = load_server_env(APP_CONFIG_FILE)
    APP_PORT = get_server_port(streamlit_env)

    # Streamlitを起動
    sys.argv = [
        "streamlit",
        "run",
        resource_path(TOP_PAGE),
        "--server.headless", "true",
        "--server.address", host,
        "--server.port", str(APP_PORT),
        "--logger.level", "error",
        "--global.developmentMode", "false",
    ]
    sys.exit(stcli.main())
common.py
import datetime
import multiprocessing
import os
import re
import sys
import pathlib
import tomllib


def print_log(*args, **kwargs):
    t = datetime.datetime.now()
    p_args = [t.strftime("%Y-%m-%d %H:%M:%S"), *args]
    print(*p_args, **kwargs)


def init_platform():
    """multiprocessing 初期化"""
    if sys.platform == "darwin":
        # Mac なら fork 使用
        multiprocessing.set_start_method("fork")
        os.environ["PYWEBVIEW_GUI"] = "cocoa"
    elif sys.platform == "win32":
        # exe化した時に親子プロセスの区別をつける為に必要
        multiprocessing.freeze_support()
        os.environ["PYWEBVIEW_GUI"] = "edgechromium"
    else:
        multiprocessing.set_start_method("fork")
        os.environ["PYWEBVIEW_GUI"] = "gtk"


def check_edge_installed():
    """Edge Webview2 インストール確認"""
    if sys.platform != "win32":
        print("[INFO] Non-Windows OS, skipping Edge check")
        return True

    try:
        import winreg
    except ImportError:
        print("[WARN] winreg module not available")
        return False

    keys_to_check = [
        r"SOFTWARE\Microsoft\EdgeUpdate",
        r"SOFTWARE\Microsoft\Edge",
    ]

    for key_path in keys_to_check:
        try:
            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path):
                print(f"[INFO] Found registry key: {key_path}")
                return True
        except FileNotFoundError:
            continue

    print("[WARN] No Edge registry key found. WebView2 may not work.")
    return False


def install_webview2():
    """WebView2 ランタイムのインストール案内(GUI + ブラウザ起動)"""
    if sys.platform != "win32":
        return

    import webbrowser
    try:
        import ctypes
        MB_OK = 0x00000000
        MB_ICONINFORMATION = 0x00000040
        ctypes.windll.user32.MessageBoxW(
            None,
            "WebView2 ランタイムが見つかりません。\nOK を押すとダウンロードページを開きます。",
            "WebView のインストールが必要です",
            MB_OK | MB_ICONINFORMATION,
        )
    except Exception:
        # メッセージボックスに失敗しても続行(ブラウザ起動)
        pass

    url = "https://developer.microsoft.com/ja-jp/microsoft-edge/webview2/"
    try:
        webbrowser.open(url)
    except Exception:
        pass

    # インストール後の再起動を促すため、ここで異常終了させる
    raise RuntimeError("WebView2 runtime not installed. Prompted user to install.")


def to_streamlit_envs(env_dict):
    """
    streamlitの設定ファイル情報を環境変数として取得する.

    Returns:
        {"STREAMLIT_XXXX": 値, "STREAMLIT_XXXX": 値, ...}
    """
    results = {}
    keys = env_dict.copy().keys()
    for camel_key1 in keys:
        values = env_dict[camel_key1]
        snake_key1 = re.sub("([A-Z])", lambda x: "_" + x.group(1).lower(), camel_key1).upper()
        child_keys = values.keys()
        for camel_key2 in child_keys:
            val = values[camel_key2]
            snake_key2 = re.sub("([A-Z])", lambda x: "_" + x.group(1).lower(), camel_key2).upper()
            env_name = f"STREAMLIT_{snake_key1}_{snake_key2}"
            results[env_name] = val
    return results


def resource_path(relative_path):
    """リソースパス解決"""
    if hasattr(sys, "_MEIPASS"):
        print("sys._MEIPASS: ", sys._MEIPASS)
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)


def load_server_env(config_path):
    """サーバ設定ファイル読み込み"""
    # 設定ファイルの内容を取得
    internal_config_path = resource_path(config_path)
    print(f"internal_config_path: {internal_config_path}")
    data = {}
    if os.path.isfile(internal_config_path):
        with open(internal_config_path, mode="rb") as f:
            data = to_streamlit_envs(tomllib.load(f))

    overwrite_config_path = os.path.join(str(pathlib.Path(sys.executable).parent), config_path)
    print(f"overwrite_config_path: {overwrite_config_path}")

    # 上書き設定ファイルがある場合は取得 及び 上書き
    if os.path.isfile(overwrite_config_path) and internal_config_path != overwrite_config_path:
        with open(overwrite_config_path, mode="rb") as f:
            data2 = to_streamlit_envs(tomllib.load(f))
            keys = data2.keys()
            for key in keys:
                data[key] = data2[key]
    # 環境変数を設定
    for key in data.keys():
        print(f"{key} = {data[key]}")
        os.environ[key] = f"{data[key]}"

    return data


def get_server_port(streamlit_env):
    """streamlitのポート番号取得"""
    if "STREAMLIT_SERVER_PORT" in streamlit_env:
        return streamlit_env["STREAMLIT_SERVER_PORT"]
    return 8501


def get_initial_content():
    """初期ページ取得"""
    return """<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<style>
body {
  text-align: center;
  margin-top: 20%;
}
.loading {
  position: relative;
}
.loading::after {
  content: "";
  display: inline-block;
  width: 30px;
  text-align: left;
  animation: dots 1.5s steps(3, end) infinite;
}
@keyframes dots {
  0%  { content: ""; }
  25% { content: "."; }
  50% { content: ".."; }
  75% { content: "..."; }
}
</style>
</head>
<body>

<h1 class="loading">アプリケーションを起動しています</h1>

</body>
</html>
"""
check_sign_win.py
import subprocess
import json


def get_exe_thumbprint(exe_path):
    info = get_signature_info(exe_path)
    if "Thumbprint" not in info:
        raise ValueError("証明書を取得できませんでした。")
    return info["Thumbprint"]


def get_signature_info(exe_path: str):
    ps_script = f"""
    $sig = Get-AuthenticodeSignature '{exe_path}';
    $obj = [PSCustomObject]@{{
        Status = $sig.Status.ToString();
        Signer = $sig.SignerCertificate.Subject;
        Issuer = $sig.SignerCertificate.Issuer;
        Thumbprint = $sig.SignerCertificate.Thumbprint;
    } };
    $obj | ConvertTo-Json -Compress
    """
    result = subprocess.run(["powershell", "-Command", ps_script], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError("署名情報の取得に失敗しました")

    info = json.loads(result.stdout)
    return info


if __name__ == "__main__":
    # 確認用
    exe = "dist/myapp_server.exe"
    info = get_signature_info(exe)
    print("署名検証結果:")
    print(json.dumps(info, indent=2, ensure_ascii=False))

    # 署名者(Signer)、証明書ハッシュ(Thumbprint) などを照合
    if "Your Company Name" in info["Signer"]:
        print("OK")
    else:
        print("NG")

バイナリビルド(PyInstaller, Nuitka)

x_build.sh

#!/bin/bash
#====================================================
# build app
#====================================================

venv_dir=".venv"
stime=`date "+%Y-%m-%d %H:%M:%S"`

# Python仮想環境作成
if [ ! -d "${venv_dir}" ]; then
    echo "Creating virtual environment..."
    python -m venv ${venv_dir}
fi
source ${venv_dir}/bin/activate

# 必要パッケージインストール
pip install --upgrade pip
pip install -r requirements.txt

# ビルド形式
#BUILD_MODE="--onedir"
BUILD_MODE=--onefile
#NOCONSOLE=--noconsole
NOCONSOLE=

# webviewビルド
echo "Building webview exe..."
pyinstaller ${BUILD_MODE} ${NOCONSOLE} \
    --clean \
    --noconfirm \
    --add-data ".streamlit:.streamlit" \
    myapp_webview.py


# webアプリ(streamlit)ビルド
echo "Building Streamlit exe..."
pyinstaller ${BUILD_MODE} ${NOCONSOLE} \
    --clean \
    --noconfirm \
    --add-data "pages:pages" \
    --add-data ".streamlit:.streamlit" \
    --collect-all streamlit \
    --collect-all pyvista \
    --collect-all stpyvista \
    myapp_server.py

deactivate

etime=`date "+%Y-%m-%d %H:%M:%S"`
echo ""
echo "process times ... started at: ${stime} , finished at: ${etime}"
echo ""

win_build_pyinstaller.bat

@echo off
REM ###############################################
REM pyinstaller でビルド
REM ###############################################

set STIME=%DATE% %TIME%

set venv_dir=.venv

REM 仮想環境の有効化
IF NOT EXIST %venv_dir%\Scripts\activate.bat python -m venv %venv_dir%
call %venv_dir%\Scripts\activate

REM 必要パッケージインストール
set PYTHONUTF8=1
REM pip install --upgrade pip
pip install -r requirements.txt

REM set BUILD_MODE=--onedir
set BUILD_MODE=--onefile
REM set NOCONSOLE=--noconsole
set NOCONSOLE=

echo "webview build"
pyinstaller ^
    %BUILD_MODE% %NOCONSOLE% ^
    --clean ^
    --noconfirm ^
    --add-data ".streamlit:.streamlit" ^
    myapp_webview.py

echo "streamlit build"
pyinstaller ^
    %BUILD_MODE% %NOCONSOLE% ^
    --clean ^
    --noconfirm ^
    --add-data "pages:pages" ^
    --add-data ".streamlit:.streamlit" ^
    --collect-all streamlit ^
    --collect-all pyvista ^
    --collect-all stpyvista ^
    myapp_server.py

call %venv_dir%\Scripts\deactivate

set ETIME=%DATE% %TIME%
echo.
echo process times ... started at: %STIME% , finished at: %ETIME%
echo.

win_build_nuitka.bat

@echo off
REM ====================================================
REM Nuitka でビルド(Windows)
REM ====================================================

set STIME=%DATE% %TIME%

set venv_dir=.venv_webview

REM 仮想環境の有効化
IF NOT EXIST %venv_dir%\Scripts\activate.bat python -m venv %venv_dir%
call %venv_dir%\Scripts\activate

REM 必要パッケージインストール
set PYTHONUTF8=1
REM pip install --upgrade pip
pip install -r requirements_webview.txt
pip install Nuitka zstandard

REM 出力ディレクトリ作成
IF NOT EXIST dist_nuitka_webview mkdir dist_nuitka_webview
IF NOT EXIST dist_nuitka_server mkdir dist_nuitka_server

REM Nuitka のキャッシュをワークスペース内に制限(任意)
set NUITKA_CACHE_DIR=%CD%\.nuitka-cache
IF NOT EXIST "%NUITKA_CACHE_DIR%" mkdir "%NUITKA_CACHE_DIR%"

REM 追加データ
set INCLUDE_STREAMLIT_DIR=
IF EXIST .streamlit (
  set "INCLUDE_STREAMLIT_DIR=--include-data-dir=.streamlit=.streamlit"
)
set INCLUDE_PAGES=
IF EXIST pages (
  set "INCLUDE_PAGES=--include-data-dir=pages=pages"
)

REM デバッグ用に一時ディレクトリを残す場合は "cached" を指定
set ONEFILE_CACHE_MODE="--onefile-cache-mode=auto"
REM set ONEFILE_CACHE_MODE="--onefile-cache-mode=cached"

REM webviewビルド実行
REM 一部設定を追加(pywebview の動作に必要なdllがコピーされない為)
python -m nuitka ^
  --onefile ^
  --remove-output ^
  --output-dir=dist_nuitka_webview ^
  --disable-plugin=anti-bloat ^
  --nofollow-import-to=tkinter,_curses,curses ^
  --enable-plugin=multiprocessing ^
  --no-deployment-flag=self-execution ^
  %ONEFILE_CACHE_MODE% ^
  %INCLUDE_STREAMLIT_DIR% ^
  --include-data-files=%venv_dir%\Lib\site-packages\webview\lib\WebBrowserInterop.x86.dll=webview/lib/WebBrowserInterop.x86.dll ^
  --include-data-files=%venv_dir%\Lib\site-packages\webview\lib\runtimes\win-arm64\native\WebView2Loader.dll=webview/lib/runtimes/win-arm64/native/WebView2Loader.dll ^
  --include-data-files=%venv_dir%\Lib\site-packages\webview\lib\runtimes\win-x86\native\WebView2Loader.dll=webview/lib/runtimes/win-x86/native/WebView2Loader.dll ^
  myapp_webview.py

REM serverビルド実行
python -m nuitka ^
  --onefile ^
  --remove-output ^
  --output-dir=dist_nuitka_server ^
  --disable-plugin=anti-bloat ^
  --nofollow-import-to=tkinter,_curses,curses ^
  --enable-plugin=multiprocessing ^
  --no-deployment-flag=self-execution ^
  %ONEFILE_CACHE_MODE% ^
  %INCLUDE_STREAMLIT_DIR% ^
  %INCLUDE_PAGES% ^
  myapp_server.py

call %venv_dir%\Scripts\deactivate

set ETIME=%DATE% %TIME%
echo.
echo process times ... started at: %STIME% , finished at: %ETIME%
echo.

TODO

TODO: サーバ起動待機中にwebviewを終了すると一時ディレクトリが残る?

添付ファイル: fileexe_image.png 4件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-12-01 (月) 08:22:04 (4d)