Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions src/local_asr_server/app_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,54 @@ def as_health_payload(self) -> dict:
}


def get_bundle_display_name() -> str:
"""Return the visible bundle name from Info.plist when bundled."""
def _bundle_info() -> dict:
contents_dir = get_app_contents_dir()
if contents_dir is None:
return APP_NAME
return {}

info_plist = contents_dir / "Info.plist"
try:
with info_plist.open("rb") as f:
payload = plistlib.load(f)
except Exception:
return APP_NAME
return {}
return payload if isinstance(payload, dict) else {}


def get_bundle_display_name() -> str:
"""Return the visible bundle name from Info.plist when bundled."""
payload = _bundle_info()
value = payload.get("CFBundleDisplayName") or payload.get("CFBundleName")
return str(value or APP_NAME)


def get_bundle_identifier() -> str:
"""Return the bundle identifier from Info.plist when bundled, falling back to paths.APP_BUNDLE_ID."""
contents_dir = get_app_contents_dir()
if contents_dir is None:
return APP_BUNDLE_ID

info_plist = contents_dir / "Info.plist"
try:
with info_plist.open("rb") as f:
payload = plistlib.load(f)
except Exception:
return APP_BUNDLE_ID

"""Return the bundle identifier from Info.plist when bundled."""
payload = _bundle_info()
value = payload.get("CFBundleIdentifier")
return str(value or APP_BUNDLE_ID)


def get_bundle_version() -> str | None:
"""Return the product version embedded in the macOS bundle, when available."""
payload = _bundle_info()
value = payload.get("CFBundleShortVersionString") or payload.get("CFBundleVersion")
text = str(value or "").strip()
return text or None


def get_app_version() -> str:
"""Return the installed package version, falling back to the module version."""
"""Return the product identity version for bundled apps, package version otherwise.

ClosedRoom's root VERSION owns the macOS product version and is embedded in
Info.plist at build time. The Python distribution has an independent package
version, so a frozen app must not expose that package version as its runtime
application identity.
"""
if is_bundled():
bundle_version = get_bundle_version()
if bundle_version:
return bundle_version
try:
return metadata.version("local-asr-server")
except metadata.PackageNotFoundError:
Expand Down
60 changes: 60 additions & 0 deletions test/test_app_identity_product_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import plistlib
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

from local_asr_server import app_identity


class AppIdentityProductVersionTests(unittest.TestCase):
def _contents_dir(self, payload: dict) -> tuple[tempfile.TemporaryDirectory[str], Path]:
tmp = tempfile.TemporaryDirectory()
contents = Path(tmp.name) / "Contents"
contents.mkdir(parents=True)
with (contents / "Info.plist").open("wb") as handle:
plistlib.dump(payload, handle)
return tmp, contents

def test_bundled_identity_uses_product_bundle_version_not_python_package_version(self) -> None:
tmp, contents = self._contents_dir(
{
"CFBundleIdentifier": "com.closedroom.app",
"CFBundleName": "ClosedRoom",
"CFBundleDisplayName": "ClosedRoom",
"CFBundleVersion": "0.2.0",
"CFBundleShortVersionString": "0.2.0",
}
)
self.addCleanup(tmp.cleanup)

with (
patch.object(app_identity, "get_app_contents_dir", return_value=contents),
patch.object(app_identity, "is_bundled", return_value=True),
patch.object(app_identity.metadata, "version", return_value="0.1.0"),
):
identity = app_identity.get_app_identity()

self.assertEqual(identity.version, "0.2.0")
self.assertEqual(identity.as_health_payload()["app_version"], "0.2.0")
self.assertNotEqual(identity.version, "0.1.0")

def test_bundle_version_falls_back_to_cf_bundle_version(self) -> None:
tmp, contents = self._contents_dir({"CFBundleVersion": "0.2.1"})
self.addCleanup(tmp.cleanup)

with patch.object(app_identity, "get_app_contents_dir", return_value=contents):
self.assertEqual(app_identity.get_bundle_version(), "0.2.1")

def test_non_bundled_identity_keeps_python_package_version(self) -> None:
with (
patch.object(app_identity, "is_bundled", return_value=False),
patch.object(app_identity.metadata, "version", return_value="0.1.0"),
):
self.assertEqual(app_identity.get_app_version(), "0.1.0")


if __name__ == "__main__":
unittest.main()
Loading