Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
593a3d8
fix(desktop): make frozen pip installs independent of system python
zouyonghe Feb 17, 2026
86d904c
refactor: migrate desktop backend to cpython runtime
zouyonghe Feb 17, 2026
42a151f
fix(desktop): stabilize CI runtime and windows backend cleanup
zouyonghe Feb 18, 2026
cb15255
fix: write knowledge base under astrbot data dir
zouyonghe Feb 18, 2026
6f6d45b
fix(ci): support branch-based manual release runs
zouyonghe Feb 18, 2026
3c7fd49
fix(desktop): harden runtime packaging and windows cleanup
zouyonghe Feb 18, 2026
07bb9d2
fix(desktop): validate runtime python and harden windows pid checks
zouyonghe Feb 18, 2026
a858939
fix(desktop): harden cleanup fallback and runtime version detection
zouyonghe Feb 18, 2026
fe94a0a
fix(desktop): improve runtime validation and backend cleanup
zouyonghe Feb 18, 2026
c6c238e
fix(desktop): refactor packaged backend runtime resolution
zouyonghe Feb 18, 2026
f63f960
fix(desktop): harden packaged launch fallback and cleanup matching
zouyonghe Feb 18, 2026
d70a75f
refactor(desktop): split backend runtime helpers and cleanup logic
zouyonghe Feb 18, 2026
fb2a41b
refactor(core): remove pyinstaller-specific reboot reset
zouyonghe Feb 18, 2026
4260a95
refactor(core): remove frozen-runtime legacy branches
zouyonghe Feb 18, 2026
9f99b95
docs: use venv runtime env var in desktop readme
zouyonghe Feb 18, 2026
5512afb
fix: block desktop packaged online update paths
zouyonghe Feb 18, 2026
8a1f58a
fix: use writable cwd for packaged backend runtime
zouyonghe Feb 18, 2026
0300d56
refactor: simplify desktop runtime build and cleanup flow
zouyonghe Feb 18, 2026
d5ab04b
fix: normalize nested css selectors and simplify runtime probe parsing
zouyonghe Feb 18, 2026
8761e11
fix: scope css selector normalization and simplify backend launch flow
zouyonghe Feb 18, 2026
8834425
refactor: remove legacy frozen runtime compatibility path
zouyonghe Feb 18, 2026
72791a4
refactor: inline python runtime probe parsing flow
zouyonghe Feb 18, 2026
cdb8b26
refactor: simplify desktop backend build and launch strategy flow
zouyonghe Feb 18, 2026
4ca9618
fix: avoid auto-cleanup on plugin load failure and improve reload checks
zouyonghe Feb 18, 2026
b9c0945
fix: avoid packaging virtualenv as desktop runtime
zouyonghe Feb 18, 2026
184dd4b
refactor: simplify backend launch flow and runtime probe errors
zouyonghe Feb 18, 2026
e1b0a0f
docs: add troubleshooting note for requires-python probe failures
zouyonghe Feb 18, 2026
7365cc2
refactor: streamline backend config and unmanaged cleanup flow
zouyonghe Feb 18, 2026
e9920d1
fix(ci): package relocatable cpython runtime for desktop
zouyonghe Feb 18, 2026
3108863
fix(desktop): install runtime deps into packaged python
zouyonghe Feb 18, 2026
bdc963c
fix(desktop): retry pip install for uv-managed runtime
zouyonghe Feb 18, 2026
5257548
fix(ci): use setup-python runtime source for desktop packaging
zouyonghe Feb 18, 2026
126954f
refactor(ci): remove obsolete uv fallback paths
zouyonghe Feb 18, 2026
3a85efb
refactor(desktop): remove unused electron runtime APIs
zouyonghe Feb 18, 2026
650039a
fix(ci): use python-build-standalone runtime for desktop packaging
zouyonghe Feb 18, 2026
4987911
chore(ci): remove runtime import smoke check
zouyonghe Feb 18, 2026
3d86324
fix(desktop): add windows dll search paths for bundled runtime
zouyonghe Feb 18, 2026
8e15e80
fix(desktop): harden windows dll resolution in launcher
zouyonghe Feb 18, 2026
4e30186
refactor(ci): rebuild windows desktop release jobs
zouyonghe Feb 18, 2026
b46bd76
fix(ci): avoid cryptography source build on windows arm64
zouyonghe Feb 18, 2026
908c367
fix(desktop): bundle msvc runtime for windows backend
zouyonghe Feb 18, 2026
ac27872
fix(desktop): force utf-8 backend log output on windows
zouyonghe Feb 19, 2026
669d6d8
fix: make tray backend restart always run in main process
zouyonghe Feb 19, 2026
38f8d55
fix: enforce wheel-only plugin dependency installs in packaged runtime
zouyonghe Feb 19, 2026
ae9b164
refactor: simplify backend cleanup flow and extract vite postcss plugin
zouyonghe Feb 19, 2026
bca8ab1
refactor(ci): deduplicate packaged cpython runtime resolution
zouyonghe Feb 19, 2026
6f8417b
refactor: simplify windows cleanup state and harden runtime CI checks
zouyonghe Feb 19, 2026
cd16451
fix(ci): pass github token to runtime resolver
zouyonghe Feb 19, 2026
ed53778
fix(desktop): disable blockmap outputs and add jsonschema dependency
zouyonghe Feb 19, 2026
4e749bb
refactor: simplify backend cleanup and centralize pbs mapping
zouyonghe Feb 19, 2026
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
214 changes: 214 additions & 0 deletions .github/scripts/resolve_packaged_cpython_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Resolve and verify python-build-standalone runtime for desktop packaging."""

from __future__ import annotations

import hashlib
import json
import os
import pathlib
import shutil
import subprocess
import sys
import tarfile
import time
import urllib.parse
import urllib.request


def _require_env(name: str) -> str:
value = (os.environ.get(name) or "").strip()
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value


def _download_with_retries(
url: str, output_path: pathlib.Path, retries: int = 3
) -> None:
last_error: Exception | None = None
for attempt in range(1, retries + 1):
try:
with urllib.request.urlopen(url, timeout=180) as response:
with output_path.open("wb") as output:
shutil.copyfileobj(response, output)
return
except Exception as exc: # pragma: no cover - network-path fallback
last_error = exc
if attempt >= retries:
raise RuntimeError(
f"Failed to download python-build-standalone asset: {url}"
) from exc
time.sleep(attempt * 2)

raise RuntimeError(
f"Failed to download python-build-standalone asset: {url}"
) from last_error


def _build_request(url: str) -> urllib.request.Request:
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "astrbot-release-workflow",
}
github_token = (os.environ.get("GITHUB_TOKEN") or "").strip()
if github_token:
headers["Authorization"] = f"Bearer {github_token}"
return urllib.request.Request(url, headers=headers)


def _read_json_with_retries(url: str, retries: int = 3) -> dict:
last_error: Exception | None = None
for attempt in range(1, retries + 1):
try:
request = _build_request(url)
with urllib.request.urlopen(request, timeout=60) as response:
return json.load(response)
except Exception as exc: # pragma: no cover - network-path fallback
last_error = exc
if attempt >= retries:
raise RuntimeError(f"Failed to fetch release metadata: {url}") from exc
time.sleep(attempt * 2)

raise RuntimeError(f"Failed to fetch release metadata: {url}") from last_error


def _resolve_expected_sha256(release: str, asset_name: str) -> str:
release_api_url = (
"https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/"
f"{urllib.parse.quote(release)}"
)
release_data = _read_json_with_retries(release_api_url)
assets = release_data.get("assets")
if not isinstance(assets, list):
raise RuntimeError("Invalid GitHub release metadata: missing assets list.")

matched_asset = next(
(
item
for item in assets
if isinstance(item, dict) and item.get("name") == asset_name
),
None,
)
if matched_asset is None:
raise RuntimeError(
f"Cannot find expected python-build-standalone asset in release {release}: {asset_name}"
)

digest = matched_asset.get("digest")
if not isinstance(digest, str) or not digest.startswith("sha256:"):
raise RuntimeError(
f"Release metadata does not provide sha256 digest for asset: {asset_name}"
)
return digest.split(":", 1)[1].lower()


def _calculate_sha256(file_path: pathlib.Path) -> str:
digest = hashlib.sha256()
with file_path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()


def _resolve_runtime_python(runtime_root: pathlib.Path) -> pathlib.Path:
if sys.platform == "win32":
candidates = [
runtime_root / "python.exe",
runtime_root / "Scripts" / "python.exe",
]
else:
candidates = [
runtime_root / "bin" / "python3",
runtime_root / "bin" / "python",
]

runtime_python = next(
(candidate for candidate in candidates if candidate.is_file()), None
)
if runtime_python is None:
raise RuntimeError(
f"Cannot find verification runtime binary under {runtime_root}"
)
return runtime_python


def _run_probe(runtime_python: pathlib.Path, args: list[str], label: str) -> None:
result = subprocess.run(
[str(runtime_python), *args],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
f"Packaged runtime {label} probe failed: "
+ (
result.stderr.strip()
or result.stdout.strip()
or f"exit={result.returncode}"
)
)


def main() -> None:
runner_temp_dir = os.environ.get("RUNNER_TEMP_DIR") or os.environ.get("RUNNER_TEMP")
if not runner_temp_dir:
raise RuntimeError("RUNNER_TEMP_DIR or RUNNER_TEMP must be set.")
runner_temp = pathlib.Path(runner_temp_dir)

release = _require_env("PYTHON_BUILD_STANDALONE_RELEASE")
version = _require_env("PYTHON_BUILD_STANDALONE_VERSION")
target = _require_env("PYTHON_BUILD_STANDALONE_TARGET")

asset_name = f"cpython-{version}+{release}-{target}-install_only_stripped.tar.gz"
asset_url = (
"https://github.com/astral-sh/python-build-standalone/releases/download/"
f"{release}/{urllib.parse.quote(asset_name)}"
)
expected_sha256 = _resolve_expected_sha256(release, asset_name)

target_runtime_root = runner_temp / "astrbot-cpython-runtime"
download_archive_path = runner_temp / asset_name
extract_root = runner_temp / "astrbot-cpython-runtime-extract"

if target_runtime_root.exists():
shutil.rmtree(target_runtime_root)
if extract_root.exists():
shutil.rmtree(extract_root)
extract_root.mkdir(parents=True, exist_ok=True)

_download_with_retries(asset_url, download_archive_path)
actual_sha256 = _calculate_sha256(download_archive_path)
if actual_sha256 != expected_sha256:
raise RuntimeError(
"Downloaded runtime archive sha256 mismatch: "
+ f"expected={expected_sha256} actual={actual_sha256}"
)

with tarfile.open(download_archive_path, "r:gz") as archive:
archive.extractall(extract_root)

source_runtime_root = extract_root / "python"
if not source_runtime_root.is_dir():
raise RuntimeError(
"Invalid python-build-standalone archive layout: missing top-level python/ directory."
)

shutil.copytree(
source_runtime_root,
target_runtime_root,
symlinks=sys.platform != "win32",
)

runtime_python = _resolve_runtime_python(target_runtime_root)
_run_probe(runtime_python, ["-V"], "version")
_run_probe(runtime_python, ["-c", "import ssl"], "ssl")

print(f"ASTRBOT_DESKTOP_CPYTHON_HOME={target_runtime_root}")
print(f"ASTRBOT_DESKTOP_CPYTHON_ASSET={asset_name}")


if __name__ == "__main__":
main()
42 changes: 42 additions & 0 deletions .github/scripts/resolve_pbs_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Resolve python-build-standalone target by platform and architecture."""

from __future__ import annotations

import argparse

TARGETS = {
("linux", "amd64"): "x86_64-unknown-linux-gnu",
("linux", "arm64"): "aarch64-unknown-linux-gnu",
("mac", "amd64"): "x86_64-apple-darwin",
("mac", "arm64"): "aarch64-apple-darwin",
("windows", "amd64"): "x86_64-pc-windows-msvc",
("windows", "arm64"): "aarch64-pc-windows-msvc",
}


def resolve_target(platform: str, arch: str) -> str:
key = (platform.strip().lower(), arch.strip().lower())
target = TARGETS.get(key)
if not target:
supported = ", ".join(
f"{item_platform}/{item_arch}" for item_platform, item_arch in TARGETS
)
raise RuntimeError(
f"Unsupported python-build-standalone mapping for {platform}/{arch}. "
f"Supported: {supported}"
)
return target


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--platform", required=True, help="linux/mac/windows")
parser.add_argument("--arch", required=True, help="amd64/arm64")
args = parser.parse_args()

print(resolve_target(args.platform, args.arch))


if __name__ == "__main__":
main()
102 changes: 102 additions & 0 deletions .github/scripts/smoke_test_packaged_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Run smoke checks against bundled desktop runtime python."""

from __future__ import annotations

import argparse
import pathlib
import subprocess
import sys


def _resolve_runtime_python(runtime_root: pathlib.Path) -> pathlib.Path:
if sys.platform == "win32":
candidates = [
runtime_root / "python.exe",
runtime_root / "Scripts" / "python.exe",
]
else:
candidates = [
runtime_root / "bin" / "python3",
runtime_root / "bin" / "python",
]

runtime_python = next(
(candidate for candidate in candidates if candidate.is_file()), None
)
if runtime_python is None:
raise RuntimeError(
f"Packaged runtime python executable is missing under {runtime_root}"
)
return runtime_python


def _run_command(
command: list[str], failure_message: str
) -> subprocess.CompletedProcess:
result = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
failure_message
+ (
result.stderr.strip()
or result.stdout.strip()
or f"exit={result.returncode}"
)
)
return result


def _check_runtime_dependencies(runtime_python: pathlib.Path) -> None:
if sys.platform == "darwin":
deps = _run_command(
["otool", "-L", str(runtime_python)],
"Failed to inspect macOS runtime by otool: ",
)
if "/Library/Frameworks/Python.framework/" in deps.stdout:
raise RuntimeError(
"Packaged runtime still links to absolute /Library/Frameworks/Python.framework path."
)
elif sys.platform.startswith("linux"):
deps = _run_command(
["ldd", str(runtime_python)],
"Failed to inspect Linux runtime by ldd: ",
)
if "not found" in deps.stdout:
raise RuntimeError(
"Packaged runtime has unresolved shared libraries:\n" + deps.stdout
)


def main() -> None:
parser = argparse.ArgumentParser(
description="Smoke test packaged desktop runtime python."
)
parser.add_argument(
"runtime_root",
nargs="?",
default="desktop/resources/backend/python",
help="Path to packaged runtime root directory.",
)
args = parser.parse_args()

runtime_root = pathlib.Path(args.runtime_root)
runtime_python = _resolve_runtime_python(runtime_root)

_run_command(
[str(runtime_python), "-V"], "Packaged runtime python smoke test failed: "
)
_run_command(
[str(runtime_python), "-c", "import ssl"],
"Packaged runtime ssl smoke test failed: ",
)
_check_runtime_dependencies(runtime_python)


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