diff --git a/installer/build.py b/installer/build.py index 91fbcb20..db3291e7 100644 --- a/installer/build.py +++ b/installer/build.py @@ -405,6 +405,40 @@ def run_dev() -> int: ).returncode +def generate_build_metadata(args: argparse.Namespace) -> None: + """Generate version and build info files (mirrors CI steps).""" + print("\n" + "=" * 60) + print("Generating build metadata...") + print("=" * 60 + "\n") + + # Generate version file + version_script = ROOT / "scripts" / "generate_version.py" + if version_script.exists(): + run_command([sys.executable, str(version_script)], cwd=ROOT) + else: + print(f"Warning: {version_script} not found, skipping version generation") + + # Generate build info (_build_info.py with BUILD_TAG) + build_info_script = ROOT / "scripts" / "generate_build_info.py" + if build_info_script.exists(): + tag = args.tag + if not tag and args.nightly: + from datetime import datetime, timezone + + tag = f"nightly-{datetime.now(timezone.utc).strftime('%Y%m%d')}" + + cmd = [sys.executable, str(build_info_script)] + if tag: + cmd.append(tag) + print(f" Build tag: {tag}") + else: + print(" Build tag: None (stable)") + + run_command(cmd, cwd=ROOT) + else: + print(f"Warning: {build_info_script} not found, skipping build info generation") + + def main() -> int: """Run the build process.""" parser = argparse.ArgumentParser( @@ -441,6 +475,17 @@ def main() -> int: action="store_true", help="Skip portable ZIP creation", ) + parser.add_argument( + "--nightly", + action="store_true", + help="Build as nightly (generates nightly-YYYYMMDD build tag)", + ) + parser.add_argument( + "--tag", + type=str, + default=None, + help="Custom build tag (e.g. nightly-20260208). Overrides --nightly.", + ) args = parser.parse_args() @@ -471,6 +516,9 @@ def main() -> int: if not args.skip_icons: check_icons() + # Generate version and build info (same as CI) + generate_build_metadata(args) + # Build with PyInstaller if not build_pyinstaller(): return 1 diff --git a/src/accessiweather/app.py b/src/accessiweather/app.py index 1afee0e3..869488e5 100644 --- a/src/accessiweather/app.py +++ b/src/accessiweather/app.py @@ -377,6 +377,16 @@ def do_check(): current_version = getattr(self, "version", "0.0.0") build_tag = getattr(self, "build_tag", None) current_nightly_date = parse_nightly_date(build_tag) if build_tag else None + display_version = current_nightly_date if current_nightly_date else current_version + + # Safety: if frozen but no build_tag and checking nightly channel, + # skip auto-prompt to avoid infinite update loops + if not build_tag and channel == "nightly": + logger.warning( + "Skipping startup nightly update check: no build_tag available. " + "Use Help > Check for Updates to check manually." + ) + return async def check(): service = UpdateService("AccessiWeather") @@ -399,7 +409,7 @@ async def check(): def show_update_notification(): result = wx.MessageBox( f"A new {channel_label} update is available!\n\n" - f"Current: {current_version}\n" + f"Current: {display_version}\n" f"Latest: {update_info.version}\n\n" "Download now?", "Update Available", diff --git a/src/accessiweather/config_utils.py b/src/accessiweather/config_utils.py index 585b1ceb..94947a2a 100644 --- a/src/accessiweather/config_utils.py +++ b/src/accessiweather/config_utils.py @@ -52,6 +52,20 @@ def is_portable_mode() -> bool: logger.debug(f"Checking portable mode for executable directory: {app_dir}") + # Check for uninstaller (Inno Setup leaves unins*.exe in app directory) + # This reliably detects installed copies regardless of install location + app_dir_path = os.path.dirname(sys.executable) + uninstaller_exists = any( + f.startswith("unins") and f.endswith(".exe") + for f in os.listdir(app_dir_path) + if os.path.isfile(os.path.join(app_dir_path, f)) + ) + if uninstaller_exists: + logger.debug( + f"Not in portable mode: uninstaller found in {app_dir_path}" + ) + return False + # Check if we're running from Program Files (standard installation) program_files = os.environ.get("PROGRAMFILES", "") program_files_x86 = os.environ.get("PROGRAMFILES(X86)", "") diff --git a/src/accessiweather/ui/main_window.py b/src/accessiweather/ui/main_window.py index 43b10c09..930ebfe4 100644 --- a/src/accessiweather/ui/main_window.py +++ b/src/accessiweather/ui/main_window.py @@ -429,6 +429,8 @@ def _on_check_updates(self) -> None: current_version = getattr(self.app, "version", "0.0.0") build_tag = getattr(self.app, "build_tag", None) current_nightly_date = parse_nightly_date(build_tag) if build_tag else None + # Show nightly date as the display version when running a nightly build + display_version = current_nightly_date if current_nightly_date else current_version # Show checking status wx.BeginBusyCursor() @@ -460,7 +462,7 @@ async def check(): elif current_nightly_date: msg = f"You're on the latest nightly ({current_nightly_date})." else: - msg = f"You're up to date ({current_version})." + msg = f"You're up to date ({display_version})." wx.CallAfter( wx.MessageBox, @@ -475,7 +477,7 @@ async def check(): def prompt(): result = wx.MessageBox( f"A new {channel_label} update is available!\n\n" - f"Current: {current_version}\n" + f"Current: {display_version}\n" f"Latest: {update_info.version}\n\n" "Download now?", "Update Available",