diff --git a/book/vscode-ext/src/commands/buildCommands.ts b/book/vscode-ext/src/commands/buildCommands.ts index 38d2c1f91..daa4acc46 100644 --- a/book/vscode-ext/src/commands/buildCommands.ts +++ b/book/vscode-ext/src/commands/buildCommands.ts @@ -5,6 +5,7 @@ import { discoverChapters } from '../utils/chapters'; import { getRepoRoot, parseQmdFile } from '../utils/workspace'; import { runInVisibleTerminal } from '../utils/terminal'; import { getQuartoResetAllFormatsCommand, withQuartoResetPrefix } from '../utils/quartoConfigReset'; +import { showBuildManifest } from '../utils/buildManifest'; /** Map volume → PDF filename (derived from book title in Quarto config). */ const PDF_FILENAMES: Record = { @@ -100,6 +101,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void { const buildCmd = `./book/binder build ${fmtLower} --${vol} -v`; const label = `Build Volume ${fmt.toUpperCase()} (${vol})`; const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd); + showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd }); if (fmtLower === 'pdf') { runPdfBuildAndOpen(fullCmd, root, vol, label); } else { @@ -125,6 +127,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void { const buildCmd = `./book/binder build ${fmtLower} --${vol} -v`; const label = `Build Full Volume ${fmtLower.toUpperCase()} (${vol})`; const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd); + showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd }); if (fmtLower === 'pdf') { runPdfBuildAndOpen(fullCmd, root, vol, label); } else { @@ -170,6 +173,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void { const buildCmd = `./book/binder build ${fmtLower} ${chapterList} --${vol} -v`; const label = `Build Chapters ${fmtLower.toUpperCase()} (${vol}): ${chapters.length} chapter(s)`; const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd); + showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd }); if (fmtLower === 'pdf') { runPdfBuildAndOpen(fullCmd, root, vol, label); } else { diff --git a/book/vscode-ext/src/commands/debugCommands.ts b/book/vscode-ext/src/commands/debugCommands.ts index 80fbfc0cf..29b568b51 100644 --- a/book/vscode-ext/src/commands/debugCommands.ts +++ b/book/vscode-ext/src/commands/debugCommands.ts @@ -3,6 +3,7 @@ import { VolumeId } from '../types'; import { getRepoRoot } from '../utils/workspace'; import { discoverChapters } from '../utils/chapters'; import { runInVisibleTerminal } from '../utils/terminal'; +import { showBuildManifest } from '../utils/buildManifest'; import { cancelActiveDebugSession, getDebugSessionById, @@ -67,6 +68,17 @@ async function runTestAllChapters( await context.workspaceState.update(STATE_LAST_PARALLEL_FORMAT, format); const allChapters = volume.chapters.map(ch => ch.name); + + const parallelCmd = `./book/binder ${format} reset --${volumeId} && [parallel: ${workers} workers × ./book/binder build ${format} --${volumeId} -v]`; + showBuildManifest({ + repoRoot, + vol: volumeId, + format, + mode: 'parallel', + command: parallelCmd, + workers, + }); + vscode.window.showInformationMessage( `Testing ${allChapters.length} chapters (${format.toUpperCase()}, ${workers} workers)...` ); @@ -102,6 +114,13 @@ async function runDebugAllChapters( await context.workspaceState.update(STATE_LAST_PARALLEL_VOLUME, selection.id); const cmd = `./book/binder debug pdf --${selection.id} -v`; + showBuildManifest({ + repoRoot: root, + vol: selection.id, + format: 'pdf', + mode: 'sequential', + command: cmd, + }); runInVisibleTerminal(cmd, root, `Debug All Chapters (${selection.id})`); } diff --git a/book/vscode-ext/src/utils/buildManifest.ts b/book/vscode-ext/src/utils/buildManifest.ts new file mode 100644 index 000000000..6b4abdef2 --- /dev/null +++ b/book/vscode-ext/src/utils/buildManifest.ts @@ -0,0 +1,70 @@ +import { VolumeId, BuildFormat } from '../types'; +import { getBuildChannel } from './terminal'; +import { getBuildEntriesForFormat } from './chapters'; + +const BAR = '═'.repeat(70); +const THIN = '─'.repeat(70); + +export interface BuildManifestOptions { + repoRoot: string; + vol: VolumeId; + format: BuildFormat; + /** 'sequential' for series builds; 'parallel' for worktree-based parallel debug. */ + mode: 'sequential' | 'parallel'; + /** The exact shell command that will run in the terminal. */ + command: string; + /** Worker count (parallel mode only). */ + workers?: number; +} + +/** + * Writes a pre-flight build manifest to the MLSysBook Build output channel. + * + * Reads the YML config (comment markers stripped) so the list always reflects + * the full intended volume structure — not just what is currently uncommented. + * Add a new chapter to the YML (even commented out) and it will appear here + * and be included in the build after `binder reset` runs. + */ +export function showBuildManifest(opts: BuildManifestOptions): void { + const { repoRoot, vol, format, mode, command, workers } = opts; + const volLabel = vol === 'vol1' ? 'Volume I' : 'Volume II'; + const { configFile, relPaths } = getBuildEntriesForFormat(repoRoot, vol, format); + const now = new Date().toLocaleString(); + const modeLabel = + mode === 'parallel' ? `Parallel (${workers ?? 4} workers)` : 'Sequential'; + + const ch = getBuildChannel(); + ch.appendLine(''); + ch.appendLine(BAR); + ch.appendLine(' MLSysBook · Build Manifest'); + ch.appendLine(BAR); + ch.appendLine(''); + ch.appendLine(` Volume : ${volLabel} (${vol})`); + ch.appendLine(` Format : ${format.toUpperCase()}`); + ch.appendLine(` Mode : ${modeLabel}`); + ch.appendLine(` Config : ${configFile}`); + ch.appendLine(` Started : ${now}`); + ch.appendLine(''); + + if (relPaths.length === 0) { + ch.appendLine(' (no entries found — check config path)'); + } else { + ch.appendLine(` Files to build (${relPaths.length} total)`); + ch.appendLine(` ${THIN}`); + relPaths.forEach((p, i) => { + ch.appendLine(` [${String(i + 1).padStart(2, '0')}] ${p}`); + }); + } + + ch.appendLine(''); + ch.appendLine(` ${THIN}`); + ch.appendLine(` Command: ${command}`); + ch.appendLine(` ${THIN}`); + ch.appendLine(''); + ch.appendLine(' Starting build in terminal...'); + ch.appendLine(''); + ch.appendLine(BAR); + ch.appendLine(''); + + ch.show(false); +} diff --git a/book/vscode-ext/src/utils/chapters.ts b/book/vscode-ext/src/utils/chapters.ts index 09677fa93..543bccde4 100644 --- a/book/vscode-ext/src/utils/chapters.ts +++ b/book/vscode-ext/src/utils/chapters.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { ChapterInfo, ChapterOrderSource, VolumeId, VolumeInfo } from '../types'; +import { BuildFormat, ChapterInfo, ChapterOrderSource, VolumeId, VolumeInfo } from '../types'; function toDisplayName(dirName: string): string { return dirName @@ -119,6 +119,27 @@ function pathToChapterInfo( }; } +/** + * Returns all buildable paths for a given format by reading the YML config, + * stripping comment markers so every entry — whether currently commented out + * or active — is included in the correct book order. + * The YML is the single source of truth: add a chapter there (even commented) + * and it will appear in the manifest and be built after `binder reset`. + */ +export function getBuildEntriesForFormat( + repoRoot: string, + vol: VolumeId, + format: BuildFormat, +): { configFile: string; relPaths: string[] } { + const source = format as ChapterOrderSource; + const entries = readBuildablePathsFromConfig(repoRoot, vol, source); + const configFile = path.join('book', 'quarto', 'config', `_quarto-${format}-${vol}.yml`); + return { + configFile, + relPaths: entries.map(e => e.relPath), + }; +} + export function discoverChapters(repoRoot: string): VolumeInfo[] { const volumes: VolumeInfo[] = []; const chapterOrderSource = vscode.workspace diff --git a/book/vscode-ext/src/utils/terminal.ts b/book/vscode-ext/src/utils/terminal.ts index b143c1aa8..296026c56 100644 --- a/book/vscode-ext/src/utils/terminal.ts +++ b/book/vscode-ext/src/utils/terminal.ts @@ -164,6 +164,11 @@ function getLastFailure(): LastFailureState | undefined { return context.workspaceState.get(STATE_LAST_FAILURE_KEY); } +/** Exposed so build manifest can append to the same channel without creating a duplicate. */ +export function getBuildChannel(): vscode.OutputChannel { + return getOutputChannel(); +} + export function initializeRunManager(context: vscode.ExtensionContext): void { extensionContext = context; const savedRuns = context.workspaceState.get(STATE_COMMAND_HISTORY_KEY, []);