Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions scripts/prebuild.mts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const REPOS: Record<Source, string> = {

export const TAG_RE = /^nightly-\d{8}-[0-9a-f]{7}$/;
export const SIZES_RE = /^sizes\.(.+)\.json$/;
export const KCONFIG_GRAPH_RE = /^kconfig-graph\.(.+)\.json$/;
export const KCONFIG_HELP_RE = /^kconfig-help\.(.+)\.json$/;

export type GhRelease = {
tagName: string;
Expand Down Expand Up @@ -76,6 +78,12 @@ export type IndexFile = {
generated_at: string;
retention: number;
builds: BuildEntry[];
// v0.3 addition: which platforms have kconfig-graph data available under
// ./data/<source>/kconfig/. Per-source (not per-build) — the configurator
// ships the latest graph snapshot only, because Kconfig dep relations
// evolve too slowly for per-build to be worth the storage hit.
// Older clients ignore this field (forward-compatible).
kconfig_available_for?: string[];
};

// Hookable shell-out + filesystem so unit tests can inject mocks without
Expand Down Expand Up @@ -127,6 +135,11 @@ export function platformFromAssetName(name: string): string | null {
return m ? m[1] : null;
}

export function platformFromKconfigGraphName(name: string): string | null {
const m = KCONFIG_GRAPH_RE.exec(name);
return m ? m[1] : null;
}

export type RunOpts = {
outDir: string;
cacheDir?: string;
Expand Down Expand Up @@ -268,12 +281,32 @@ export async function runPrebuild(opts: RunOpts): Promise<{
// Sort newest first for the index (matches manifest.json convention).
builds.sort((a, b) => b.built_at.localeCompare(a.built_at));

// v0.3: pull Kconfig graph from the newest tag only. The graph evolves
// slowly relative to nightly cadence; per-build kconfig storage was
// measured at ~3 GB raw across the retention window for negligible UX
// gain. The newest snapshot is good enough for "what can I disable on
// this board?". Help text ships per-platform alongside (small).
const kconfigAvailableFor = await downloadKconfig({
builds,
source,
repo,
sourceOut,
cacheDir,
forceRefetch,
gh,
fs,
log,
});

const index: IndexFile = {
schema: 1,
source,
generated_at: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
retention,
builds,
...(kconfigAvailableFor.length > 0
? { kconfig_available_for: kconfigAvailableFor }
: {}),
};
fs.write(join(sourceOut, "index.json"), JSON.stringify(index, null, 2) + "\n");

Expand All @@ -284,6 +317,79 @@ export async function runPrebuild(opts: RunOpts): Promise<{
return { builds: out };
}

/**
* Walk the build list newest-first and download Kconfig assets from the first
* tag that has them. Files land at:
* <sourceOut>/kconfig/<platform>.json (graph, what user can toggle)
* <sourceOut>/kconfig/<platform>.help.json (per-symbol help, lazy-load)
* Returns the platform-name list for the index's `kconfig_available_for`.
*/
async function downloadKconfig(args: {
builds: BuildEntry[];
source: Source;
repo: string;
sourceOut: string;
cacheDir: string;
forceRefetch: boolean;
gh: GhFn;
fs: FsHooks;
log: (msg: string) => void;
}): Promise<string[]> {
const { builds, source, repo, sourceOut, cacheDir, forceRefetch, gh, fs, log } = args;
const kconfigOut = join(sourceOut, "kconfig");

for (const build of builds) {
const tag = build.id;
const kcCache = join(cacheDir, source, tag, "kconfig");

const cached =
!forceRefetch &&
fs.exists(kcCache) &&
fs.listDir(kcCache).some((n) => KCONFIG_GRAPH_RE.test(n));

if (!cached) {
fs.rmDir(kcCache);
fs.mkdir(kcCache);
try {
gh([
"release",
"download",
tag,
"--repo",
repo,
"--pattern",
"kconfig-*.json",
"--dir",
kcCache,
"--skip-existing",
]);
} catch {
// No kconfig assets on this tag — try the next one.
continue;
}
}

const graphs = fs.listDir(kcCache).filter((n) => KCONFIG_GRAPH_RE.test(n));
if (graphs.length === 0) continue;

// Copy verbatim — the explorer fetches by the original
// kconfig-graph.<plat>.json / kconfig-help.<plat>.json filenames.
fs.rmDir(kconfigOut);
fs.mkdir(kconfigOut);
fs.copyDir(kcCache, kconfigOut);

const platforms = graphs
.map(platformFromKconfigGraphName)
.filter((p): p is string => p !== null)
.sort();
log(`[${source}] kconfig from ${tag}: ${platforms.length} platforms`);
return platforms;
}

log(`[${source}] no kconfig assets found in any retained tag`);
return [];
}

// --- CLI entrypoint ---------------------------------------------------------

const isMain =
Expand Down
63 changes: 43 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { ModuleTable } from "./components/ModuleTable";
import { PackageTreemap } from "./components/PackageTreemap";
import { RemovedPanel } from "./components/RemovedPanel";
import { DriftView } from "./components/DriftView";
import { WhatIfPanel } from "./components/WhatIfPanel";

type Tab = "tree" | "packages" | "modules" | "removed" | "drift";
type Tab = "tree" | "packages" | "modules" | "removed" | "drift" | "configure";

export function App() {
const initial = useMemo(() => readQueryString(window.location.search), []);
Expand Down Expand Up @@ -132,25 +133,44 @@ export function App() {
<SizeSummary sizes={sizes} />

<nav className="tabs" role="tablist">
{(
[
["tree", "Treemap"],
["packages", `Packages (${sizes.packages.length})`],
["modules", `Modules (${sizes.linux_components.modules.length})`],
["removed", `Removed-by-finalize (${sizes.removed_by_finalize.length})`],
["drift", "Drift vs another build"],
] as Array<[Tab, string]>
).map(([k, label]) => (
<button
key={k}
role="tab"
aria-selected={tab === k}
className={tab === k ? "active" : ""}
onClick={() => setTab(k)}
>
{label}
</button>
))}
{(() => {
const kconfigAvailable =
platform !== null &&
index?.kconfig_available_for?.includes(platform);
const tabs: Array<[Tab, string, boolean]> = [
["tree", "Treemap", true],
["packages", `Packages (${sizes.packages.length})`, true],
[
"modules",
`Modules (${sizes.linux_components.modules.length})`,
true,
],
[
"removed",
`Removed-by-finalize (${sizes.removed_by_finalize.length})`,
true,
],
["drift", "Drift vs another build", true],
["configure", "Configure (what-if)", !!kconfigAvailable],
];
return tabs.map(([k, label, enabled]) => (
<button
key={k}
role="tab"
aria-selected={tab === k}
className={tab === k ? "active" : ""}
disabled={!enabled}
title={
!enabled && k === "configure"
? "Kconfig graph not yet published for this platform"
: undefined
}
onClick={() => enabled && setTab(k)}
>
{label}
</button>
));
})()}
</nav>

<main>
Expand All @@ -170,6 +190,9 @@ export function App() {
platform={platform}
/>
)}
{tab === "configure" && platform && (
<WhatIfPanel source={source} platform={platform} sizes={sizes} />
)}
</main>
</>
)}
Expand Down
Loading