Last active
May 6, 2026 20:10
-
-
Save CypherpunkSamurai/662d138d3637e93374e015fa30f61a68 to your computer and use it in GitHub Desktop.
libportable.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| portablelib.py — Python port of the portapps Go library. | |
| Windows-specific features (registry, msgbox, mutex, console, locale, env) | |
| use ctypes/winreg and are no-ops / raise NotImplementedError on non-Windows. | |
| """ | |
| import ctypes | |
| import json | |
| import logging | |
| import os | |
| import platform | |
| import shutil | |
| import subprocess | |
| import sys | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Any | |
| import requests | |
| import yaml | |
| # --------------------------------------------------------------------------- | |
| # Logging | |
| # --------------------------------------------------------------------------- | |
| logging.basicConfig(format="%(asctime)s [%(levelname)s] %(message)s", level=logging.DEBUG) | |
| log = logging.getLogger("portapps") | |
| _IS_WINDOWS = platform.system() == "Windows" | |
| # --------------------------------------------------------------------------- | |
| # pkg/win | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class WinVersion: | |
| major: int = 0 | |
| minor: int = 0 | |
| build: int = 0 | |
| def get_win_version() -> WinVersion: | |
| if not _IS_WINDOWS: | |
| return WinVersion() | |
| ver = sys.getwindowsversion() | |
| return WinVersion(major=ver.major, minor=ver.minor, build=ver.build) | |
| # --- msgbox constants (mirrors win/msgbox.go) --- | |
| MB_OK = 0x000000 | |
| MB_OKCANCEL = 0x000001 | |
| MB_ABORTRETRYIGNORE = 0x000002 | |
| MB_YESNOCANCEL = 0x000003 | |
| MB_YESNO = 0x000004 | |
| MB_RETRYCANCEL = 0x000005 | |
| MB_CANCELTRYCONTINUE = 0x000006 | |
| MB_ICONERROR = 0x000010 | |
| MB_ICONQUESTION = 0x000020 | |
| MB_ICONWARNING = 0x000030 | |
| MB_ICONINFORMATION = 0x000040 | |
| MB_TOPMOST = 0x041000 | |
| IDOK = 1 | |
| IDCANCEL = 2 | |
| IDABORT = 3 | |
| IDRETRY = 4 | |
| IDIGNORE = 5 | |
| IDYES = 6 | |
| IDNO = 7 | |
| IDTRYAGAIN = 10 | |
| IDCONTINUE = 11 | |
| def msg_box(title: str, message: str, flags: int = MB_OK) -> int: | |
| if _IS_WINDOWS: | |
| return ctypes.windll.user32.MessageBoxW(0, message, title, flags) | |
| print(f"[{title}] {message}") | |
| return IDOK | |
| def get_console_title() -> str: | |
| if _IS_WINDOWS: | |
| buf = ctypes.create_unicode_buffer(256) | |
| ctypes.windll.kernel32.GetConsoleTitleW(buf, len(buf)) | |
| return buf.value | |
| return "" | |
| def set_console_title(title: str) -> None: | |
| if _IS_WINDOWS: | |
| ctypes.windll.kernel32.SetConsoleTitleW(title) | |
| def locale() -> str: | |
| if _IS_WINDOWS: | |
| buf = ctypes.create_unicode_buffer(128) | |
| ret = ctypes.windll.kernel32.GetUserDefaultLocaleName(buf, len(buf)) | |
| return buf.value if ret else "en-US" | |
| return "en-US" | |
| def refresh_env() -> None: | |
| """Broadcast WM_SETTINGCHANGE so new env vars are picked up system-wide.""" | |
| if not _IS_WINDOWS: | |
| return | |
| HWND_BROADCAST = 0xFFFF | |
| WM_SETTINGCHANGE = 0x001A | |
| SMTO_ABORTIFHUNG = 0x0002 | |
| result = ctypes.c_long() | |
| ctypes.windll.user32.SendMessageTimeoutW( | |
| HWND_BROADCAST, WM_SETTINGCHANGE, 0, | |
| "Environment", SMTO_ABORTIFHUNG, 5000, | |
| ctypes.byref(result), | |
| ) | |
| def _open_env_key(hive_flag: int): | |
| import winreg | |
| return winreg.OpenKey(hive_flag, "Environment", 0, winreg.KEY_ALL_ACCESS) | |
| def set_perm_env(hive_flag: int, name: str, value: str) -> None: | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| import winreg | |
| with _open_env_key(hive_flag) as key: | |
| winreg.SetValueEx(key, name, 0, winreg.REG_SZ, value) | |
| def delete_perm_env(hive_flag: int, name: str) -> None: | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| import winreg | |
| with _open_env_key(hive_flag) as key: | |
| winreg.DeleteValue(key, name) | |
| def get_perm_env(hive_flag: int, name: str) -> str: | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| import winreg | |
| with _open_env_key(hive_flag) as key: | |
| value, _ = winreg.QueryValueEx(key, name) | |
| return value | |
| # --------------------------------------------------------------------------- | |
| # pkg/mutex | |
| # --------------------------------------------------------------------------- | |
| def create_mutex(name: str): | |
| """Returns a mutex handle (Windows) or None. Raises RuntimeError if already running.""" | |
| if not _IS_WINDOWS: | |
| return None | |
| full = f"Portapps{name}" | |
| handle = ctypes.windll.kernel32.OpenMutexW(0x1F0001, False, full) | |
| if handle: | |
| ctypes.windll.kernel32.CloseHandle(handle) | |
| raise RuntimeError("already running") | |
| return ctypes.windll.kernel32.CreateMutexW(None, False, full) | |
| def release_mutex(handle) -> None: | |
| if _IS_WINDOWS and handle: | |
| ctypes.windll.kernel32.CloseHandle(handle) | |
| # --------------------------------------------------------------------------- | |
| # pkg/proc | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class CmdOptions: | |
| command: str | |
| args: list[str] = field(default_factory=list) | |
| working_dir: str = "" | |
| hide_window: bool = False | |
| @dataclass | |
| class CmdResult: | |
| options: CmdOptions | |
| exit_code: int = 0 | |
| stdout: str = "" | |
| stderr: str = "" | |
| def cmd(options: CmdOptions) -> CmdResult: | |
| kwargs: dict[str, Any] = { | |
| "capture_output": True, | |
| "text": True, | |
| "cwd": options.working_dir or None, | |
| } | |
| if _IS_WINDOWS and options.hide_window: | |
| si = subprocess.STARTUPINFO() | |
| si.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |
| si.wShowWindow = 0 | |
| kwargs["startupinfo"] = si | |
| proc = subprocess.run([options.command] + options.args, **kwargs) | |
| return CmdResult( | |
| options=options, | |
| exit_code=proc.returncode, | |
| stdout=proc.stdout.strip(), | |
| stderr=proc.stderr.strip(), | |
| ) | |
| def quick_cmd(command: str, args: list[str]) -> None: | |
| result = cmd(CmdOptions(command=command, args=args, hide_window=True)) | |
| if result.exit_code != 0: | |
| msg = f"exit code {result.exit_code}" | |
| if result.stderr: | |
| msg += f"\n{result.stderr}" | |
| raise RuntimeError(msg) | |
| # --------------------------------------------------------------------------- | |
| # pkg/registry | |
| # --------------------------------------------------------------------------- | |
| MAX_BACKUP = 19 | |
| class Key: | |
| """Windows registry key wrapper (mirrors registry/key.go).""" | |
| def __init__(self, key: str, arch: str = "64", default: str = ""): | |
| self.key = key | |
| self.arch = arch | |
| self.default = default | |
| def _reg_args(self, *extra) -> list[str]: | |
| return [*extra, self.key, f"/reg:{self.arch}"] | |
| def add(self, force: bool = False) -> None: | |
| args = self._reg_args("add") | |
| if self.default: | |
| args += ["/d", self.default] | |
| if force: | |
| args.append("/f") | |
| quick_cmd("reg", args) | |
| def delete(self, force: bool = False) -> None: | |
| args = self._reg_args("delete") | |
| if force: | |
| args.append("/f") | |
| quick_cmd("reg", args) | |
| def exists(self) -> bool: | |
| result = cmd(CmdOptions("reg", self._reg_args("query"), hide_window=True)) | |
| return result.exit_code == 0 | |
| def export(self, file: str) -> None: | |
| if not self.exists(): | |
| return | |
| quick_cmd("reg", ["export", self.key, file, "/y", f"/reg:{self.arch}"]) | |
| reg_dir = Path(file).parent | |
| reg_files = sorted(p for p in reg_dir.iterdir() if p.suffix != ".reg" and p.is_file()) | |
| while len(reg_files) > MAX_BACKUP: | |
| reg_files[0].unlink() | |
| reg_files = reg_files[1:] | |
| def import_reg(self, file: str) -> None: | |
| import time | |
| self.export(f"{file}.{time.strftime('%Y%m%d%H%M%S')}") | |
| if not Path(file).exists(): | |
| raise FileNotFoundError(f"reg file {file} not found") | |
| quick_cmd("reg", ["import", file, f"/reg:{self.arch}"]) | |
| def open(self): | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| import winreg | |
| hives = { | |
| "HKCR": winreg.HKEY_CLASSES_ROOT, | |
| "HKCU": winreg.HKEY_CURRENT_USER, | |
| "HKLM": winreg.HKEY_LOCAL_MACHINE, | |
| "HKU": winreg.HKEY_USERS, | |
| "HKCC": winreg.HKEY_CURRENT_CONFIG, | |
| } | |
| hive, subkey = self.key.split("\\", 1) | |
| if hive not in hives: | |
| raise ValueError(f"unknown hive {hive}") | |
| return winreg.OpenKey(hives[hive], subkey, 0, winreg.KEY_ALL_ACCESS) | |
| # --------------------------------------------------------------------------- | |
| # pkg/shortcut | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class ShortcutProperty: | |
| value: str = "" | |
| clear: bool = False | |
| @dataclass | |
| class Shortcut: | |
| shortcut_path: str = "" | |
| target_path: str = "" | |
| arguments: ShortcutProperty = field(default_factory=ShortcutProperty) | |
| description: ShortcutProperty = field(default_factory=ShortcutProperty) | |
| icon_location: ShortcutProperty = field(default_factory=ShortcutProperty) | |
| working_directory: ShortcutProperty = field(default_factory=ShortcutProperty) | |
| def create_shortcut(shortcut: Shortcut) -> None: | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| import pythoncom | |
| import win32com.client | |
| pythoncom.CoInitialize() | |
| try: | |
| shell = win32com.client.Dispatch("WScript.Shell") | |
| lnk = shell.CreateShortcut(shortcut.shortcut_path) | |
| lnk.TargetPath = shortcut.target_path | |
| if shortcut.arguments.value or shortcut.arguments.clear: | |
| lnk.Arguments = shortcut.arguments.value | |
| if shortcut.description.value or shortcut.description.clear: | |
| lnk.Description = shortcut.description.value | |
| if shortcut.icon_location.value or shortcut.icon_location.clear: | |
| lnk.IconLocation = shortcut.icon_location.value | |
| if shortcut.working_directory.value or shortcut.working_directory.clear: | |
| lnk.WorkingDirectory = shortcut.working_directory.value | |
| lnk.Save() | |
| finally: | |
| pythoncom.CoUninitialize() | |
| # --------------------------------------------------------------------------- | |
| # pkg/utl | |
| # --------------------------------------------------------------------------- | |
| def set_file_attributes(path: str, attrs: int) -> None: | |
| """Set Windows file attributes.""" | |
| if not _IS_WINDOWS: | |
| raise NotImplementedError | |
| ctypes.windll.kernel32.SetFileAttributesW(path, attrs) | |
| def copy_file(src: str, dest: str) -> None: | |
| shutil.copy2(src, dest) | |
| def copy_folder(source: str, dest: str) -> None: | |
| shutil.copytree(source, dest, dirs_exist_ok=True) | |
| def remove_contents(directory: str) -> None: | |
| for item in Path(directory).iterdir(): | |
| if item.is_dir(): | |
| shutil.rmtree(item) | |
| else: | |
| item.unlink() | |
| def create_folder(*parts: str) -> str: | |
| folder = Path(*parts) | |
| folder.mkdir(parents=True, exist_ok=True) | |
| return str(folder) | |
| def create_file(path: str, content: str) -> None: | |
| Path(path).write_text(content) | |
| def format_unix_path(path: str) -> str: | |
| return path.replace("\\", "/") | |
| def format_windows_path(path: str) -> str: | |
| return path.replace("/", "\\") | |
| def exists(name: str) -> bool: | |
| return Path(name).exists() | |
| def write_to_file(name: str, content: str) -> None: | |
| Path(name).write_text(content) | |
| def append_to_file(name: str, content: str) -> None: | |
| with open(name, "a") as f: | |
| f.write(content) | |
| def file_contains(name: str, text: str) -> bool: | |
| return text in Path(name).read_text() | |
| def replace_by_prefix(filename: str, prefix: str, replace: str) -> None: | |
| lines = Path(filename).read_text().splitlines() | |
| lines = [replace if line.startswith(prefix) else line for line in lines] | |
| Path(filename).write_text("\n".join(lines)) | |
| def replace_in_file(filename: str, old: str, new: str) -> None: | |
| Path(filename).write_text(Path(filename).read_text().replace(old, new)) | |
| def is_dir_empty(name: str) -> bool: | |
| return not any(Path(name).iterdir()) | |
| def roaming_path() -> str: | |
| return os.environ.get("APPDATA", "") | |
| def start_menu_path() -> str: | |
| return str(Path(roaming_path()) / "Microsoft" / "Windows" / "Start Menu" / "Programs") | |
| def cleanup(folders: list[str]) -> None: | |
| for folder in folders: | |
| try: | |
| shutil.rmtree(folder, ignore_errors=True) | |
| except Exception as e: | |
| log.error("Cannot cleanup %s: %s", folder, e) | |
| def download_file(filepath: str, url: str) -> None: | |
| with requests.get(url, timeout=30, stream=True) as r: | |
| r.raise_for_status() | |
| with open(filepath, "wb") as f: | |
| for chunk in r.iter_content(chunk_size=8192): | |
| f.write(chunk) | |
| def find_electron_app_folder(prefix: str, source: str) -> str: | |
| for entry in Path(source).iterdir(): | |
| if entry.is_dir() and entry.name.startswith(prefix): | |
| return entry.name | |
| raise FileNotFoundError(f"Electron main path does not exist with prefix '{prefix}' in {source}") | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class CommonConfig: | |
| disable_log: bool = False | |
| args: list[str] = field(default_factory=list) | |
| env: dict[str, str] = field(default_factory=dict) | |
| app_path: str = "" | |
| @dataclass | |
| class Config: | |
| common: CommonConfig = field(default_factory=CommonConfig) | |
| app: Any = None | |
| # --------------------------------------------------------------------------- | |
| # App | |
| # --------------------------------------------------------------------------- | |
| @dataclass | |
| class AppInfo: | |
| id: str = "" | |
| guid: str = "" | |
| name: str = "" | |
| version: str = "" | |
| release: str = "" | |
| date: str = "" | |
| publisher: str = "" | |
| url: str = "" | |
| portapps_version: str = "" | |
| @dataclass | |
| class AppPrev: | |
| info: AppInfo = field(default_factory=AppInfo) | |
| win_version: WinVersion = field(default_factory=WinVersion) | |
| root_path: str = "" | |
| app_path: str = "" | |
| data_path: str = "" | |
| class App: | |
| def __init__(self, id: str, name: str, app_cfg: Any = None): | |
| self.id = id | |
| self.name = name | |
| self.info = AppInfo() | |
| self.prev = AppPrev() | |
| self.args: list[str] = [] | |
| self.process: str = "" | |
| self._logfile = None | |
| self.config = None | |
| self.app_path = "" | |
| self.data_path = "" | |
| self.working_dir = "" | |
| # WinVersion | |
| try: | |
| self.win_version = get_win_version() | |
| except Exception as e: | |
| self.fatal_box(f"Cannot get Windows version: {e}") | |
| # Root path | |
| try: | |
| ex = Path(sys.argv[0]).resolve() | |
| self.root_path = str(ex.parent) | |
| except Exception as e: | |
| self.fatal_box(f"Cannot get root absolute path: {e}") | |
| # Load info | |
| info_file = Path(self.root_path) / "portapp.json" | |
| try: | |
| with open(info_file) as f: | |
| data = json.load(f) | |
| self.info = AppInfo(**{k: data.get(k, "") for k in AppInfo.__dataclass_fields__}) | |
| except FileNotFoundError as e: | |
| self.fatal_box(f"Cannot load portapps.json: {e}") | |
| except json.JSONDecodeError as e: | |
| self.fatal_box(f"Cannot unmarshal portapps.json: {e}") | |
| # Load config | |
| try: | |
| self.config = self._load_config(app_cfg) | |
| except Exception as e: | |
| self.fatal_box(f"Cannot load configuration: {e}") | |
| if app_cfg is not None and isinstance(app_cfg, dict): | |
| try: | |
| app_cfg.update(self.config.app or {}) | |
| except Exception as e: | |
| self.fatal_box(f"Cannot decode {self.name} configuration: {e}") | |
| # Init logger | |
| try: | |
| self._init_logger() | |
| except Exception as e: | |
| self.fatal_box(f"Cannot configure logger: {e}") | |
| # Startup | |
| log.info("--------") | |
| log.info("Operating System: Windows %d.%d.%d", | |
| self.win_version.major, self.win_version.minor, self.win_version.build) | |
| log.info("Starting %s %s-%s (portapps %s)...", | |
| self.name, self.info.version, self.info.release, self.info.portapps_version) | |
| log.info("Release date: %s", self.info.date) | |
| log.info("Publisher: %s (%s)", self.info.publisher, self.info.url) | |
| log.info("Root path: %s", self.root_path) | |
| # Display config | |
| b = yaml.dump(self._config_dict()) | |
| log.info("Configuration:\n%s", b) | |
| # Set paths | |
| self.app_path = str(Path(self.root_path) / "app") | |
| if self.config.common.app_path: | |
| self.app_path = self.config.common.app_path | |
| self.data_path = str(Path(self.root_path) / "data") | |
| self.working_dir = self.app_path | |
| # Load previous | |
| prev_file = Path(self.root_path) / "portapp-prev.json" | |
| if prev_file.exists(): | |
| try: | |
| with open(prev_file) as f: | |
| p = json.load(f) | |
| except (OSError, IOError) as e: | |
| self.fatal_box(f"Cannot load portapp-prev: {e}") | |
| except json.JSONDecodeError as e: | |
| log.error("Cannot unmarshal portapp-prev") | |
| prev_file.unlink(missing_ok=True) | |
| else: | |
| wv = p.get("win_version", {}) | |
| self.prev = AppPrev( | |
| info=AppInfo(**{k: p.get("info", {}).get(k, "") for k in AppInfo.__dataclass_fields__}), | |
| win_version=WinVersion(**wv) if wv else WinVersion(), | |
| root_path=p.get("root_path", ""), | |
| app_path=p.get("app_path", ""), | |
| data_path=p.get("data_path", ""), | |
| ) | |
| # Load env vars from config | |
| if len(self.config.common.env) > 0: | |
| log.info("Setting environment variables from config...") | |
| for key, value in self.config.common.env.items(): | |
| os.environ[key] = self._expand(value) | |
| # --- config loading --- | |
| def _load_config(self, app_cfg: Any) -> Config: | |
| cfg_path = Path(self.root_path) / f"{self.id}.yml" | |
| # Initialize with defaults | |
| config = Config( | |
| common=CommonConfig( | |
| disable_log=False, | |
| args=[], | |
| env={}, | |
| app_path="", | |
| ), | |
| app=app_cfg, | |
| ) | |
| # Write sample config | |
| sample_path = Path(self.root_path) / f"{self.id}.sample.yml" | |
| raw = yaml.dump(self._config_to_dict(config)) | |
| sample_path.write_text(raw) | |
| # Skip if config file not found | |
| if not cfg_path.exists(): | |
| return config | |
| # Read config | |
| raw_data = yaml.safe_load(cfg_path.read_text()) | |
| if raw_data is None: | |
| return config | |
| # Unmarshal into config | |
| if "common" in raw_data: | |
| cr = raw_data["common"] | |
| config.common = CommonConfig( | |
| disable_log=cr.get("disable_log", False), | |
| args=cr.get("args", []), | |
| env=cr.get("env", {}), | |
| app_path=cr.get("app_path", ""), | |
| ) | |
| if "app" in raw_data: | |
| config.app = raw_data["app"] | |
| return config | |
| def _config_to_dict(self, config: Config) -> dict: | |
| return { | |
| "common": { | |
| "disable_log": config.common.disable_log, | |
| "args": config.common.args, | |
| "env": config.common.env, | |
| "app_path": config.common.app_path, | |
| }, | |
| "app": config.app, | |
| } | |
| def _config_dict(self) -> dict: | |
| return self._config_to_dict(self.config) | |
| # --- logger --- | |
| def _init_logger(self) -> None: | |
| if self.config.common.disable_log: | |
| logging.disable(logging.CRITICAL) | |
| return | |
| log_folder = Path(self.root_path) / "log" | |
| log_folder.mkdir(exist_ok=True) | |
| log_path = log_folder / f"{self.id}.log" | |
| self._logfile = open(log_path, "a") | |
| fh = logging.FileHandler(log_path) | |
| fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) | |
| logging.getLogger().addHandler(fh) | |
| # Add fatal level (mirrors zerolog fatal hook) | |
| logging.addLevelName(60, "FATAL") | |
| def fatal(msg, *args, **kwargs): | |
| log.log(60, msg, *args, **kwargs) | |
| self.error_box(msg % args if args else msg) | |
| log.fatal = fatal | |
| # --- placeholder expansion --- | |
| def _expand(self, value: str) -> str: | |
| for placeholder, replacement in { | |
| "@ROOT_PATH@": self.root_path, | |
| "@APP_PATH@": self.app_path, | |
| "@DATA_PATH@": self.data_path, | |
| "@DRIVE_LETTER@": self.root_path[0], | |
| }.items(): | |
| value = value.replace(placeholder, replacement) | |
| return value | |
| # --- dialogs --- | |
| def error_box(self, msg: Any) -> None: | |
| msg_box(f"{self.name} portable", str(msg), MB_OK | MB_ICONERROR) | |
| def error_box_log(self, msg: Any) -> None: | |
| log.error("%s", msg) | |
| self.error_box(msg) | |
| def fatal_box(self, msg: Any) -> None: | |
| self.error_box(msg) | |
| sys.exit(1) | |
| def fatal_box_log(self, msg: Any) -> None: | |
| log.error("%s", msg) | |
| self.fatal_box(msg) | |
| # --- electron --- | |
| def electron_app_path(self) -> str: | |
| folder = find_electron_app_folder("app-", self.app_path) | |
| return str(Path(self.app_path) / folder) | |
| # --- launch --- | |
| def launch(self, extra_args: list[str] = []) -> None: | |
| log.info("Process: %s", self.process) | |
| log.info("Args (config file): %s", " ".join(self.config.common.args)) | |
| log.info("Args (cmd line): %s", " ".join(extra_args)) | |
| log.info("Args (hardcoded): %s", " ".join(self.args)) | |
| log.info("Working dir: %s", self.working_dir) | |
| log.info("App path: %s", self.app_path) | |
| log.info("Data path: %s", self.data_path) | |
| log.info("Previous path: %s", self.prev.root_path) | |
| if not Path(self.process).exists(): | |
| log.fatal("Application not found in %s", self.process) | |
| sys.exit(1) | |
| log.info("Launching %s", self.name) | |
| all_args = self.config.common.args + extra_args + self.args | |
| out = self._logfile if not self.config.common.disable_log else None | |
| log.info("Exec %s %s", self.process, " ".join(all_args)) | |
| try: | |
| result = subprocess.run([self.process] + all_args, cwd=self.working_dir, stdout=out, stderr=out) | |
| if result.returncode != 0: | |
| log.fatal("Command failed") | |
| sys.exit(result.returncode) | |
| except Exception as e: | |
| log.fatal("Command failed: %s", e) | |
| sys.exit(1) | |
| # --- close --- | |
| def close(self) -> None: | |
| log.info("Closing %s", self.name) | |
| prev_file = Path(self.root_path) / "portapp-prev.json" | |
| prev_data = { | |
| "info": self.info.__dict__, | |
| "win_version": self.win_version.__dict__, | |
| "root_path": self.root_path, | |
| "app_path": self.app_path, | |
| "data_path": self.data_path, | |
| } | |
| try: | |
| json_prev = json.dumps(prev_data, indent=2) | |
| except Exception as e: | |
| log.error("Cannot marshal portapp-prev") | |
| return | |
| try: | |
| prev_file.write_text(json_prev) | |
| except Exception as e: | |
| log.error("Cannot write portapp-prev") | |
| if self._logfile: | |
| self._logfile.close() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # Create portapp.json before App init | |
| root_path = Path(sys.argv[0]).resolve().parent | |
| portapp_file = root_path / "portapp.json" | |
| if not portapp_file.exists(): | |
| portapp_file.write_text(json.dumps({ | |
| "id": "antigravity", | |
| "guid": "", | |
| "name": "Antigravity", | |
| "version": "1.0.0", | |
| "release": "1", | |
| "date": "2026-05-06", | |
| "publisher": "Antigravity", | |
| "url": "", | |
| "portapps_version": "3.0.0", | |
| "antigravity_folder": "vscodium" | |
| }, indent=2)) | |
| from libportable import App | |
| cfg = {"cleanup": False} | |
| app = App("antigravity", "Antigravity", cfg) | |
| with open(portapp_file) as f: | |
| portapp = json.load(f) | |
| Path(app.data_path).mkdir(parents=True, exist_ok=True) | |
| app.process = str(Path(app.root_path) / portapp["antigravity_folder"] / "antigravity.exe") | |
| app.working_dir = str(Path(app.root_path) / portapp["antigravity_folder"]) | |
| os.environ["VSCODE_APPDATA"] = str(Path(app.data_path) / "appdata") | |
| if not app.config.common.disable_log: | |
| os.environ["VSCODE_LOGS"] = str(Path(app.data_path) / "logs") | |
| os.environ["VSCODE_EXTENSIONS"] = str(Path(app.data_path) / "extensions") | |
| try: | |
| app.launch(sys.argv[1:]) | |
| finally: | |
| app.close() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment