Add brand-specific update channel and environment configuration

- Updated `brand_config.py` to include `WEBDROP_UPDATE_CHANNEL` in the environment variables.
- Enhanced `build_macos.sh` to create a bundled `.env` file with brand-specific defaults, including the update channel.
- Implemented a method in `build_windows.py` to create a bundled `.env` file for Windows builds, incorporating brand-specific runtime defaults.
- Modified `config.py` to ensure the application can locate the `.env` file in various installation scenarios.
- Added unit tests in `test_config.py` to verify the loading of the bootstrap `.env` from the PyInstaller runtime directory.
- Generated new WiX object and script files for the Windows installer, including application shortcuts and registry entries.
This commit is contained in:
claudi 2026-03-12 09:04:27 +01:00
parent de6e9838e5
commit eab1009d8c
9 changed files with 3083 additions and 2886 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Product Id="*" Name="Agravity Bridge" Language="1033" Version="0.8.4"
Manufacturer="agravity"
UpgradeCode="4a7c80da-6170-4d88-8efc-3f30636f6392">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Media Id="1" Cabinet="AgravityBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<!-- Application Icon -->
<Icon Id="AppIcon.ico" SourceFile="C:\Development\VS Code Projects\webdrop_bridge\resources\icons\app.ico" />
<!-- Custom branding for InstallDir dialog set -->
<WixVariable Id="WixUIDialogBmp" Value="C:\Development\VS Code Projects\webdrop_bridge\resources\icons\background.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="C:\Development\VS Code Projects\webdrop_bridge\resources\icons\banner.bmp" />
<WixVariable Id="WixUILicenseRtf" Value="C:\Development\VS Code Projects\webdrop_bridge\resources\license.rtf" />
<!-- Installation UI dialogs -->
<UIRef Id="WixUI_InstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
<!-- Close running application before installation -->
<util:CloseApplication
Target="AgravityBridge.exe"
CloseMessage="yes"
RebootPrompt="no"
ElevatedCloseMessage="no" />
<Feature Id="ProductFeature" Title="Agravity Bridge" Level="1">
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="ProgramMenuShortcut" />
<ComponentRef Id="DesktopShortcut" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="Agravity Bridge" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="Agravity Bridge"/>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ProgramMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="Agravity Bridge"
Description="Agravity drag-and-drop bridge"
Target="[INSTALLFOLDER]AgravityBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="ApplicationProgramsFolderRemove"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\AgravityBridge"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="*">
<Shortcut Id="DesktopApplicationShortcut"
Name="Agravity Bridge"
Description="Agravity drag-and-drop bridge"
Target="[INSTALLFOLDER]AgravityBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU"
Key="Software\AgravityBridge"
Name="DesktopShortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -269,6 +269,7 @@ def cli_env(args: argparse.Namespace) -> int:
"WEBDROP_EXE_NAME": brand.exe_name,
"WEBDROP_BUNDLE_ID": brand.bundle_identifier,
"WEBDROP_CONFIG_DIR_NAME": brand.config_dir_name,
"WEBDROP_UPDATE_CHANNEL": brand.update_channel,
"WEBDROP_ICON_ICO": str(brand.icon_ico),
"WEBDROP_ICON_ICNS": str(brand.icon_icns),
}

View file

@ -200,8 +200,21 @@ build_executable() {
log_info "Building macOS executable with PyInstaller..."
echo ""
# Create bundled runtime .env with brand defaults so first launch
# uses brand-specific app name and config directory.
BUNDLED_ENV_FILE="$TEMP_BUILD/.env"
cp "$ENV_FILE" "$BUNDLED_ENV_FILE"
{
echo ""
echo "# Brand-specific defaults added during packaging"
echo "APP_NAME=\"$WEBDROP_APP_DISPLAY_NAME\""
echo "BRAND_ID=\"$WEBDROP_BRAND_ID\""
echo "APP_CONFIG_DIR_NAME=\"$WEBDROP_CONFIG_DIR_NAME\""
echo "UPDATE_CHANNEL=\"$WEBDROP_UPDATE_CHANNEL\""
} >> "$BUNDLED_ENV_FILE"
# Export env file for spec file to pick up
export WEBDROP_ENV_FILE="$ENV_FILE"
export WEBDROP_ENV_FILE="$BUNDLED_ENV_FILE"
export WEBDROP_VERSION="$VERSION"
export WEBDROP_BUNDLE_ID="$BUNDLE_IDENTIFIER"

View file

@ -38,6 +38,8 @@ import argparse
from pathlib import Path
from datetime import datetime
from dotenv import dotenv_values
# Import shared version utilities
from brand_config import load_brand_config
from sync_version import get_current_version, do_sync_version
@ -95,6 +97,44 @@ class WindowsBuilder:
shutil.rmtree(path)
print(f" Removed {path}")
@staticmethod
def _format_env_value(value: str) -> str:
"""Format env values safely for .env files."""
if any(ch in value for ch in [" ", "#", '"', "'", "\t"]):
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
return value
def _create_bundled_env_file(self) -> Path:
"""Create a bundled .env file with brand-specific runtime defaults."""
values = dotenv_values(self.env_file)
overrides = {
"APP_NAME": self.brand.display_name,
"BRAND_ID": self.brand.brand_id,
"APP_CONFIG_DIR_NAME": self.brand.config_dir_name,
"UPDATE_CHANNEL": self.brand.update_channel,
}
output_env = self.temp_dir / ".env"
output_env.parent.mkdir(parents=True, exist_ok=True)
lines: list[str] = []
for key, raw_value in values.items():
if key in overrides:
continue
if raw_value is None:
lines.append(key)
else:
lines.append(f"{key}={self._format_env_value(str(raw_value))}")
lines.append("")
lines.append("# Brand-specific defaults added during packaging")
for key, value in overrides.items():
lines.append(f"{key}={self._format_env_value(value)}")
output_env.write_text("\n".join(lines) + "\n", encoding="utf-8")
return output_env
def build_executable(self) -> bool:
"""Build executable using PyInstaller."""
print("\n🔨 Building Windows executable with PyInstaller...")
@ -119,7 +159,7 @@ class WindowsBuilder:
# Set environment variable for spec file to use
env = os.environ.copy()
env["WEBDROP_ENV_FILE"] = str(self.env_file)
env["WEBDROP_ENV_FILE"] = str(self._create_bundled_env_file())
env["WEBDROP_BRAND_ID"] = self.brand.brand_id
env["WEBDROP_APP_DISPLAY_NAME"] = self.brand.display_name
env["WEBDROP_ASSET_PREFIX"] = self.brand.asset_prefix

View file

@ -408,7 +408,15 @@ class Config:
candidate_paths.append(Path(env_file).resolve())
else:
if getattr(sys, "frozen", False):
candidate_paths.append(Path(sys.executable).resolve().parent / ".env")
exe_dir = Path(sys.executable).resolve().parent
# One-folder fallback: some packagers place data files in _internal.
candidate_paths.append(exe_dir / ".env")
candidate_paths.append(exe_dir / "_internal" / ".env")
# PyInstaller runtime extraction directory (one-file and one-folder).
meipass = getattr(sys, "_MEIPASS", None)
if meipass:
candidate_paths.append(Path(meipass).resolve() / ".env")
candidate_paths.append(Path.cwd() / ".env")
candidate_paths.append(Path(__file__).resolve().parents[2] / ".env")

View file

@ -1,6 +1,7 @@
"""Unit tests for configuration system."""
import os
import sys
import pytest
@ -232,3 +233,22 @@ class TestConfigValidation:
assert config_path.parts[-2:] == ("agravity_bridge", "config.json")
assert log_path.parts[-2:] == ("logs", "agravity_bridge.log")
class TestBootstrapEnvLoading:
"""Test bootstrap .env loading behavior for packaged builds."""
def test_load_bootstrap_env_reads_meipass_dotenv(self, tmp_path, monkeypatch):
"""Packaged app should load .env from PyInstaller runtime directory."""
meipass_dir = tmp_path / "runtime"
meipass_dir.mkdir(parents=True)
env_path = meipass_dir / ".env"
env_path.write_text("APP_NAME=Agravity Bridge\n", encoding="utf-8")
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "_MEIPASS", str(meipass_dir), raising=False)
loaded_path = Config.load_bootstrap_env()
assert loaded_path == env_path
assert os.getenv("APP_NAME") == "Agravity Bridge"