diff --git a/.gitea/workflows/rolling-release.yml b/.gitea/workflows/rolling-release.yml index fc7cfb8..4f19c6d 100644 --- a/.gitea/workflows/rolling-release.yml +++ b/.gitea/workflows/rolling-release.yml @@ -31,6 +31,7 @@ jobs: mkdir -p /tmp/jarvis-ci-models printf '%s\n' '{"cacheDirectory":"/tmp/jarvis-ci-models"}' > "$QVAC_CONFIG_PATH" npm run package + tar -tzf dist/jarvis-qvac-bare-linux-*.tar.gz | grep -E 'node_modules/bare-runtime-linux-.*/bin/bare$' - name: Publish rolling prerelease to Gitea env: GITEA_SERVER_URL: ${{ gitea.server_url }} diff --git a/docs/bare-runtime.md b/docs/bare-runtime.md index e6f5e80..cd05ed2 100644 --- a/docs/bare-runtime.md +++ b/docs/bare-runtime.md @@ -40,10 +40,11 @@ npm run bare:smoke npm run package:bare ``` -The installer executes `node_modules/bare-runtime-linux-*/bin/bare` directly. -The `node_modules/.bin/bare` convenience script is a Node launcher and is not -used by the service. The generated systemd unit therefore has no Node -executable in its `ExecStart` line. +The installer requires `node_modules/bare-runtime-linux-*/bin/bare` inside the +installed tree. It does not fall back to a `bare` binary on `PATH`. The user +unit starts `%h/.local/share/jarvis-qvac/packaging/jarvisd-launch.sh`, which +resolves that ELF relative to the install. The unit does not contain a +machine path, and the checkout unit under `systemd/user/` is not shipped. GPU policy remains strict. The Bare QVAC engine must report a visible GPU and the master load path passes `device: "gpu"`, full GPU layer offload, and GPU diff --git a/docs/operations.md b/docs/operations.md index e703e9a..cae2a07 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -50,11 +50,10 @@ journalctl --user -u jarvisd.service -f ``` The unit at `packaging/jarvisd.service` (installed to -`~/.config/systemd/user/jarvisd.service`) starts Bare on -`daemon/bare-entry.js` with working directory `~/.local/share/jarvis-qvac` and -`QVAC_CONFIG_PATH=~/.config/jarvis/qvac.config.json`. The repository -development unit uses the checkout path; use the packaged unit when testing -installation behavior. +`~/.config/systemd/user/jarvisd.service`) starts the bundled Bare ELF through +`packaging/jarvisd-launch.sh`. It uses `%h` for the install directory and does +not record a host path. The file `systemd/user/jarvisd.service` is a checkout +helper only and is not included in the release archive. Restart after daemon code changes: diff --git a/packaging/build-runtime-bundle.sh b/packaging/build-runtime-bundle.sh index a2f1aa8..48fe008 100755 --- a/packaging/build-runtime-bundle.sh +++ b/packaging/build-runtime-bundle.sh @@ -15,7 +15,8 @@ mkdir -p "${OUT_DIR}" BUNDLE_DIR="${OUT_DIR}/jarvis-qvac-bare-${PLATFORM}" rm -rf "${BUNDLE_DIR}" "${OUT_DIR}/jarvis-qvac-bare-${PLATFORM}.tar.gz" mkdir -p "${BUNDLE_DIR}" -tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./vendor/agent-harness/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${BUNDLE_DIR}" +# The checkout unit points at a developer tree and Node. Do not ship it. +tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./systemd' --exclude='./vendor/agent-harness/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${BUNDLE_DIR}" cp -a "${ROOT_DIR}/node_modules" "${BUNDLE_DIR}/" # QVAC publishes one package containing native prebuilds for every supported # platform. The rolling asset is architecture-specific: retain all Linux x64 @@ -31,6 +32,18 @@ fi if [[ "${JARVIS_VERIFY_VOICE_MODELS:-0}" == "1" ]]; then (cd "${BUNDLE_DIR}" && bash packaging/bare-launch.sh packaging/bare-run.js packaging/voice-model-smoke.js) fi +chmod 755 "${BUNDLE_DIR}/packaging/jarvisd-launch.sh" "${BUNDLE_DIR}/node_modules/bare-runtime-${PLATFORM}/bin/bare" tar -C "${OUT_DIR}" -czf "${OUT_DIR}/jarvis-qvac-bare-${PLATFORM}.tar.gz" "jarvis-qvac-bare-${PLATFORM}" rm -rf "${BUNDLE_DIR}" -echo "${OUT_DIR}/jarvis-qvac-bare-${PLATFORM}.tar.gz" +ARCHIVE="${OUT_DIR}/jarvis-qvac-bare-${PLATFORM}.tar.gz" +MEMBER="jarvis-qvac-bare-${PLATFORM}/node_modules/bare-runtime-${PLATFORM}/bin/bare" +if ! tar -tzf "${ARCHIVE}" | grep -qx "${MEMBER}"; then + echo "Release archive is missing the Bare runtime executable: ${MEMBER}" >&2 + exit 1 +fi +if tar -tzf "${ARCHIVE}" | grep -q 'systemd/user/jarvisd.service'; then + echo "Release archive still contains the developer systemd unit." >&2 + exit 1 +fi +echo "Packaged Bare runtime ${MEMBER}" +echo "${ARCHIVE}" diff --git a/packaging/install.sh b/packaging/install.sh index 5d28aa6..02ef448 100755 --- a/packaging/install.sh +++ b/packaging/install.sh @@ -114,14 +114,13 @@ case "$(uname -m)" in aarch64|arm64) BARE_BIN="${APP_DIR}/node_modules/bare-runtime-linux-arm64/bin/bare" ;; *) BARE_BIN="" ;; esac -if [[ ! -x "${BARE_BIN}" ]]; then BARE_BIN="$(command -v bare || true)"; fi if [[ -z "${BARE_BIN}" || ! -x "${BARE_BIN}" ]]; then log "error: no Bare runtime for $(uname -m) in the installed bundle" - echo "The installed bundle does not contain a Bare runtime for $(uname -m). Install the official rolling bundle or build it with npm run package:bare." >&2 + echo "The installed bundle does not contain a Bare runtime for $(uname -m). Install the official rolling bundle. A Bare binary from PATH is not used." >&2 exit 1 fi -chmod 755 "${BARE_BIN}" -log "using Bare runtime ${BARE_BIN}" +chmod 755 "${BARE_BIN}" "${ROOT_DIR}/packaging/jarvisd-launch.sh" +log "using bundled Bare runtime ${BARE_BIN}" NODE_BIN="$(command -v node || true)" if [[ -z "${NODE_BIN}" || ! -x "${NODE_BIN}" ]]; then NODE_BIN="$(bash -lc 'command -v node' 2>/dev/null || true)" @@ -193,7 +192,7 @@ activate_extension() { log "GNOME extension ${EXT_UUID} is installed but not active in this session; log out and back in, or run: gnome-extensions enable ${EXT_UUID}" fi } -sed -e "s#__JARVIS_BARE__#${BARE_BIN}#" -e "s#__JARVIS_NODE__#${NODE_BIN}#" "${ROOT_DIR}/packaging/jarvisd.service" > "${HOME}/.config/systemd/user/jarvisd.service" +sed -e "s#__JARVIS_NODE__#${NODE_BIN}#" "${ROOT_DIR}/packaging/jarvisd.service" > "${HOME}/.config/systemd/user/jarvisd.service" log "wrote ${HOME}/.config/systemd/user/jarvisd.service" DBUS_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/dbus-1/services" mkdir -p "${DBUS_DIR}" diff --git a/packaging/jarvisd-launch.sh b/packaging/jarvisd-launch.sh new file mode 100755 index 0000000..d4f3baf --- /dev/null +++ b/packaging/jarvisd-launch.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Start the Bare ELF shipped with this install. Never use a bare from PATH. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +case "$(uname -m)" in + x86_64|amd64) BIN="${ROOT}/node_modules/bare-runtime-linux-x64/bin/bare" ;; + aarch64|arm64) BIN="${ROOT}/node_modules/bare-runtime-linux-arm64/bin/bare" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 2 ;; +esac +if [[ ! -x "${BIN}" ]]; then + echo "This Jarvis install has no Bare runtime at ${BIN}." >&2 + echo "Install the official rolling bundle. A Bare binary from PATH is not used." >&2 + exit 1 +fi +exec "${BIN}" "$@" diff --git a/packaging/jarvisd.service b/packaging/jarvisd.service index cc07f8b..271a77e 100644 --- a/packaging/jarvisd.service +++ b/packaging/jarvisd.service @@ -7,7 +7,7 @@ PartOf=graphical-session.target [Service] Type=simple WorkingDirectory=%h/.local/share/jarvis-qvac -ExecStart=__JARVIS_BARE__ %h/.local/share/jarvis-qvac/daemon/bare-entry.js +ExecStart=%h/.local/share/jarvis-qvac/packaging/jarvisd-launch.sh %h/.local/share/jarvis-qvac/daemon/bare-entry.js Environment=QVAC_CONFIG_PATH=%h/.config/jarvis/qvac.config.json Environment=JARVIS_GPU_REQUIRED=1 Environment=JARVIS_QVAC_OWNER=jarvisd diff --git a/packaging/publish-gitea-release.sh b/packaging/publish-gitea-release.sh index ccab121..369bc5c 100755 --- a/packaging/publish-gitea-release.sh +++ b/packaging/publish-gitea-release.sh @@ -32,8 +32,15 @@ created="$(curl -fsS -X POST "${AUTH[@]}" "${JSON[@]}" "${API}/releases" --data release_id="$(jq -r '.id // empty' <<<"${created}")" test -n "${release_id}" +shopt -s nullglob +bare_assets=(dist/jarvis-qvac-bare-*.tar.gz) +if (( ${#bare_assets[@]} == 0 )); then + echo "Refusing to publish: the Bare runtime bundle is missing." >&2 + exit 1 +fi +shopt -u nullglob for asset in dist/jarvis-qvac_*.deb dist/jarvis-qvac-extension.zip dist/jarvis-qvac-bare-*.tar.gz dist/SHA256SUMS; do - [[ -f "${asset}" ]] || continue + [[ -f "${asset}" ]] || { echo "Refusing to publish: missing ${asset}" >&2; exit 1; } curl -fsS -X POST "${AUTH[@]}" \ "${API}/releases/${release_id}/assets?name=$(basename "${asset}")" \ -F "attachment=@${asset}" >/dev/null diff --git a/test/install.test.js b/test/install.test.js index 666901a..6dcf0ae 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -39,6 +39,7 @@ test('install.sh replaces leftover files from an older version and keeps config' await mkdir(bin, { recursive: true }); await cp(path.join(repo, 'packaging/install.sh'), path.join(root, 'packaging/install.sh')); await cp(path.join(repo, 'packaging/jarvisd.service'), path.join(root, 'packaging/jarvisd.service')); + await cp(path.join(repo, 'packaging/jarvisd-launch.sh'), path.join(root, 'packaging/jarvisd-launch.sh')); await cp(path.join(repo, 'packaging/qvac.config.template.json'), path.join(root, 'packaging/qvac.config.template.json')); await cp(path.join(repo, 'apps/gnome-extension/jarvis@qvac.local'), path.join(root, 'apps/gnome-extension/jarvis@qvac.local'), { recursive: true }); await writeFile(path.join(root, 'node_modules', bareName, 'bin', 'bare'), '#!/bin/sh\nexit 0\n'); @@ -108,7 +109,9 @@ exit 0 assert.equal(await readFile(path.join(appDir, 'daemon-marker'), 'utf8'), 'new-tree\n'); assert.equal(await readFile(path.join(configDir, 'config.json'), 'utf8'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n'); const unit = await readFile(path.join(home, '.config/systemd/user/jarvisd.service'), 'utf8'); - assert.match(unit, /bare-runtime-linux/); + const execStart = unit.split('\n').find((line) => line.startsWith('ExecStart=')); + assert.match(execStart, /ExecStart=%h\/\.local\/share\/jarvis-qvac\/packaging\/jarvisd-launch\.sh/); + assert.doesNotMatch(execStart, /\/home\/|dev\/jarvis|__JARVIS_BARE__|\/usr\/bin\/node/); assert.match(unit, /WantedBy=default\.target/); assert.match(unit, /Restart=always/); assert.doesNotMatch(unit, /ExecStart=\/usr\/bin\/node/); @@ -152,6 +155,7 @@ test('install.sh seeds default config when missing and does not require first-ru await mkdir(bin, { recursive: true }); await cp(path.join(repo, 'packaging/install.sh'), path.join(root, 'packaging/install.sh')); await cp(path.join(repo, 'packaging/jarvisd.service'), path.join(root, 'packaging/jarvisd.service')); + await cp(path.join(repo, 'packaging/jarvisd-launch.sh'), path.join(root, 'packaging/jarvisd-launch.sh')); await cp(path.join(repo, 'packaging/qvac.config.template.json'), path.join(root, 'packaging/qvac.config.template.json')); await cp(path.join(repo, 'apps/gnome-extension/jarvis@qvac.local'), path.join(root, 'apps/gnome-extension/jarvis@qvac.local'), { recursive: true }); await writeFile(path.join(root, 'node_modules', bareName, 'bin', 'bare'), '#!/bin/sh\nexit 0\n');