ShiboSoftwareDev/linux-gameboy-advance

A four-layer Linux handheld game-console carrier PCB built around an F1C100S SoC, USB-C and 2×AA power, LCD/microSD interfaces, flash, audio amplifier/speaker, controls, service headers, and RGB status LED.

Version
1.0.124
License
unset
Stars
0

scripts/cloud-build.mjs

import { spawn } from "node:child_process"
import {
  existsSync,
  mkdirSync,
  readFileSync,
  writeFileSync,
} from "node:fs"
import { fileURLToPath } from "node:url"
import { gunzipSync } from "node:zlib"
import {
  getCloudArtifactSourceState,
  sha256File,
} from "./cloud-artifact-sources.mjs"

const projectDirectory = fileURLToPath(new URL("../", import.meta.url))
const distRoot = new URL("../dist/", import.meta.url)
const distDirectory = new URL("../dist/index/", import.meta.url)
const configuredPreviewDirectory = new URL(
  "../dist/index.circuit/",
  import.meta.url,
)
const manifestPath = new URL("../cloud-artifacts/manifest.json", import.meta.url)
const prebuiltArtifacts = [
  {
    source: new URL("../cloud-artifacts/index.html.gz", import.meta.url),
    destination: new URL("index.html", distRoot),
    compressed: true,
  },
  {
    source: new URL(
      "../cloud-artifacts/index.circuit.json.gz",
      import.meta.url,
    ),
    destination: new URL("circuit.json", distDirectory),
    compressed: true,
  },
  {
    source: new URL("../cloud-artifacts/index.pcb.svg.gz", import.meta.url),
    destination: new URL("pcb.svg", distDirectory),
    compressed: true,
  },
  {
    source: new URL(
      "../cloud-artifacts/index.schematic.svg.gz",
      import.meta.url,
    ),
    destination: new URL("schematic.svg", distDirectory),
    compressed: true,
  },
  {
    source: new URL("../cloud-artifacts/index.3d.png", import.meta.url),
    destination: new URL("3d.png", distDirectory),
    compressed: false,
  },
]

// The full board render is intentionally performed and verified locally before
// publishing. Cloud workers are substantially slower on this dense four-layer
// design and can be killed by the registry's fixed 30-minute outer timeout.
// Restoring the verified Circuit JSON keeps the package's interactive PCB,
// schematic, and 3D views intact without rerunning the expensive autorouter.
const prebuiltArtifactCount = prebuiltArtifacts.filter(({ source }) =>
  existsSync(source),
).length

if (prebuiltArtifactCount > 0) {
  if (prebuiltArtifactCount !== prebuiltArtifacts.length) {
    throw new Error(
      `Prebuilt cloud artifact set is incomplete (${prebuiltArtifactCount}/${prebuiltArtifacts.length})`,
    )
  }

  if (!existsSync(manifestPath)) {
    throw new Error(
      "Prebuilt cloud artifacts have no freshness manifest; run `bun run prepare:cloud-artifact`",
    )
  }

  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"))
  const sourceState = getCloudArtifactSourceState(projectDirectory)
  if (manifest.sourceSha256 !== sourceState.sourceSha256) {
    const expectedFiles = new Map(
      (manifest.sourceFiles ?? []).map((file) => [file.path, file.sha256]),
    )
    const actualFiles = new Map(
      sourceState.sourceFiles.map((file) => [file.path, file.sha256]),
    )
    const changedInputs = [...new Set([
      ...expectedFiles.keys(),
      ...actualFiles.keys(),
    ])]
      .filter((path) => expectedFiles.get(path) !== actualFiles.get(path))
      .sort()
      .slice(0, 12)

    throw new Error(
      `Prebuilt cloud artifacts are stale for the uploaded board source; rebuild and run \`bun run prepare:cloud-artifact\`. Changed inputs: ${changedInputs.join(", ") || "unknown"}`,
    )
  }

  for (const artifact of prebuiltArtifacts) {
    const artifactName = fileURLToPath(artifact.source).split("/").at(-1)
    const expectedHash = manifest.artifactSha256?.[artifactName]
    const actualHash = sha256File(fileURLToPath(artifact.source))
    if (!expectedHash || expectedHash !== actualHash) {
      throw new Error(`Prebuilt cloud artifact integrity check failed: ${artifactName}`)
    }
  }

  const circuitArtifact = prebuiltArtifacts[1]
  const circuitJsonText = gunzipSync(
    readFileSync(circuitArtifact.source),
  ).toString("utf8")
  const circuitJson = JSON.parse(circuitJsonText)

  if (!Array.isArray(circuitJson) || circuitJson.length === 0) {
    throw new Error("Prebuilt cloud Circuit JSON is empty or malformed")
  }

  mkdirSync(distRoot, { recursive: true })
  mkdirSync(distDirectory, { recursive: true })

  for (const artifact of prebuiltArtifacts) {
    const artifactBytes = readFileSync(artifact.source)
    writeFileSync(
      artifact.destination,
      artifact.compressed ? gunzipSync(artifactBytes) : artifactBytes,
    )
  }

  // tscircuit.com resolves previewComponentPath="index.circuit.tsx" to this
  // exact artifact path. Keep the conventional dist/index/circuit.json too,
  // because the CLI's download/export tooling uses that layout.
  mkdirSync(configuredPreviewDirectory, { recursive: true })
  writeFileSync(
    new URL("circuit.json", configuredPreviewDirectory),
    circuitJsonText,
  )

  console.log(
    `[cloud-build] restored ${prebuiltArtifacts.length} verified site artifacts (${circuitJson.length} elements)`,
  )
  process.exit(0)
}

const buildArgs = [
  "build",
  "index.circuit.tsx",
  "--disable-parts-engine",
  // The package page renders its interactive views from circuit.json. Avoid
  // --site/--preview-images here because both force a headless 3D PNG render;
  // that optional render can stall on the cloud worker until its 30-minute
  // outer timeout, even though circuit generation has already succeeded.
  "--pcb-svgs",
  "--schematic-svgs",
  // Preserve the generated artifacts when the cloud-only geometry reporter
  // emits its synthetic DRC exception. Real validation is performed by the
  // explicit source, netlist, via, and Gerber-shorts checks before publishing.
  "--ignore-errors",
  // The CLI's routing-DRC geometry pass currently crashes on this four-layer
  // board (`relate(shape1, shape2).covered`) after circuit generation.  The
  // project runs its source/netlist/Gerber-shorts verification separately, so
  // skip only that broken reporting pass for the website artifact build.
  "--ignore-routing-drc",
  "--autorouter-timeout",
  "15m",
  "--concurrency",
  "1",
]

const startedAt = Date.now()
const child = spawn(process.platform === "win32" ? "tsci.cmd" : "tsci", buildArgs, {
  env: process.env,
  stdio: "inherit",
})

const heartbeat = setInterval(() => {
  const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000)
  console.log(`[cloud-build] tscircuit is still running (${elapsedSeconds}s elapsed)`)
}, 30_000)

const forwardSignal = (signal) => {
  if (!child.killed) child.kill(signal)
}

process.once("SIGINT", () => forwardSignal("SIGINT"))
process.once("SIGTERM", () => forwardSignal("SIGTERM"))

const exitCode = await new Promise((resolve) => {
  child.once("error", (error) => {
    console.error(`[cloud-build] failed to start tscircuit: ${error.message}`)
    resolve(1)
  })
  child.once("exit", (code, signal) => {
    if (signal) console.error(`[cloud-build] tscircuit stopped by ${signal}`)
    resolve(code ?? 1)
  })
})

clearInterval(heartbeat)
process.exitCode = exitCode