Skip to content

Commit f8ebaa9

Browse files
committed
claude review on linux build
1 parent 6832591 commit f8ebaa9

7 files changed

Lines changed: 204 additions & 62 deletions

File tree

.github/workflows/build-linux.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,12 @@ jobs:
215215
)
216216
missing=0
217217
for f in "${required[@]}"; do
218-
if [ ! -e "$f" ]; then echo "::error::Missing or empty: $f"; missing=1; fi
218+
# Flag a file that is missing OR a zero-byte/truncated download. The
219+
# size test only applies to regular files (directory entries in the
220+
# list pass on existence alone).
221+
if [ ! -e "$f" ] || { [ -f "$f" ] && [ ! -s "$f" ]; }; then
222+
echo "::error::Missing or empty: $f"; missing=1
223+
fi
219224
done
220225
if [ -z "$(find model/huggingface/hub/models--roberta-base -name tokenizer.json -print -quit)" ]; then
221226
echo "::error::roberta-base tokenizer.json missing after HF-cache prune"; missing=1

.github/workflows/pr-test-link.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ jobs:
149149
// Upsert the managed block, leaving the author's text untouched.
150150
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
151151
let body = pr.body || '';
152+
153+
// Strip any legacy block left by the old in-build step (it used a
154+
// different marker), so renaming the markers does not orphan a stale,
155+
// never-updated block on in-flight PRs.
156+
const LEGACY_START = '<!-- macos-test-build:start -->';
157+
const LEGACY_END = '<!-- macos-test-build:end -->';
158+
const ls = body.indexOf(LEGACY_START);
159+
const le = body.indexOf(LEGACY_END);
160+
if (ls !== -1 && le !== -1 && le > ls) {
161+
body = (body.slice(0, ls) + body.slice(le + LEGACY_END.length)).replace(/\s+$/, '');
162+
}
163+
152164
const s = body.indexOf(START);
153165
const e = body.indexOf(END);
154166
if (s !== -1 && e !== -1 && e > s) {

linux/build.sh

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,31 +95,29 @@ if [ "$UNAME_ARCH" != "x86_64" ]; then
9595
echo "::error::Expected bundled PostgreSQL (pgsql/bin) in $BUNDLE (aarch64 build)"; exit 1
9696
fi
9797
else
98-
# x86_64: repair the bundled pgserver (pgserver wheel) PostgreSQL tree, which
99-
# PyInstaller damages in two independent ways -- both confirmed to break
100-
# startup, and both fixed by overlaying the pristine wheel tree below:
98+
# x86_64: repair the bundled pgserver (pgserver wheel) PostgreSQL tree.
10199
#
102-
# 1. STRIPPED EXECUTABLES. The spec sets strip=True to shrink the unstripped
103-
# scipy/numpy/onnx/PyAV wheels (hundreds of MB). But pgserver's binaries
104-
# were already processed by auditwheel/patchelf (mangled libpq soname +
105-
# injected RPATH); running GNU `strip` over a patchelf-modified ELF
106-
# corrupts it, so the libpq-linked executables (initdb, psql, pg_dump,
107-
# pg_restore) SIGSEGV at load time -- initdb dies before it can create
108-
# the cluster and the whole supervisor startup fails.
100+
# The spec pulls the tree in via collect_data_files('pgserver'), but that
101+
# helper EXCLUDES shared libraries, so every loadable module under
102+
# pginstall/lib/postgresql is dropped: plpgsql.so, vector.so (pgvector),
103+
# dict_snowball.so (initdb's post-bootstrap text-search setup loads it ->
104+
# initdb fails without it), pgoutput, the encoding converters, etc. Without
105+
# them initdb cannot create the cluster and supervisor startup fails.
109106
#
110-
# 2. MISSING LOADABLE MODULES. The spec pulls the tree in via
111-
# collect_data_files('pgserver'), but that helper excludes shared
112-
# libraries, so every loadable module under pginstall/lib/postgresql is
113-
# dropped: plpgsql.so, vector.so (pgvector), dict_snowball.so (initdb's
114-
# post-bootstrap text-search setup loads it -> initdb fails even once it
115-
# no longer segfaults), pgoutput, the encoding converters, etc.
107+
# NOTE: the executables themselves are fine -- the spec sets strip=False
108+
# (stripping is disabled on Linux precisely because it corrupts pgserver's
109+
# patchelf-modified ELFs; see AudioMuse-AI.spec), and collect_data_files
110+
# copies binaries verbatim, so the only thing actually missing is the .so
111+
# modules above. Do NOT re-enable strip=True to "shrink" the bundle: that is
112+
# what would corrupt initdb/psql/pg_dump and make them SIGSEGV at load.
116113
#
117114
# Overlay the COMPLETE, pristine pginstall tree from the installed wheel onto
118-
# the bundle. cp merges: it overwrites the corrupted executables and adds the
119-
# missing .so modules, while leaving the vendored unaccent/pg_trgm contrib the
120-
# spec grafted in (those are not in the wheel). Also refresh the external libs
121-
# (pgserver.libs/). Must run inside the build venv where pgserver is
122-
# importable (the CI step and build.sh's own usage both activate it).
115+
# the bundle. cp merges: it adds the missing .so modules (and harmlessly
116+
# re-copies the already-correct executables), while leaving the vendored
117+
# unaccent/pg_trgm contrib the spec grafted in (those are not in the wheel).
118+
# Also refresh the external libs (pgserver.libs/). Must run inside the build
119+
# venv where pgserver is importable (the CI step and build.sh's own usage both
120+
# activate it).
123121
PG_PKG="$(python -c 'import os, pgserver; print(os.path.dirname(os.path.abspath(pgserver.__file__)))')"
124122
PG_SITE="$(dirname "$PG_PKG")"
125123
DST_PGINSTALL="$BUNDLE/_internal/pgserver/pginstall"

linux/embedded_pg.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import logging
2020
import os
21+
import shutil
2122
import subprocess
2223
import threading
2324

@@ -28,6 +29,12 @@
2829
_lock = threading.RLock()
2930
_data_dir = None # remembered so stop() can target the right cluster
3031

32+
# Written only after initdb AND our config edits both succeed, so its presence
33+
# proves a *complete* cluster. PG_VERSION alone is not enough: initdb writes it
34+
# partway through, so a crash/SIGKILL mid-initdb leaves a half-built dir that
35+
# looks initialized forever and wedges every later start.
36+
_READY_MARKER = "audiomuse_initialized"
37+
3138

3239
def _pg_env():
3340
"""Environment for the bundled Postgres tools.
@@ -56,8 +63,41 @@ def _bin(name):
5663

5764

5865
def _initialized(data_dir):
59-
# PG_VERSION exists only after a successful initdb.
60-
return os.path.exists(os.path.join(data_dir, "PG_VERSION"))
66+
if os.path.exists(os.path.join(data_dir, _READY_MARKER)):
67+
return True
68+
# Legacy clusters created before the completion marker existed: adopt them if
69+
# initdb actually finished. global/pg_control is written by initdb and is
70+
# required for the server to start, so treat it as the completeness signal,
71+
# then stamp the marker so future starts take the fast path.
72+
if os.path.exists(os.path.join(data_dir, "global", "pg_control")):
73+
try:
74+
with open(os.path.join(data_dir, _READY_MARKER), "w", encoding="utf-8") as fh:
75+
fh.write("ok\n")
76+
except OSError:
77+
pass
78+
return True
79+
return False
80+
81+
82+
def _reset_data_dir(data_dir):
83+
"""Empty a partially-initialized data dir so initdb can retry.
84+
85+
initdb refuses a non-empty target, so a half-built cluster (interrupted
86+
initdb, no completion marker) would otherwise wedge every start forever.
87+
Only ever called when :func:`_initialized` is False, so a complete cluster's
88+
data is never touched."""
89+
if not (os.path.isdir(data_dir) and os.listdir(data_dir)):
90+
return
91+
logger.warning("Clearing incomplete PostgreSQL data dir %s before re-init", data_dir)
92+
for entry in os.listdir(data_dir):
93+
target = os.path.join(data_dir, entry)
94+
try:
95+
if os.path.isdir(target) and not os.path.islink(target):
96+
shutil.rmtree(target)
97+
else:
98+
os.unlink(target)
99+
except OSError:
100+
logger.exception("Could not remove %s", target)
61101

62102

63103
def _dsn(data_dir):
@@ -100,6 +140,11 @@ def _init_cluster(data_dir, env):
100140
fh.write("\n# --- AudioMuse-AI embedded overrides ---\n")
101141
fh.write(f"unix_socket_directories = '{data_dir}'\n")
102142
fh.write("listen_addresses = ''\n")
143+
# Stamp the completion marker last: only now is the cluster fully usable, so
144+
# an interruption before this point leaves _initialized() False and triggers
145+
# a clean re-init on the next start.
146+
with open(os.path.join(data_dir, _READY_MARKER), "w", encoding="utf-8") as fh:
147+
fh.write("ok\n")
103148

104149

105150
def start(data_dir):
@@ -109,6 +154,7 @@ def start(data_dir):
109154
env = _pg_env()
110155
if not _initialized(data_dir):
111156
logger.info("Initializing embedded PostgreSQL cluster at %s", data_dir)
157+
_reset_data_dir(data_dir) # clear any half-built cluster first
112158
_init_cluster(data_dir, env)
113159
if not _is_running(data_dir, env):
114160
_run_checked([_bin("pg_ctl"), "-D", data_dir, "-w", "start"], env)

linux/launcher.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,19 @@ def _handle_signal(signum, _frame):
173173
signal.signal(signal.SIGINT, _handle_signal)
174174
signal.signal(signal.SIGTERM, _handle_signal)
175175

176-
def _boot():
177-
try:
178-
supervisor.start_all()
179-
print("AudioMuse-AI is running at %s" % WEB_URL)
180-
if open_browser:
181-
_open_browser()
182-
except Exception as exc: # startup already logged; surface a hint too
183-
print("AudioMuse-AI failed to start: %s" % exc, file=sys.stderr)
184-
stop_event.set()
185-
186-
threading.Thread(target=_boot, name="boot", daemon=True).start()
176+
def _on_ready():
177+
print("AudioMuse-AI is running at %s" % WEB_URL)
178+
if open_browser:
179+
_open_browser()
180+
181+
def _on_error(exc): # startup already logged; surface a hint too
182+
print("AudioMuse-AI failed to start: %s" % exc, file=sys.stderr)
183+
stop_event.set()
184+
185+
# The supervisor owns the boot thread so stop_all() can join it before
186+
# tearing down -- a SIGTERM racing an in-progress boot must not leave the
187+
# boot thread spawning children after the teardown sweep.
188+
supervisor.start_in_background(on_ready=_on_ready, on_error=_on_error)
187189

188190
try:
189191
while not stop_event.wait(0.5):
@@ -230,8 +232,11 @@ def _cmd_open():
230232
from linux import paths
231233
if _running_supervisor_pid(paths) is None:
232234
# Not running yet: start it in the foreground (this call becomes the
233-
# supervisor and opens the browser once it is up).
234-
return _run_supervisor(open_browser=True)
235+
# supervisor and opens the browser once it is up). Honor
236+
# AUDIOMUSE_OPEN_BROWSER like `start` does, so a headless/service
237+
# invocation does not try to spawn a browser.
238+
open_browser = os.environ.get("AUDIOMUSE_OPEN_BROWSER", "1") != "0"
239+
return _run_supervisor(open_browser=open_browser)
235240
_open_browser()
236241
return 0
237242

linux/packaging/audiomuse-ai.service

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[Unit]
22
Description=AudioMuse-AI (standalone music analysis + smart playlists)
33
Documentation=https://github.com/NeptuneHub/AudioMuse-AI
4-
After=default.target
54

65
[Service]
76
Type=simple

0 commit comments

Comments
 (0)