#!/usr/bin/env node /** * Normalize every symlink/junction in a copied harness tree so the tree is * self-contained: no link may resolve to a path outside the tree. * * A pnpm workspace is held together by links inside node_modules. On Windows * those are often **junctions** (absolute-path reparse points) because real * symlinks need Developer Mode or an admin shell; even plain symlinks can be * absolute. `build-harness.mjs` copies the working tree with * `cpSync(..., { recursive: true })`, which — with the default * `dereference: false` — recreates every link verbatim, so the packaged harness * keeps pointing at the BUILD machine's dev tree: it reads dev resources on the * same machine and dangles on any other. This module rewrites each such link to * a RELATIVE target inside the harness (mirroring the dev layout), and copies * the real content of any genuinely external target in so nothing escapes. * * Pure Node, no Electron and no workspace TS program: imported by * `scripts/build-harness.mjs` / `scripts/verify-harness.mjs` and unit-tested * from `tests/harness-relink.spec.ts`. * @module dsh-desktop/harness-relink */ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readlinkSync, rmSync, symlinkSync, } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' /** True when `p` resolves inside `root` (or is `root` itself). */ export function isWithin(root, p) { const rel = relative(resolve(root), resolve(p)) return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) } /** The link's own kind so Windows creates the right reparse point ('dir'/'file'). */ function linkKind(target) { try { return lstatSync(target).isDirectory() ? 'dir' : 'file' } catch { return 'dir' } } /** Remove a link and copy the target's real content into its place. */ function dereferenceInto(link, target) { rmSync(link, { force: true }) cpSync(target, link, { recursive: true, dereference: true }) } /** Rewrite `link` as a relative symlink to `mirrored`; dereference on failure. */ function relinkTo(link, mirrored, report) { const rel = relative(dirname(link), mirrored) try { rmSync(link, { force: true }) symlinkSync(rel, link, linkKind(mirrored)) report.normalized += 1 } catch (error) { // Symlink creation failed (e.g. no Developer Mode on Windows): a compact // portable link is impossible, so copy the target content in instead. try { dereferenceInto(link, mirrored) report.dereferenced += 1 } catch (copyError) { report.errors.push(`${link} -> ${mirrored} (${String(copyError)})`) } } } /** * Walk `out` and normalize every symlink/junction to a relative in-tree link. * Idempotent: a link already pointing inside the tree is left alone. * @param out - the harness root. * @param repoRoot - the repository root whose layout `out` mirrors. * @returns per-kind counts plus any unrecoverable errors. */ export function relinkHarness(out, repoRoot) { const report = { normalized: 0, dereferenced: 0, unchanged: 0, removed: 0, errors: [] } forEachLink(out, (link) => { const target = readlinkSync(link) const abs = resolve(dirname(link), target) if (isWithin(out, abs)) { if (!existsSync(abs)) { // A link inside the tree whose target is absent points at a workspace // package the harness does not ship (python/sdk-runtime, examples, …). // It cannot resolve and is not part of the runtime: drop it. rmSync(link, { force: true }) report.removed += 1 } else if (relative(dirname(link), abs) === target) { // Already inside the tree: make sure the link is a relative in-tree path. report.unchanged += 1 } else { relinkTo(link, abs, report) } } else if (isWithin(repoRoot, abs)) { // Points at the dev tree: mirror the target into the harness and relink. const mirrored = join(out, relative(repoRoot, abs)) if (existsSync(mirrored)) { relinkTo(link, mirrored, report) } else if (isWithin(abs, out)) { // The target contains the harness itself (e.g. the desktop shell // package, which the harness deliberately does not ship). Copying it in // would recurse into the bundle and it is not part of the runtime // closure, so the link cannot be made self-contained — drop it. rmSync(link, { force: true }) report.removed += 1 } else if (existsSync(abs)) { try { dereferenceInto(link, abs) report.dereferenced += 1 } catch (error) { report.errors.push(`${link} -> ${abs} (${String(error)})`) } } else { report.errors.push(`${link} -> ${abs} (dangling)`) } } else if (existsSync(abs)) { // Genuinely external target (e.g. a global store): copy content in. try { dereferenceInto(link, abs) report.dereferenced += 1 } catch (error) { report.errors.push(`${link} -> ${abs} (${String(error)})`) } } else { report.errors.push(`${link} -> ${abs} (dangling)`) } }) return report } /** * Every symlink/junction whose target escapes `out` or no longer exists. A * non-empty result means the harness is NOT detached from its build machine. */ export function findEscapingLinks(out) { const escaping = [] forEachLink(out, (link) => { const abs = resolve(dirname(link), readlinkSync(link)) if (!isWithin(out, abs) || !existsSync(abs)) escaping.push(link) }) return escaping } /** Throw unless the tree is self-contained (no escaping or dangling links). */ export function assertSelfContained(out) { const escaping = findEscapingLinks(out) if (escaping.length === 0) return const listed = escaping.slice(0, 20).join('\n ') throw new Error( `harness is NOT self-contained: ${escaping.length} link(s) resolve outside it or are dangling:\n ${listed}`, ) } /** * Whether the current host can create (relative) directory symlinks. Windows * without Developer Mode (and without an admin shell) cannot; without this * capability the packaged harness could not be relinked compactly. */ export function probeSymlinkSupport() { const base = mkdtempSync(join(tmpdir(), 'dsh-symlink-probe-')) const target = join(base, 'target') const link = join(base, 'link') try { // The target must exist: Windows refuses a directory link to a missing // target, and on POSIX existsSync() would report a dangling link as absent. mkdirSync(target, { recursive: true }) symlinkSync(target, link, 'dir') return existsSync(link) } catch { return false } finally { rmSync(base, { recursive: true, force: true }) } } /** * Visit every symlink/junction under `root` without following into any link. * Junctions are reported as symlinks by `lstat` on Windows; because the walk * never recurses through a link, it cannot loop or escape the tree. */ function forEachLink(root, fn) { const stack = [resolve(root)] const seen = new Set() while (stack.length > 0) { const dir = stack.pop() if (dir === undefined || seen.has(dir)) continue seen.add(dir) let entries try { entries = readdirSync(dir, { withFileTypes: true }) } catch { continue } for (const entry of entries) { const path = join(dir, entry.name) let stat try { stat = lstatSync(path) } catch { continue } if (stat.isSymbolicLink()) { fn(path) } else if (stat.isDirectory()) { stack.push(path) } } } }