Skip to content

Commit 650039a

Browse files
committed
fix(ci): use python-build-standalone runtime for desktop packaging
1 parent 3a85efb commit 650039a

3 files changed

Lines changed: 101 additions & 113 deletions

File tree

.github/workflows/release.yml

Lines changed: 87 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,36 @@ jobs:
114114
runner: ubuntu-24.04
115115
os: linux
116116
arch: amd64
117+
pbs_target: x86_64-unknown-linux-gnu
117118
- name: linux-arm64
118119
runner: ubuntu-24.04-arm
119120
os: linux
120121
arch: arm64
122+
pbs_target: aarch64-unknown-linux-gnu
121123
- name: windows-x64
122124
runner: windows-2022
123125
os: win
124126
arch: amd64
127+
pbs_target: x86_64-pc-windows-msvc
125128
- name: windows-arm64
126129
runner: windows-11-arm
127130
os: win
128131
arch: arm64
132+
pbs_target: aarch64-pc-windows-msvc
129133
- name: macos-x64
130134
runner: macos-15-intel
131135
os: mac
132136
arch: amd64
137+
pbs_target: x86_64-apple-darwin
133138
- name: macos-arm64
134139
runner: macos-15
135140
os: mac
136141
arch: arm64
142+
pbs_target: aarch64-apple-darwin
137143
env:
138144
CSC_IDENTITY_AUTO_DISCOVERY: "false"
145+
PYTHON_BUILD_STANDALONE_RELEASE: "20260211"
146+
PYTHON_BUILD_STANDALONE_VERSION: "3.12.12"
139147
steps:
140148
- name: Checkout repository
141149
uses: actions/checkout@v6
@@ -172,6 +180,7 @@ jobs:
172180
env:
173181
RUNNER_TEMP_DIR: ${{ runner.temp }}
174182
SETUP_PYTHON_PATH: ${{ steps.setup-python.outputs.python-path }}
183+
PYTHON_BUILD_STANDALONE_TARGET: ${{ matrix.pbs_target }}
175184
run: |
176185
if [ -z "$SETUP_PYTHON_PATH" ]; then
177186
echo "actions/setup-python did not return python-path output." >&2
@@ -183,32 +192,28 @@ jobs:
183192
import shutil
184193
import subprocess
185194
import sys
186-
187-
executable = pathlib.Path(sys.executable).resolve()
188-
candidate_roots = []
189-
if sys.platform == "win32":
190-
candidate_roots.extend([executable.parent, executable.parent.parent])
191-
else:
192-
candidate_roots.extend([executable.parent.parent, executable.parent])
193-
194-
def is_runtime_root(root: pathlib.Path) -> bool:
195-
if sys.platform == "win32":
196-
return (root / "python.exe").is_file() or (
197-
root / "Scripts" / "python.exe"
198-
).is_file()
199-
return (root / "bin" / "python3").is_file() or (
200-
root / "bin" / "python"
201-
).is_file()
202-
203-
source_runtime_root = next(
204-
(candidate for candidate in candidate_roots if is_runtime_root(candidate)),
205-
None,
206-
)
207-
if source_runtime_root is None:
195+
import tarfile
196+
import time
197+
import urllib.parse
198+
import urllib.request
199+
200+
release = (os.environ.get("PYTHON_BUILD_STANDALONE_RELEASE") or "").strip()
201+
version = (os.environ.get("PYTHON_BUILD_STANDALONE_VERSION") or "").strip()
202+
target = (os.environ.get("PYTHON_BUILD_STANDALONE_TARGET") or "").strip()
203+
if not release or not version or not target:
208204
raise RuntimeError(
209-
f"Cannot resolve runtime root from executable: {executable}"
205+
"Missing python-build-standalone selection envs: "
206+
"PYTHON_BUILD_STANDALONE_RELEASE / PYTHON_BUILD_STANDALONE_VERSION / PYTHON_BUILD_STANDALONE_TARGET."
210207
)
211208
209+
asset_name = (
210+
f"cpython-{version}+{release}-{target}-install_only_stripped.tar.gz"
211+
)
212+
asset_url = (
213+
"https://github.com/astral-sh/python-build-standalone/releases/download/"
214+
f"{release}/{urllib.parse.quote(asset_name)}"
215+
)
216+
212217
target_runtime_root = (
213218
pathlib.Path(
214219
os.environ.get("RUNNER_TEMP_DIR") or os.environ["RUNNER_TEMP"]
@@ -217,99 +222,50 @@ jobs:
217222
)
218223
if target_runtime_root.exists():
219224
shutil.rmtree(target_runtime_root)
225+
226+
download_archive_path = pathlib.Path(
227+
os.environ.get("RUNNER_TEMP_DIR") or os.environ["RUNNER_TEMP"]
228+
) / asset_name
229+
extract_root = pathlib.Path(
230+
os.environ.get("RUNNER_TEMP_DIR") or os.environ["RUNNER_TEMP"]
231+
) / "astrbot-cpython-runtime-extract"
232+
if extract_root.exists():
233+
shutil.rmtree(extract_root)
234+
extract_root.mkdir(parents=True, exist_ok=True)
235+
236+
last_error = None
237+
for attempt in range(1, 4):
238+
try:
239+
with urllib.request.urlopen(asset_url, timeout=180) as response:
240+
with download_archive_path.open("wb") as output:
241+
shutil.copyfileobj(response, output)
242+
break
243+
except Exception as exc:
244+
last_error = exc
245+
if attempt >= 3:
246+
raise RuntimeError(
247+
f"Failed to download python-build-standalone asset: {asset_url}"
248+
) from exc
249+
time.sleep(attempt * 2)
250+
if last_error and not download_archive_path.exists():
251+
raise RuntimeError(
252+
f"Failed to download python-build-standalone asset: {asset_url}"
253+
) from last_error
254+
255+
with tarfile.open(download_archive_path, "r:gz") as archive:
256+
archive.extractall(extract_root)
257+
258+
source_runtime_root = extract_root / "python"
259+
if not source_runtime_root.is_dir():
260+
raise RuntimeError(
261+
"Invalid python-build-standalone archive layout: missing top-level python/ directory."
262+
)
220263
shutil.copytree(
221264
source_runtime_root,
222265
target_runtime_root,
223266
symlinks=sys.platform != "win32",
224267
)
225268
226-
if sys.platform == "darwin":
227-
probe_binary = target_runtime_root / "bin" / "python3"
228-
if not probe_binary.is_file():
229-
raise RuntimeError(
230-
f"Cannot find runtime probe binary: {probe_binary}"
231-
)
232-
233-
probe = subprocess.run(
234-
["otool", "-L", str(probe_binary)],
235-
stdout=subprocess.PIPE,
236-
stderr=subprocess.PIPE,
237-
text=True,
238-
check=False,
239-
)
240-
if probe.returncode != 0:
241-
raise RuntimeError(
242-
f"Failed to inspect runtime binary by otool: {probe.stderr.strip()}"
243-
)
244-
245-
old_dependency = None
246-
for line in probe.stdout.splitlines()[1:]:
247-
candidate = line.strip().split(" (", 1)[0]
248-
if (
249-
candidate.startswith(
250-
"/Library/Frameworks/Python.framework/Versions/"
251-
)
252-
and candidate.endswith("/Python")
253-
):
254-
old_dependency = candidate
255-
break
256-
257-
if old_dependency:
258-
patched = 0
259-
for file_path in target_runtime_root.rglob("*"):
260-
if not file_path.is_file():
261-
continue
262-
file_mode = file_path.stat().st_mode
263-
if (
264-
file_path.name != "Python"
265-
and file_path.suffix not in {".dylib", ".so"}
266-
and (file_mode & 0o111) == 0
267-
):
268-
continue
269-
inspected = subprocess.run(
270-
["otool", "-L", str(file_path)],
271-
stdout=subprocess.PIPE,
272-
stderr=subprocess.DEVNULL,
273-
text=True,
274-
check=False,
275-
)
276-
if (
277-
inspected.returncode != 0
278-
or old_dependency not in inspected.stdout
279-
):
280-
continue
281-
282-
relative_libpython = pathlib.Path(
283-
os.path.relpath(
284-
target_runtime_root / "Python", file_path.parent
285-
)
286-
).as_posix()
287-
new_dependency = f"@loader_path/{relative_libpython}"
288-
289-
subprocess.run(
290-
[
291-
"install_name_tool",
292-
"-change",
293-
old_dependency,
294-
new_dependency,
295-
str(file_path),
296-
],
297-
check=True,
298-
)
299-
subprocess.run(
300-
["codesign", "--force", "--sign", "-", str(file_path)],
301-
check=True,
302-
stdout=subprocess.DEVNULL,
303-
stderr=subprocess.DEVNULL,
304-
)
305-
patched += 1
306-
307-
if patched == 0:
308-
raise RuntimeError(
309-
"Detected absolute Python.framework linkage on macOS runtime, "
310-
"but failed to patch any binary."
311-
)
312-
313269
if sys.platform == "win32":
314270
verify_candidates = [
315271
target_runtime_root / "python.exe",
@@ -338,15 +294,33 @@ jobs:
338294
)
339295
if verify.returncode != 0:
340296
raise RuntimeError(
341-
"Relocated runtime probe failed: "
297+
"Packaged runtime probe failed: "
342298
+ (
343299
verify.stderr.strip()
344300
or verify.stdout.strip()
345301
or f"exit={verify.returncode}"
346302
)
347303
)
348304
305+
ssl_verify = subprocess.run(
306+
[str(verify_binary), "-c", "import ssl"],
307+
stdout=subprocess.PIPE,
308+
stderr=subprocess.PIPE,
309+
text=True,
310+
check=False,
311+
)
312+
if ssl_verify.returncode != 0:
313+
raise RuntimeError(
314+
"Packaged runtime ssl probe failed: "
315+
+ (
316+
ssl_verify.stderr.strip()
317+
or ssl_verify.stdout.strip()
318+
or f"exit={ssl_verify.returncode}"
319+
)
320+
)
321+
349322
print(f"ASTRBOT_DESKTOP_CPYTHON_HOME={target_runtime_root}")
323+
print(f"ASTRBOT_DESKTOP_CPYTHON_ASSET={asset_name}")
350324
PY
351325
352326
- name: Setup pnpm
@@ -430,15 +404,15 @@ jobs:
430404
)
431405
432406
module_check = subprocess.run(
433-
[str(runtime_python), "-c", "import aiohttp"],
407+
[str(runtime_python), "-c", "import ssl, aiohttp"],
434408
stdout=subprocess.PIPE,
435409
stderr=subprocess.PIPE,
436410
text=True,
437411
check=False,
438412
)
439413
if module_check.returncode != 0:
440414
raise RuntimeError(
441-
"Packaged runtime dependency smoke test failed (import aiohttp): "
415+
"Packaged runtime dependency smoke test failed (import ssl, aiohttp): "
442416
+ (module_check.stderr.strip() or module_check.stdout.strip())
443417
)
444418

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ paru -S astrbot-git
150150

151151
桌面端(Electron 打包,`pnpm` 工作流)构建流程请参阅:[`desktop/README.md`](desktop/README.md)
152152
打包前需要准备 CPython 运行时目录,并设置 `ASTRBOT_DESKTOP_CPYTHON_HOME`(详见该文档)。
153+
建议使用 `python-build-standalone``install_only` 运行时作为分发基线。
153154

154155
## 支持的消息平台
155156

desktop/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ pnpm --dir desktop install --frozen-lockfile
4040
pnpm --dir desktop run dist:full
4141
```
4242

43+
Recommended runtime source for local packaging is `python-build-standalone` (same family used in CI):
44+
45+
```bash
46+
PBS_RELEASE=20260211
47+
PBS_VERSION=3.12.12
48+
PBS_TARGET=aarch64-apple-darwin # e.g. x86_64-apple-darwin / x86_64-unknown-linux-gnu / x86_64-pc-windows-msvc
49+
RUNTIME_BASE="$HOME/.cache/astrbot-python-runtime/$PBS_TARGET-$PBS_VERSION"
50+
mkdir -p "$RUNTIME_BASE"
51+
curl -L "https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_VERSION}%2B${PBS_RELEASE}-${PBS_TARGET}-install_only_stripped.tar.gz" \
52+
| tar -xzf - -C "$RUNTIME_BASE"
53+
export ASTRBOT_DESKTOP_CPYTHON_HOME="$RUNTIME_BASE/python"
54+
```
55+
4356
`ASTRBOT_DESKTOP_CPYTHON_HOME` must point to a standalone/distributable CPython runtime directory.
4457
Virtual environments (for example `.venv`, detected by `pyvenv.cfg`) are not supported for packaged runtime builds.
4558

0 commit comments

Comments
 (0)