Skip to content

[4.0.0-beta desktop] Frozen app loads config.json via __file__ instead of _MEIPASS #256

Closed

Description

@Fujio-Turner

PROBLEM

Static assets go through app_base.get_resource_path(), which already handles PyInstaller:

def get_resource_path():
    if getattr(sys, "frozen", False):
        return sys._MEIPASS
    return os.path.dirname(os.path.abspath(__file__))

Server config does not:

# app.py _load_server_config()
here = os.path.dirname(os.path.abspath(__file__))
candidates.extend([
    os.path.join(here, "config.json"),
    os.path.join(here, "config.default.json"),
])

On Windows --onefile, __file__ for app.py is not reliably the same directory as the extracted datas (config.json is added as ('.',) into _MEIPASS). Result: server.port from config is ignored (falls back to the 8080 default — currently masks the bug) and any future keys in config.json never load in the packaged app.

config.json is also in .gitignore but tracked on this branch, so CI happens to copy it. A clean clone that respects gitignore would make PyInstaller fail the datas entry ('config.json', '.') because the file is absent — another landmine.

Suggested RCA

Resource-path handling was added for index.html / assets in app_base.py and never reused for the later _load_server_config() helper. CI --add-data "config.json;." assumes a file that gitignore says should not be in the repo (config.default.json is the one that should be bundled).

Suggested FIX

# app.py
def _load_server_config() -> dict:
    candidates = []
    env_path = os.environ.get("APP_CONFIG_FILE")
    if env_path:
        candidates.append(env_path)
    here = get_resource_path()  # same helper as static files
    candidates.extend([
        os.path.join(here, "config.json"),          # user override, optional
        os.path.join(here, "config.default.json"),  # shipped default
    ])
    ...

Import get_resource_path from app_base (already imported as DIRECTORY).

PyInstaller datas: ship config.default.json, not config.json.

datas += [('config.default.json', '.')]

Unit tests

# tests/python/test_server_config_path.py
import os
import json
from unittest.mock import patch

def test_load_server_config_uses_meipass_when_frozen(tmp_path, monkeypatch):
    cfg = tmp_path / "config.default.json"
    cfg.write_text(json.dumps({"server": {"port": 9090}}))
    import app

    monkeypatch.setattr(app.sys, "frozen", True, raising=False)
    monkeypatch.setattr(app.sys, "_MEIPASS", str(tmp_path), raising=False)
    monkeypatch.delenv("PORT", raising=False)
    monkeypatch.delenv("APP_CONFIG_FILE", raising=False)

    with patch("app.get_resource_path", return_value=str(tmp_path)):
        data = app._load_server_config()
    assert data["server"]["port"] == 9090

def test_spec_bundles_default_config_not_user_config():
    from pathlib import Path
    mac = Path("app/build_mac.spec").read_text()
    assert "config.default.json" in mac
    # user config.json must not be required at build time
    assert "('config.json'" not in mac

Context: found on branch release-4.0.0-beta while reviewing standalone macOS .app / Windows .exe packaging. Issues in this batch share the title prefix [4.0.0-beta desktop].

Activity

  1. Fujio-Turner commented on Sep 15, 2026

    @Fujio-Turner
    OwnerAuthor

    Fixed on release-4.0.0-beta via #263

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions