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