sotashimozono_obsidian-remo.../plugin/scripts/build-server.mjs
Souta 79aace49c3 fix(daemon): sha-verify on the fast path + keep the dev-daemon marker in E2E
Addresses /review-pr findings on #447 and the E2E smoke failure they caused.

- F1 (code-reviewer + silent-failure): the connect fast-path trusted
  existsSync alone, re-opening the "deploy an unverified binary" gap one layer
  above DaemonDownloader's fixed sha-check. Now records `daemonBinarySha` and
  re-hashes the cached file on the fast path; a post-write-corrupted binary
  fails the check and is re-fetched (network-free). Version + sha must both
  match to reuse.
- F2 (silent-failure): a saveSettings() failure after a successful download
  was caught by the generic "download failed -> SFTP" handler and misreported.
  Marker persistence is now its own non-fatal try — the verified binary is
  still returned; a persist failure just re-verifies next connect.
- F3 (comment-analyzer): corrected the dev-install marker comment (a stale
  binary is refreshed only when its sha differs, not "always on a bump").
- F4/F5 (pr-test): new ensureDaemonBinary.test.ts covers the locateDaemonBinary
  .dev-daemon gate and the fast-path reuse/skip (sha match, sha mismatch,
  version mismatch).

E2E fix: the new locateDaemonBinary `.dev-daemon` gate broke the Obsidian smoke
run — E2E stages the daemon via `build:server` (no dev-install), so the staged
binary was unmarked -> gated out -> no RPC -> the live-reflect specs failed.
build-server.mjs now writes the `.dev-daemon` marker; the E2E vault-scaffold
copies server-bin/ verbatim so it rides along. (Release daemons are built via
the Makefile, never marked — correct: users' downloads stay subject to the sha
refresh.)

tsc/lint/vitest green (1193). Still beta.14 (same PR #447).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:07:38 +09:00

126 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
/*
* build-server — cross-compile the obsidian-remote-server Go daemon
* for linux/amd64 and stage the resulting binary into
* <plugin>/server-bin/ so dev-install.mjs picks it up alongside
* main.js / manifest.json / styles.css.
*
* Usage:
* node scripts/build-server.mjs
* node scripts/build-server.mjs --goos=linux --goarch=arm64
*
* Locating Go:
* 1. `go` on PATH (preferred).
* 2. `~/tools/go-portable/go/bin/go(.exe)` — useful for the dev box
* where Go isn't installed system-wide; ignored elsewhere.
* 3. Otherwise the script exits with a clear error.
*
* Skipping:
* Set REMOTE_SSH_SKIP_SERVER_BUILD=1 to no-op (build:install can
* then run on machines that don't have Go and don't need a fresh
* binary).
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pluginRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(pluginRoot, '..');
const serverDir = path.join(repoRoot, 'server');
const stageDir = path.join(pluginRoot, 'server-bin');
if (process.env.REMOTE_SSH_SKIP_SERVER_BUILD === '1') {
console.log('build-server: REMOTE_SSH_SKIP_SERVER_BUILD set, skipping');
process.exit(0);
}
// Parse flags.
const args = process.argv.slice(2);
const flag = (name) => {
const hit = args.find(a => a.startsWith(`--${name}=`));
return hit ? hit.slice(name.length + 3) : undefined;
};
const goos = flag('goos') ?? 'linux';
const goarch = flag('goarch') ?? 'amd64';
const outName = `obsidian-remote-server-${goos}-${goarch}` + (goos === 'windows' ? '.exe' : '');
const goExe = locateGo();
if (!goExe) {
console.error('build-server: could not locate the Go toolchain.');
console.error(' Install Go (https://go.dev/dl/) or set REMOTE_SSH_SKIP_SERVER_BUILD=1.');
process.exit(1);
}
if (!fs.existsSync(serverDir)) {
console.error(`build-server: ${serverDir} does not exist`);
process.exit(1);
}
fs.mkdirSync(stageDir, { recursive: true });
const outPath = path.join(stageDir, outName);
console.log(`build-server: ${goExe} build -o ${path.relative(repoRoot, outPath)} (GOOS=${goos} GOARCH=${goarch})`);
const result = spawnSync(
goExe,
['build', '-o', outPath, './cmd/obsidian-remote-server'],
{
cwd: serverDir,
stdio: 'inherit',
env: { ...process.env, GOOS: goos, GOARCH: goarch, CGO_ENABLED: '0' },
},
);
if (result.status !== 0) {
console.error(`build-server: go build exited ${result.status}`);
process.exit(result.status ?? 1);
}
console.log(`build-server: staged ${path.relative(repoRoot, outPath)} (${prettySize(fs.statSync(outPath).size)})`);
// Mark the staged binary as a DEV build so the plugin's locateDaemonBinary
// trusts it over a download (it now requires this marker). dev-install.mjs and
// the E2E vault-scaffold copy server-bin/ verbatim, so the marker rides along;
// build:server on its own (the E2E path, no dev-install) would otherwise leave
// the daemon unmarked → gated out → RPC unavailable in the Obsidian smoke run.
fs.writeFileSync(path.join(stageDir, '.dev-daemon'), '');
function locateGo() {
const fromPath = which('go') ?? which('go.exe');
if (fromPath) return fromPath;
// Fallback: the portable Go on the dev box.
const portableCandidates = [
path.join(os.homedir(), 'tools', 'go-portable', 'go', 'bin', 'go.exe'),
path.join(os.homedir(), 'tools', 'go-portable', 'go', 'bin', 'go'),
];
for (const c of portableCandidates) {
if (fs.existsSync(c)) return c;
}
return null;
}
function which(cmd) {
const sep = process.platform === 'win32' ? ';' : ':';
const exts = process.platform === 'win32'
? (process.env.PATHEXT?.split(';') ?? ['.EXE', '.CMD', '.BAT'])
: [''];
for (const dir of (process.env.PATH ?? '').split(sep)) {
if (!dir) continue;
for (const ext of exts) {
const p = path.join(dir, cmd + ext);
if (fs.existsSync(p)) return p;
}
}
return null;
}
function prettySize(n) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
return `${(n / 1024 / 1024).toFixed(2)} MiB`;
}