diff --git a/.github/workflows/cef-learning-harness.yml b/.github/workflows/cef-learning-harness.yml new file mode 100644 index 00000000..52407452 --- /dev/null +++ b/.github/workflows/cef-learning-harness.yml @@ -0,0 +1,86 @@ +# ============================================================ +# WorldScript Studio – CEF Learning Harness (Wave 2 bootstrap) +# +# docs/cef/ROADMAP-CEF-DESKTOP-MIGRATION.md §3142 (Wave 2 deliverables), ADR-0020 +# (CEF binding choice — the Wave 2 spike this harness formalizes into a real, +# repo-committed, CI-run check per §61.1.4's evidence-link discipline). +# +# Scope of this first increment: fetch + verify the pinned CEF SDK, report a +# clean-machine Linux runtime-dependency inventory (before any apt-get — a genuinely +# new data point vs. the spike's already-configured dev machine), and print real +# version diagnostics parsed from the fetched SDK itself. The actual CEF host +# build/run/shutdown proof (§4.11.3's "isolated learning harness") is deliberately +# NOT part of this increment — see ADR-0020's Consequences for why that needs its +# own CMake/Cargo bootstrap PR. +# +# Advisory only: not part of the required `ci-success` aggregator. Wave 2 is still +# in progress; this harness informs it rather than gating unrelated PRs on it. +# ============================================================ + +name: 🧪 CEF Learning Harness + +on: + pull_request: + branches: [main] + paths: + - 'scripts/cef/**' + - 'docs/cef/**' + - '.github/workflows/cef-learning-harness.yml' + push: + branches: [main] + paths: + - 'scripts/cef/**' + - 'docs/cef/**' + - '.github/workflows/cef-learning-harness.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cef-learning-harness-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + harness: + name: 🧪 Fetch CEF SDK + dependency inventory + version diagnostics + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + + # Runs on the stock runner before any apt-get — the "clean machine" data point + # docs/cef/knowledge/linux-runtime-notes.md flags as still open. + - name: Clean-machine Linux dependency inventory + run: node scripts/cef/check-linux-runtime-deps.mjs + + - name: Restore CEF SDK cache + id: cef-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .cef-cache + key: cef-sdk-${{ hashFiles('scripts/cef/cef-version.json') }} + + - name: Fetch + verify CEF SDK + run: | + set -o pipefail + CEF_DIR=$(node scripts/cef/fetch-cef-sdk.mjs --cache-dir .cef-cache | tail -1) + echo "CEF_DIR=$CEF_DIR" >> "$GITHUB_ENV" + + - name: Print CEF version diagnostics + run: node scripts/cef/print-cef-version-diagnostics.mjs "$CEF_DIR" + + - name: Summary + run: | + echo "## 🧪 CEF Learning Harness" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- Pinned SDK: \`$(node -e "console.log(require('./scripts/cef/cef-version.json').cefVersion)")\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Cache hit: \`${{ steps.cef-cache.outputs.cache-hit }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Scope: fetch/verify + clean-machine dependency inventory + version diagnostics only." >> "$GITHUB_STEP_SUMMARY" + echo "- Not yet in scope: CEF host build/run/shutdown proof (see ADR-0020 Consequences)." >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index c22f837c..bc04a305 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,10 @@ public/duckdb/ .codegraph/codegraph.db-wal voice-spike/ +# CEF SDK cache (~300MB compressed / ~1.5GB extracted) — fetched on demand by +# scripts/cef/fetch-cef-sdk.mjs (Wave 2, ADR-0020), never committed. See cef-version.json for the pin. +.cef-cache/ + # Signing keys, certificates, and secrets — NEVER commit *.key *.key.pub diff --git a/docs/architecture/native-readiness.md b/docs/architecture/native-readiness.md index 9cb825b5..5f65ccaf 100644 --- a/docs/architecture/native-readiness.md +++ b/docs/architecture/native-readiness.md @@ -50,7 +50,7 @@ The Wave 1 PR A snapshot left three DEBT rows marked "not yet scheduled to a spe Re-score "Platform APIs adapter-contained" again once `services/logger.ts`'s JSONL sink migrates (Wave 5/7, see above). Re-score the three newly-scheduled rows at each wave's exit (Wave 4 typed-IPC-v1, Wave 5 domain extraction). -## Snapshot: Wave 2 binding spike (ADR-0020, `docs/cef/CEF-BINDING-DECISION-SCORECARD.md`) +## Snapshot: Wave 2 progress (ADR-0020 spike + CI bootstrap, `docs/cef/CEF-BINDING-DECISION-SCORECARD.md`) Wave 2's first deliverable — the CEF binding/C++ decision — is now backed by a real hands-on spike, not a desk pick. Full scorecard: `docs/cef/CEF-BINDING-DECISION-SCORECARD.md`. This snapshot only tracks what changes for *this* readiness table; the roadmap's own Wave 2 exit criteria (Early Accessibility Gate, "no production-critical lifecycle assumption remains undocumented") are broader than this single row and are **not** claimed as met here. @@ -61,5 +61,7 @@ Wave 2's first deliverable — the CEF binding/C++ decision — is now backed by | CEF lifecycle assumptions documented | DEBT — partial | cef-runtime, Wave 2 (in progress) | `docs/cef/knowledge/subprocess-and-shutdown.md` now has one real (non-speculative) finding — SIGTERM shutdown is not instantaneous, don't false-positive an immediate post-signal check — but it is explicitly flagged as not yet backed by a committed/CI-run test, per the doc's own evidence-link discipline. Exit condition: a real test + CI job, not just a spike observation. | | Early Accessibility Gate | Not yet attempted | cef-runtime, Wave 2 | Out of this spike's scope entirely — no accessibility tree/API integration was touched. | | Sandbox posture | Not yet attempted | desktop-security, Wave 2/3 (roadmap §12) | Spike explicitly ran with `no_sandbox=true`; zero evidence either way on this row. | +| CEF SDK fetch/verify + version diagnostics automated | **PASS** | cef-runtime | `🧪 CEF Learning Harness` CI job (`.github/workflows/cef-learning-harness.yml`) fetches the pinned CEF SDK, verifies its checksum, and parses real version macros out of the extracted `include/cef_version.h` — a genuine CI-run check, not a doc claim. Does not build or run the CEF host itself yet (see ADR-0020 Consequences for that follow-up). | +| Linux dependency inventory — clean-machine data point | DEBT — partial | cef-runtime | Same CI job runs the package-presence check against a stock `ubuntu-latest` runner before any `apt-get`, adding a real second data point beyond the spike's one already-configured dev machine. Still narrow: `dpkg` package-presence only (not `ldd` against the actual shipped `.so` files), one distro/runner image. | -**Overall for this snapshot**: 1 PASS, 2 explicit DEBT-in-progress rows (each with a concrete exit condition, not open-ended), 2 not-yet-attempted rows correctly left blank rather than assumed. No row is marked PASS without the evidence cited above. +**Overall for this snapshot**: 2 PASS, 3 explicit DEBT-in-progress rows (each with a concrete exit condition, not open-ended), 2 not-yet-attempted rows correctly left blank rather than assumed. No row is marked PASS without the evidence cited above. diff --git a/docs/cef/OWNERSHIP.yaml b/docs/cef/OWNERSHIP.yaml index 2d0aa70d..771e20e1 100644 --- a/docs/cef/OWNERSHIP.yaml +++ b/docs/cef/OWNERSHIP.yaml @@ -87,7 +87,8 @@ documents: worldscript: "v1.27.1" cef: "151.3.18+gbeff58d+chromium-151.0.7922.138" review_days: 90 - related_ci: [] + related_ci: + - cef-learning-harness driftCheckTool: "planned — not implemented, see Wave 1" note: "Appendix H scorecard, filled in per ADR-0020 — re-score triggers are listed in the doc itself, not just the review_days cadence." @@ -146,9 +147,10 @@ documents: worldscript: "v1.27.1" cef: "151.3.18+gbeff58d+chromium-151.0.7922.138" review_days: 90 - related_ci: [] + related_ci: + - cef-learning-harness driftCheckTool: "planned — not implemented, see Wave 1" - note: "Preliminary spike evidence only (ADR-0020) — one machine, one GPU, X11 only; not a compatibility contract yet." + note: "Preliminary spike evidence only (ADR-0020) — one machine, one GPU, X11 only; not a compatibility contract yet. The package-presence portion of the dependency inventory now also has a real CI-run data point via the cef-learning-harness job (stock ubuntu-latest, pre-apt-get)." - path: docs/cef/knowledge/debugging-and-crash-playbook.md tier: A @@ -189,7 +191,8 @@ documents: worldscript: "v1.27.1" cef: "151.3.18+gbeff58d+chromium-151.0.7922.138" review_days: 180 - related_ci: [] + related_ci: + - cef-learning-harness driftCheckTool: "planned — not implemented, see Wave 1" note: "Living scorecard — re-score at every architecture-changing PR and wave exit (§7.4.5), not just on a review-day cadence." diff --git a/docs/cef/knowledge/linux-runtime-notes.md b/docs/cef/knowledge/linux-runtime-notes.md index 1293b0b4..3fa14502 100644 --- a/docs/cef/knowledge/linux-runtime-notes.md +++ b/docs/cef/knowledge/linux-runtime-notes.md @@ -1,6 +1,6 @@ # CEF Linux Runtime Notes -**Status:** Preliminary spike evidence only (2026-08-18, ADR-0020) — one dev machine, one CEF version, X11 only. Not a compatibility contract yet; do not treat any number below as a floor until packaged builds prove it (§44.1). +**Status:** Preliminary spike evidence only (2026-08-18, ADR-0020) — one dev machine, one CEF version, X11 only. Not a compatibility contract yet; do not treat any number below as a floor until packaged builds prove it (§44.1). The package-presence portion of the dependency inventory now also has a real, CI-run second data point (stock `ubuntu-latest`) via the `🧪 CEF Learning Harness` workflow — see the "Remaining outline" section below for exactly what that job does and does not cover. **Scope:** The Linux Runtime Compatibility Contract for WorldScript's CEF build — minimum supported distribution/runtime baseline, CPU architectures, required dynamic libraries/packages, `libcef.so`/resource layout, loader/rpath policy, X11 vs. Wayland policy, Ozone/backend policy, GPU/driver expectations, installer dependency behavior. **Tier:** A (release/security-critical) — see [`../OWNERSHIP.yaml`](../OWNERSHIP.yaml). **Roadmap context:** [`../ROADMAP-CEF-DESKTOP-MIGRATION.md`](../ROADMAP-CEF-DESKTOP-MIGRATION.md) §44.1–§44.5 (Linux runtime compatibility as its own workstream), Appendix A.3 (compatibility matrix template). @@ -26,4 +26,4 @@ Do not hardcode a glibc/distro minimum here until packaged builds have proven it - `libcef.so` and resource layout for our actual packaging (not yet designed — this spike used CEF's own unpackaged build-output layout) - X11 and Wayland smoke-test results across KDE, GNOME × NVIDIA, AMD, Intel (Appendix A.3 matrix) — this spike covers exactly one cell (X11/Xvfb, Intel integrated) of that matrix - Sandbox requirements observed on Linux (not exercised this spike) -- Clean-machine dependency test results (§44.3) — this spike ran on an already-configured dev machine, not a clean install +- Clean-machine dependency test results (§44.3) — **partially closed**: the `🧪 CEF Learning Harness` CI job (`.github/workflows/cef-learning-harness.yml`, `scripts/cef/check-linux-runtime-deps.mjs`) runs the same package-presence check against a stock `ubuntu-latest` runner, before any `apt-get`, giving a real second data point beyond this already-configured dev machine. Still open: only checks `dpkg` package presence, not `ldd` against the actual shipped `.so` files; only one distro/runner image; still not a packaged-installer dependency declaration diff --git a/package.json b/package.json index 1b644584..cf36273a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "suppressions:check": "node scripts/check-suppressions.mjs", "token:audit": "node scripts/audit-tokens.mjs", "guardrail:desktop-imports": "node scripts/check-tauri-import-boundary.mjs", + "cef:fetch-sdk": "node scripts/cef/fetch-cef-sdk.mjs", + "cef:check-linux-deps": "node scripts/cef/check-linux-runtime-deps.mjs", + "cef:print-version-diagnostics": "node scripts/cef/print-cef-version-diagnostics.mjs", "sync:readme": "node scripts/sync-readme-metrics.mjs", "graphify": "node scripts/graphify-cli.mjs", "graphify:install": "node scripts/graphify-cli.mjs install", diff --git a/scripts/cef/cef-version.json b/scripts/cef/cef-version.json new file mode 100644 index 00000000..bc3915b4 --- /dev/null +++ b/scripts/cef/cef-version.json @@ -0,0 +1,10 @@ +{ + "$comment": "Pinned CEF binary distribution for the Wave 2 learning harness (docs/cef/ROADMAP-CEF-DESKTOP-MIGRATION.md §3142, ADR-0020). Bump this file — and only this file — to move to a new CEF version; scripts/cef/fetch-cef-sdk.mjs reads it, never hardcodes a version. sha1/sizeBytes come straight from https://cef-builds.spotifycdn.com/index.json — do not hand-compute them.", + "cefVersion": "151.3.18+gbeff58d+chromium-151.0.7922.138", + "platform": "linux64", + "distType": "minimal", + "filename": "cef_binary_151.3.18+gbeff58d+chromium-151.0.7922.138_linux64_minimal.tar.bz2", + "sha1": "5fc23dcccfead1044b2c3bf02e4ad27e86c29c0f", + "sizeBytes": 321422583, + "baseUrl": "https://cef-builds.spotifycdn.com" +} diff --git a/scripts/cef/cefPaths.mjs b/scripts/cef/cefPaths.mjs new file mode 100644 index 00000000..a9ee9085 --- /dev/null +++ b/scripts/cef/cefPaths.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env node +/** + * Shared cache-path computation for the CEF Wave 2 scripts (ADR-0020) — kept in one place so + * fetch-cef-sdk.mjs and print-cef-version-diagnostics.mjs can never compute a different path for + * the same pinned SDK. + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..', '..'); + +/** + * @param {{filename: string}} pin + * @param {string} [cacheDirArg] + */ +export function resolveCefPaths(pin, cacheDirArg) { + const cacheDir = cacheDirArg ? path.resolve(cacheDirArg) : path.join(root, '.cef-cache'); + const archivePath = path.join(cacheDir, pin.filename); + const extractedDirName = pin.filename.replace(/\.tar\.bz2$/, ''); + const extractedDir = path.join(cacheDir, extractedDirName); + return { cacheDir, archivePath, extractedDir, extractedDirName }; +} diff --git a/scripts/cef/check-linux-runtime-deps.mjs b/scripts/cef/check-linux-runtime-deps.mjs new file mode 100644 index 00000000..1419fdb1 --- /dev/null +++ b/scripts/cef/check-linux-runtime-deps.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * CEF Linux runtime-dependency inventory (docs/cef/ROADMAP-CEF-DESKTOP-MIGRATION.md §44.3, ADR-0020). + * + * Formalizes the Wave 2 spike's informal dev-machine findings + * (docs/cef/knowledge/linux-runtime-notes.md) into a real, CI-runnable check. Reports which of + * CEF's documented runtime shared-library dependencies are present via dpkg, on *this* machine. + * + * Deliberately non-fatal by default (exit 0 even with missing packages): this script's job is to + * produce an honest inventory data point, not to gate CI on a specific distro's package set — + * that gate belongs to a later packaging wave (§44's clean-machine test), once WorldScript ships + * an actual installer with declared dependencies. Pass --strict to fail on any missing package + * once a real compatibility floor has been proven (not yet — see the roadmap warning below). + * + * Run: node scripts/cef/check-linux-runtime-deps.mjs [--strict] + */ +import { execFileSync } from 'node:child_process'; + +// QNBS-v3: list is what CEF's own build docs call out, confirmed present on one dev machine — see docs/cef/knowledge/linux-runtime-notes.md; don't add packages from generic Chromium docs without a real WorldScript test. +const REQUIRED_PACKAGES = [ + 'libnss3', + 'libnspr4', + 'libatk1.0-0', + 'libatk-bridge2.0-0', + 'libcups2', + 'libdrm2', + 'libgbm1', + 'libxcomposite1', + 'libxdamage1', + 'libxfixes3', + 'libxrandr2', + 'libxkbcommon0', + 'libpango-1.0-0', + 'libcairo2', + 'libasound2', + 'libgtk-3-0', + 'libx11-xcb1', + 'libxcb1', +]; + +/** @param {string} pkg */ +function dpkgReportsInstalled(pkg) { + try { + // QNBS-v3: dpkg -s exits 0 even for a removed package still holding config-files state — only the Status line's "installed" word confirms it's actually present. + const out = execFileSync('dpkg', ['-s', pkg], { + stdio: ['ignore', 'pipe', 'ignore'], + }).toString(); + return /^Status:.*\binstalled\b/m.test(out); + } catch { + return false; + } +} + +// QNBS-v3: Ubuntu 24.04's 64-bit-time_t transition renamed several of these to a "t64" suffix (confirmed on the real ubuntu-latest CI runner) — check both spellings. +/** @param {string} pkg */ +function isInstalled(pkg) { + return dpkgReportsInstalled(pkg) || dpkgReportsInstalled(`${pkg}t64`); +} + +const strict = process.argv.includes('--strict'); +const results = REQUIRED_PACKAGES.map((pkg) => ({ pkg, present: isInstalled(pkg) })); +const missing = results.filter((r) => !r.present); + +console.log('[check-linux-deps] CEF Linux runtime-dependency inventory:'); +for (const { pkg, present } of results) { + console.log(` ${present ? '✓' : '✗'} ${pkg}`); +} + +if (missing.length > 0) { + console.log( + `\n[check-linux-deps] ${missing.length}/${results.length} package(s) missing on this machine: ` + + missing.map((m) => m.pkg).join(', '), + ); +} else { + console.log(`\n[check-linux-deps] All ${results.length} documented packages present.`); +} + +if (strict && missing.length > 0) { + console.error('[check-linux-deps] --strict requested and packages are missing — failing.'); + process.exit(1); +} diff --git a/scripts/cef/fetch-cef-sdk.mjs b/scripts/cef/fetch-cef-sdk.mjs new file mode 100644 index 00000000..ddf25949 --- /dev/null +++ b/scripts/cef/fetch-cef-sdk.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +/** + * CEF SDK fetch/verify/extract for the Wave 2 learning harness + * (docs/cef/ROADMAP-CEF-DESKTOP-MIGRATION.md §3142, ADR-0020). + * + * ADR-0020's own "Consequences" section requires this to be a fetch script, never committed + * binaries — the SDK is ~300MB compressed / ~1.5GB extracted. Idempotent: a second run with a + * matching cache is a no-op, so CI can call this on every job without re-downloading each time + * (pair with actions/cache on CACHE_DIR). + * + * Version/checksum are read from cef-version.json, never hardcoded here — bumping the pinned CEF + * version is a one-file diff, not a code change. + * + * Run: node scripts/cef/fetch-cef-sdk.mjs [--cache-dir ] + */ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolveCefPaths } from './cefPaths.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const pin = JSON.parse(fs.readFileSync(path.join(__dirname, 'cef-version.json'), 'utf8')); + +const argCacheDirIdx = process.argv.indexOf('--cache-dir'); +const cacheDirArg = argCacheDirIdx !== -1 ? process.argv[argCacheDirIdx + 1] : undefined; +if (argCacheDirIdx !== -1 && (!cacheDirArg || cacheDirArg.startsWith('--'))) { + throw new Error('[fetch-cef-sdk] --cache-dir requires a directory path'); +} +const { cacheDir, archivePath, extractedDir, extractedDirName } = resolveCefPaths(pin, cacheDirArg); +const markerPath = path.join(extractedDir, '.fetch-cef-sdk-verified'); + +/** @param {string} filePath */ +function sha1Of(filePath) { + const hash = createHash('sha1'); + hash.update(fs.readFileSync(filePath)); + return hash.digest('hex'); +} + +function alreadyVerified() { + if (!fs.existsSync(markerPath)) return false; + const marker = fs.readFileSync(markerPath, 'utf8').trim(); + return marker === pin.sha1; +} + +async function downloadArchive() { + const url = `${pin.baseUrl}/${pin.filename}`; + console.log( + `[fetch-cef-sdk] Downloading ${url} (${(pin.sizeBytes / 1024 / 1024).toFixed(0)} MB)…`, + ); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`[fetch-cef-sdk] Download failed: HTTP ${res.status} ${res.statusText}`); + } + const buffer = Buffer.from(await res.arrayBuffer()); + fs.mkdirSync(cacheDir, { recursive: true }); + fs.writeFileSync(archivePath, buffer); +} + +function verifyArchive() { + const actual = sha1Of(archivePath); + if (actual !== pin.sha1) { + fs.rmSync(archivePath, { force: true }); + throw new Error( + `[fetch-cef-sdk] Checksum mismatch for ${pin.filename}: expected ${pin.sha1}, got ${actual}. ` + + 'Deleted the bad download — re-run to retry, or update cef-version.json if the pin is stale.', + ); + } +} + +function extractArchive() { + console.log(`[fetch-cef-sdk] Extracting to ${extractedDir}…`); + fs.rmSync(extractedDir, { recursive: true, force: true }); + // QNBS-v3: shells out to tar (bzip2 support) rather than a JS decompressor — runs once per cache miss, not hot code. + execFileSync('tar', ['xjf', archivePath, '-C', cacheDir], { stdio: 'inherit' }); + fs.writeFileSync(markerPath, pin.sha1); +} + +async function main() { + // QNBS-v3: enforced here, not just in the package.json wrapper — a direct `node` invocation must not bypass the CI-only disk/RAM guard either. + if (process.env.CI !== 'true') { + throw new Error( + '[fetch-cef-sdk] CI-only on this machine (disk/RAM constraints) — use: gh workflow run cef-learning-harness.yml', + ); + } + + if (alreadyVerified()) { + console.log( + `[fetch-cef-sdk] OK — ${extractedDirName} already fetched and verified (cache hit).`, + ); + console.log(extractedDir); + return; + } + + if (!fs.existsSync(archivePath) || sha1Of(archivePath) !== pin.sha1) { + await downloadArchive(); + verifyArchive(); + } else { + console.log('[fetch-cef-sdk] Archive present and checksum-verified; skipping download.'); + } + + extractArchive(); + console.log( + `[fetch-cef-sdk] OK — fetched CEF ${pin.cefVersion} (${pin.platform}/${pin.distType}).`, + ); + console.log(extractedDir); +} + +main().catch((err) => { + console.error(err.message ?? err); + process.exit(1); +}); diff --git a/scripts/cef/print-cef-version-diagnostics.mjs b/scripts/cef/print-cef-version-diagnostics.mjs new file mode 100644 index 00000000..b573c20b --- /dev/null +++ b/scripts/cef/print-cef-version-diagnostics.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +/** + * CEF version diagnostics (docs/cef/ROADMAP-CEF-DESKTOP-MIGRATION.md §3142 "version diagnostics", + * ADR-0020). + * + * Parses the fetched SDK's include/cef_version.h for its CEF_* / CHROME_VERSION_* #define macros + * and prints them, so CI has a real, machine-readable record of exactly which CEF/Chromium build + * a given harness run exercised — not just the pin file's string, which only says what we *asked* + * for, not what actually landed in the extracted archive. + * + * Deliberately tolerant of macro-set drift across CEF versions: reports whichever of the known + * macro names it finds and does not fail on ones it doesn't, since CEF's exact macro list has + * changed across releases and this is a diagnostic aid, not a schema contract. + * + * Run: node scripts/cef/print-cef-version-diagnostics.mjs [path-to-extracted-cef-dir] + * With no argument, defaults to the standard `.cef-cache/` path that + * fetch-cef-sdk.mjs extracts to — so this is directly usable after `cef:fetch-sdk`. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolveCefPaths } from './cefPaths.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// QNBS-v3: requires BOTH a CEF and Chromium identifying macro — a partial hit doesn't establish which build this is. +const REQUIRED_IDENTIFYING_MACROS = ['CEF_VERSION_MAJOR', 'CHROME_VERSION_MAJOR']; + +const KNOWN_MACROS = [ + 'CEF_VERSION', + 'CEF_VERSION_MAJOR', + 'CEF_VERSION_MINOR', + 'CEF_VERSION_PATCH', + 'CEF_COMMIT_NUMBER', + 'CEF_COMMIT_HASH', + 'CHROME_VERSION_MAJOR', + 'CHROME_VERSION_MINOR', + 'CHROME_VERSION_BUILD', + 'CHROME_VERSION_PATCH', +]; + +let cefDir = process.argv[2]; +if (!cefDir) { + const pin = JSON.parse(fs.readFileSync(path.join(__dirname, 'cef-version.json'), 'utf8')); + cefDir = resolveCefPaths(pin).extractedDir; + console.log(`[cef-version-diagnostics] No path given — defaulting to ${cefDir}`); +} + +const versionHeaderPath = path.join(cefDir, 'include', 'cef_version.h'); +if (!fs.existsSync(versionHeaderPath)) { + console.error(`[cef-version-diagnostics] Not found: ${versionHeaderPath}`); + process.exit(1); +} + +const text = fs.readFileSync(versionHeaderPath, 'utf8'); +// QNBS-v3: [ \t] not \s — \s matches \n, so a bare `#define GUARD_H_` line would otherwise swallow the next line as its own value. +const DEFINE_RE = /^#define[ \t]+(\w+)[ \t]+(.+?)[ \t]*$/gm; + +/** @type {Record} */ +const found = {}; +for (const match of text.matchAll(DEFINE_RE)) { + const [, name, value] = match; + if (name && KNOWN_MACROS.includes(name)) { + found[name] = value.replace(/^"|"$/g, ''); + } +} + +console.log(`[cef-version-diagnostics] ${versionHeaderPath}`); +for (const macro of KNOWN_MACROS) { + console.log(` ${macro} = ${found[macro] ?? '(not found)'}`); +} + +const missingIdentifying = REQUIRED_IDENTIFYING_MACROS.filter((m) => !(m in found)); +if (missingIdentifying.length > 0) { + console.error( + `[cef-version-diagnostics] Could not identify the CEF/Chromium build — missing: ${missingIdentifying.join(', ')}. Header format may have changed.`, + ); + process.exit(1); +}