mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-05-31 19:04:29 +07:00
fix(ci): always source-build agent-native and bundle to napi/ root
The Zig NAPI provisioner had a silent failure mode that affected any matrix entry without a matching ZSeven-W/agent prebuilt: the source- build fallback dropped `agent_napi.node` at `zig-out/napi/...`, but electron-builder only ships `packages/agent-native/napi/`. The addon was therefore absent from the produced .exe / .dmg / .AppImage, and every chat call died at the dynamic `@zseven-w/agent-native` import. - Drop the prebuilt-download path; always build from source on the runner (mlugg/setup-zig is already provisioned for every workflow) - Always copy the built binary into `napi/agent_napi.node` so electron-builder packages it - Honor `ZIG_TARGET` to cross-compile (mac-x64 on arm64 runners now produces an x86_64 binary instead of a wrong-arch arm64 one) - Add `OPENPENCIL_REQUIRE_AGENT_NATIVE=1` strict mode plus a dedicated "Verify agent-native binary" step in build-electron.yml so missing binaries fail the workflow loudly - Add `OPENPENCIL_SKIP_AGENT_NATIVE=1` for publish-cli.yml, which never ships the addon and shouldn't pay for the build
This commit is contained in:
parent
f61ca2072b
commit
5e6b7475a0
3 changed files with 79 additions and 121 deletions
16
.github/workflows/build-electron.yml
vendored
16
.github/workflows/build-electron.yml
vendored
|
|
@ -22,15 +22,19 @@ jobs:
|
|||
- os: macos-latest
|
||||
platform: mac-arm64
|
||||
build_args: --mac --arm64
|
||||
zig_target: aarch64-macos
|
||||
- os: macos-latest
|
||||
platform: mac-x64
|
||||
build_args: --mac --x64
|
||||
zig_target: x86_64-macos
|
||||
- os: windows-latest
|
||||
platform: win
|
||||
build_args: --win
|
||||
zig_target: x86_64-windows
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
build_args: --linux
|
||||
zig_target: x86_64-linux
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -50,8 +54,20 @@ jobs:
|
|||
version: 0.15.2
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
OPENPENCIL_REQUIRE_AGENT_NATIVE: '1'
|
||||
ZIG_TARGET: ${{ matrix.zig_target }}
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Verify agent-native binary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f packages/agent-native/napi/agent_napi.node ]; then
|
||||
echo "::error::packages/agent-native/napi/agent_napi.node missing — electron-builder would ship without it."
|
||||
exit 1
|
||||
fi
|
||||
ls -la packages/agent-native/napi/agent_napi.node
|
||||
|
||||
- name: Build web (electron target)
|
||||
run: bun --bun run build
|
||||
env:
|
||||
|
|
|
|||
2
.github/workflows/publish-cli.yml
vendored
2
.github/workflows/publish-cli.yml
vendored
|
|
@ -34,6 +34,8 @@ jobs:
|
|||
version: 0.15.2
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
OPENPENCIL_SKIP_AGENT_NATIVE: '1'
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Get version
|
||||
|
|
|
|||
|
|
@ -1,165 +1,105 @@
|
|||
#!/usr/bin/env node
|
||||
// Ensures the Zig NAPI addon binary exists.
|
||||
// Provisions the Zig NAPI addon binary by building it from source.
|
||||
//
|
||||
// Strategy (fastest to slowest):
|
||||
// 1. Already built / bundled — use it.
|
||||
// 2. Download prebuilt from the ZSeven-W/agent release whose tag points at
|
||||
// the submodule's currently checked-out commit. This means CI and local
|
||||
// installs never need Zig installed, as long as whoever bumped the
|
||||
// submodule also tagged + published a matching release.
|
||||
// 3. Build from source with local Zig (slow but authoritative).
|
||||
// We always build from source on the host so the resulting `agent_napi.node`
|
||||
// matches the runner's platform/arch. Earlier revisions also tried to download
|
||||
// a prebuilt from a sibling release repo, but that path was racy: when the
|
||||
// prebuilt was missing for the current submodule SHA the build fell through
|
||||
// to source compilation, deposited the binary at `zig-out/napi/...`, and
|
||||
// electron-builder (which only ships `packages/agent-native/napi/`) silently
|
||||
// shipped without the addon — every chat request then died at the dynamic
|
||||
// `@zseven-w/agent-native` import.
|
||||
//
|
||||
// Failing all of those is non-fatal — the postinstall wrapper swallows exit
|
||||
// codes so `bun install` never breaks. Tests / runtime will surface a clear
|
||||
// "could not locate agent_napi.node" error instead, with instructions.
|
||||
// Build prerequisite: Zig 0.15+ on PATH. CI workflows install it via
|
||||
// `mlugg/setup-zig`; local devs install once via their package manager.
|
||||
//
|
||||
// Set OPENPENCIL_REQUIRE_AGENT_NATIVE=1 to fail the install when the build
|
||||
// can't run (electron CI uses this to surface missing prerequisites early).
|
||||
//
|
||||
// Set OPENPENCIL_SKIP_AGENT_NATIVE=1 to no-op the script entirely. Useful for
|
||||
// workflows (npm publish, lint-only CI) that never load the addon at runtime
|
||||
// and would otherwise pay for a Zig build on every install.
|
||||
//
|
||||
// Set ZIG_TARGET to cross-compile for a non-host triple (e.g. on a macOS arm64
|
||||
// runner build for x86_64-macos with `ZIG_TARGET=x86_64-macos`). Without it
|
||||
// the build follows the host arch — fine for native runs, wrong when the
|
||||
// runner doesn't match the artifact you intend to ship.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const REPO = 'ZSeven-W/agent';
|
||||
const NAPI_DIR = path.join(__dirname, '..', 'packages', 'agent-native', 'napi');
|
||||
const AGENT_DIR = path.join(__dirname, '..', 'packages', 'agent-native');
|
||||
const NAPI_DIR = path.join(AGENT_DIR, 'napi');
|
||||
const ZIG_OUT = path.join(AGENT_DIR, 'zig-out', 'napi', 'agent_napi.node');
|
||||
const BUNDLED = path.join(NAPI_DIR, 'agent_napi.node');
|
||||
const STRICT = process.env.OPENPENCIL_REQUIRE_AGENT_NATIVE === '1';
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[agent-native] ${msg}`);
|
||||
}
|
||||
|
||||
function assetNameForHost() {
|
||||
const p = process.platform; // 'darwin' | 'linux' | 'win32'
|
||||
const a = process.arch; // 'arm64' | 'x64'
|
||||
const os = p === 'darwin' ? 'macos' : p === 'win32' ? 'windows' : 'linux';
|
||||
return `agent_napi-${os}-${a}.node`;
|
||||
function fail(msg) {
|
||||
log(msg);
|
||||
return STRICT ? 1 : 0;
|
||||
}
|
||||
|
||||
function readSubmoduleSha() {
|
||||
try {
|
||||
return execSync('git rev-parse HEAD', {
|
||||
cwd: AGENT_DIR,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function ghJson(url) {
|
||||
const headers = ['-H', 'Accept: application/vnd.github+json'];
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`);
|
||||
}
|
||||
const raw = execSync(`curl -sLf ${headers.join(' ')} "${url}"`, { encoding: 'utf8' });
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function findMatchingRelease(submoduleSha) {
|
||||
if (!submoduleSha) return null;
|
||||
// Walk tags (paginated) until we find one whose commit SHA matches the
|
||||
// submodule pointer. Tags are listed newest first, so for a freshly bumped
|
||||
// submodule we almost always hit on the first page.
|
||||
for (let page = 1; page <= 3; page += 1) {
|
||||
let tags;
|
||||
try {
|
||||
tags = ghJson(`https://api.github.com/repos/${REPO}/tags?per_page=30&page=${page}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(tags) || tags.length === 0) return null;
|
||||
for (const t of tags) {
|
||||
if (t?.commit?.sha === submoduleSha) return t.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadPrebuilt(tagName) {
|
||||
let release;
|
||||
try {
|
||||
release = ghJson(`https://api.github.com/repos/${REPO}/releases/tags/${tagName}`);
|
||||
} catch (err) {
|
||||
log(`No release for tag ${tagName}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
const assetName = assetNameForHost();
|
||||
const asset = release.assets?.find((a) => a.name === assetName);
|
||||
if (!asset) {
|
||||
log(
|
||||
`Release ${tagName} has no asset ${assetName} (built: ${(release.assets ?? []).map((a) => a.name).join(', ') || 'none'}).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
log(`Downloading ${assetName} from release ${tagName}…`);
|
||||
try {
|
||||
fs.mkdirSync(NAPI_DIR, { recursive: true });
|
||||
execSync(`curl -sLf -o "${BUNDLED}" "${asset.browser_download_url}"`, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
log(`Download failed: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
return fs.existsSync(BUNDLED);
|
||||
function bundleBinary() {
|
||||
fs.mkdirSync(NAPI_DIR, { recursive: true });
|
||||
fs.copyFileSync(ZIG_OUT, BUNDLED);
|
||||
log(`Bundled binary at ${BUNDLED}.`);
|
||||
}
|
||||
|
||||
function buildFromSource() {
|
||||
try {
|
||||
execSync('zig version', { stdio: 'ignore' });
|
||||
} catch {
|
||||
log('Zig not installed; cannot build from source.');
|
||||
return false;
|
||||
return fail(
|
||||
'Zig not installed (need 0.15+). Skipping. Install Zig and re-run `bun run agent:build`.',
|
||||
);
|
||||
}
|
||||
log('Building NAPI addon from source (zig build napi)…');
|
||||
const target = process.env.ZIG_TARGET?.trim();
|
||||
const targetFlag = target ? ` -Dtarget=${target}` : '';
|
||||
log(`Building NAPI addon (zig build napi -Doptimize=ReleaseFast${targetFlag})…`);
|
||||
try {
|
||||
execSync('zig build napi -Doptimize=ReleaseFast', {
|
||||
execSync(`zig build napi -Doptimize=ReleaseFast${targetFlag}`, {
|
||||
cwd: AGENT_DIR,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch (err) {
|
||||
log(`Source build failed: ${err.message}`);
|
||||
return false;
|
||||
return fail(`Zig build failed: ${err.message}`);
|
||||
}
|
||||
return fs.existsSync(ZIG_OUT);
|
||||
if (!fs.existsSync(ZIG_OUT)) {
|
||||
return fail(`Zig build produced no output at ${ZIG_OUT}.`);
|
||||
}
|
||||
bundleBinary();
|
||||
return 0;
|
||||
}
|
||||
|
||||
function main() {
|
||||
// 1. Already have it?
|
||||
if (fs.existsSync(ZIG_OUT) || fs.existsSync(BUNDLED)) {
|
||||
log('Binary already present, skipping.');
|
||||
if (process.env.OPENPENCIL_SKIP_AGENT_NATIVE === '1') {
|
||||
log('OPENPENCIL_SKIP_AGENT_NATIVE=1, skipping native binary provisioning.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Submodule initialized?
|
||||
if (!fs.existsSync(path.join(NAPI_DIR, 'package.json'))) {
|
||||
log('Submodule not initialized; run `git submodule update --init`. Skipping.');
|
||||
return fail('Submodule not initialized; run `git submodule update --init`. Skipping.');
|
||||
}
|
||||
|
||||
// Fast path: binary already in place. Make sure both lookup locations are
|
||||
// populated so electron-builder (`napi/`) and the runtime loader (which
|
||||
// checks `zig-out/` first) both find it without re-running the build.
|
||||
if (fs.existsSync(BUNDLED)) {
|
||||
log('Binary already present, skipping rebuild.');
|
||||
return 0;
|
||||
}
|
||||
if (fs.existsSync(ZIG_OUT)) {
|
||||
log('Binary already built; copying into napi/ for electron-builder.');
|
||||
bundleBinary();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. Prebuilt release matching submodule SHA?
|
||||
const sha = readSubmoduleSha();
|
||||
if (sha) {
|
||||
const tag = findMatchingRelease(sha);
|
||||
if (tag) {
|
||||
if (downloadPrebuilt(tag)) {
|
||||
log(`Prebuilt ready at ${BUNDLED}.`);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
log(`No release tag matches submodule ${sha.slice(0, 7)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Source build fallback.
|
||||
if (buildFromSource()) {
|
||||
log(`Built at ${ZIG_OUT}.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
log('Could not provision agent_napi.node. Tests / runtime will fail loudly until resolved.');
|
||||
log(
|
||||
'Options: bump + tag the agent submodule, or install Zig 0.15+ and run `bun run agent:build`.',
|
||||
);
|
||||
return 0; // Non-fatal: let the wrapper keep install green, real error surfaces at test time.
|
||||
return buildFromSource();
|
||||
}
|
||||
|
||||
process.exit(main());
|
||||
|
|
|
|||
Loading…
Reference in a new issue