"""Launch platform install scripts to upgrade a packaged DBS Annotator build."""
from __future__ import annotations
import logging
import os
import ssl
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
import certifi
from ..config import RELEASES_GITHUB_REPO
logger = logging.getLogger(__name__)
_INSTALL_SCRIPT_BRANCH = "main"
_USER_AGENT = "DBSAnnotator-AutoUpdate/1.0"
# Windows-only; absent on Linux/macOS (CI runs unit tests there too).
_WINDOWS_NEW_CONSOLE = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
def _install_script_url(filename: str) -> str:
return (
f"https://raw.githubusercontent.com/{RELEASES_GITHUB_REPO}/"
f"{_INSTALL_SCRIPT_BRANCH}/scripts/{filename}"
)
def _download_install_script(filename: str) -> Path:
url = _install_script_url(filename)
request = urllib.request.Request(
url,
headers={"User-Agent": _USER_AGENT},
)
ctx = ssl.create_default_context(cafile=certifi.where())
with urllib.request.urlopen(request, timeout=60, context=ctx) as response:
data = response.read()
suffix = ".ps1" if filename.endswith(".ps1") else ".sh"
fd, path_str = tempfile.mkstemp(prefix="dbs_annotator_install_", suffix=suffix)
os.close(fd)
path = Path(path_str)
path.write_bytes(data)
if suffix == ".sh":
path.chmod(0o755)
return path
[docs]
def automatic_update_supported() -> bool:
"""True on platforms where ``scripts/install.*`` can be launched."""
return (
sys.platform == "win32"
or sys.platform == "darwin"
or sys.platform.startswith("linux")
)
[docs]
def automatic_update_targets_packaged_install() -> bool:
"""True when the updater installs into the standard Briefcase location."""
return bool(getattr(sys, "frozen", False))
[docs]
def launch_automatic_update(
tag_name: str,
*,
dry_run: bool = False,
) -> tuple[bool, str]:
"""Start the GitHub release installer for *tag_name* (e.g. ``v0.4.0b2``).
Args:
dry_run: If True, run the platform install script in preview mode only
(PowerShell ``-WhatIf`` / ``install.sh --dry-run``). No files are
written.
Returns:
``(True, user_message)`` on success, ``(False, error_message)`` otherwise.
The installer runs in a separate process; the user must restart the app.
"""
tag = tag_name.strip()
if not tag:
return False, "Release tag is missing."
try:
if sys.platform == "win32":
return _launch_windows(tag, dry_run=dry_run)
if sys.platform == "darwin":
return _launch_unix(tag, "install.sh", dry_run=dry_run)
if sys.platform.startswith("linux"):
return _launch_unix(tag, "install.sh", dry_run=dry_run)
return False, f"Automatic update is not supported on {sys.platform!r}."
except OSError as exc:
logger.info("Automatic update failed: %s", exc)
return False, str(exc)
except urllib.error.URLError as exc:
logger.info("Could not download install script: %s", exc)
return False, f"Could not download the installer script:\n\n{exc}"
def _launch_windows(tag: str, *, dry_run: bool = False) -> tuple[bool, str]:
script = _download_install_script("install.ps1")
try:
cmd = [
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(script),
"-VersionTag",
tag,
"-GitHubRepository",
RELEASES_GITHUB_REPO,
]
if dry_run:
cmd.append("-WhatIf")
subprocess.Popen(
cmd,
creationflags=_WINDOWS_NEW_CONSOLE,
close_fds=True,
)
finally:
if not dry_run:
try:
script.unlink(missing_ok=True)
except OSError:
pass
if dry_run:
return True, (
"Dry run: a PowerShell window will show what the installer would do "
"(no files are changed). Check that window for errors before running "
"a real update."
)
return True, (
"The updater is running in a new window. When it finishes, close this "
"application and open DBS Annotator again from the Start menu."
)
def _launch_unix(tag: str, filename: str, *, dry_run: bool = False) -> tuple[bool, str]:
script = _download_install_script(filename)
args = "--dry-run" if dry_run else ""
wrapper = script.with_name(f"run_{script.name}")
wrapper.write_text(
"#!/bin/sh\n"
f'export DBS_ANNOTATOR_INSTALL_REPO="{RELEASES_GITHUB_REPO}"\n'
f'export DBS_ANNOTATOR_VERSION="{tag}"\n'
f'exec "{script}" {args} "{tag}"\n',
encoding="utf-8",
)
wrapper.chmod(0o755)
if sys.platform == "darwin":
cmd = [
"osascript",
"-e",
f'tell application "Terminal" to do script "{wrapper}"',
]
subprocess.Popen(cmd, start_new_session=True, close_fds=True)
else:
launched = False
for term_cmd in (
["x-terminal-emulator", "-e", str(wrapper)],
["konsole", "-e", str(wrapper)],
["gnome-terminal", "--", str(wrapper)],
):
if _which(term_cmd[0]):
subprocess.Popen(
term_cmd,
start_new_session=True,
close_fds=True,
)
launched = True
break
if not launched:
env = os.environ.copy()
env["DBS_ANNOTATOR_INSTALL_REPO"] = RELEASES_GITHUB_REPO
env["DBS_ANNOTATOR_VERSION"] = tag
cmd = [str(script)]
if dry_run:
cmd.append("--dry-run")
cmd.append(tag)
subprocess.Popen(
cmd,
env=env,
start_new_session=True,
close_fds=True,
)
if dry_run:
return True, (
"Dry run: a terminal window will show what the installer would do "
"(no files are changed). Check that window for errors before running "
"a real update."
)
return True, (
"The updater is running. When it finishes, quit this application and "
"reopen DBS Annotator from your applications menu."
)
def _which(name: str) -> str | None:
from shutil import which
return which(name)