diff --git a/rules/publish.ts b/rules/publish.ts index e16bf8c7..ff75b087 100644 --- a/rules/publish.ts +++ b/rules/publish.ts @@ -11,7 +11,7 @@ export type Sprite = Artifact | SrcFile; // The unhashed -> hashed name mapping published beside a stamped set. export class Manifest { readonly ctx: DeployCtx; - private entries = new Map(); + #entries = new Map(); constructor(ctx: DeployCtx) { this.ctx = ctx; @@ -20,25 +20,25 @@ export class Manifest { set(key: string, value: string): void { // ActionQueue only dedups final dsts; hashed dsts differ even when // unhashed names collide, so check the key explicitly. - if (this.entries.has(key)) { + if (this.#entries.has(key)) { throw new Error(`duplicate sprite name ${key}`); } - this.entries.set(key, value); + this.#entries.set(key, value); } write(dst: string): void { let sorted: Record = {}; - for (let k of [...this.entries.keys()].sort()) { - sorted[k] = this.entries.get(k)!; + for (let k of [...this.#entries.keys()].sort()) { + sorted[k] = this.#entries.get(k)!; } this.ctx.write(dst, JSON.stringify(sorted, null, 4) + "\n"); } } -export interface Dest { - dir: string; - ext?: string; -} +export type Dest = { + dir: string, + ext?: string, +}; function extOf(f: Sprite, ext?: string): string { let result = ext ?? f.ext; diff --git a/tools/build/artifact.ts b/tools/build/artifact.ts index 4965705d..0165c1f3 100644 --- a/tools/build/artifact.ts +++ b/tools/build/artifact.ts @@ -15,7 +15,7 @@ export class Artifact { readonly ext: string; // "png", "gif", ... (no dot) readonly decl: RuleDecl; readonly index: number; // position among decl.outputs - private digest: string | null = null; + #digest: string | null = null; constructor(name: string, ext: string, decl: RuleDecl, index: number) { this.name = name; @@ -34,46 +34,46 @@ export class Artifact { } get hash(): string { - if (this.digest === null) { + if (this.#digest === null) { throw new Error(`Artifact ${this.filename} has not been built yet`); } - return this.digest; + return this.#digest; } resolve(digest: string): void { - if (this.digest !== null && this.digest !== digest) { + if (this.#digest !== null && this.#digest !== digest) { throw new Error(`Artifact ${this.filename} resolved twice with different digests`); } - this.digest = digest; + this.#digest = digest; } } export type Input = string | Artifact; // string = source path relative to repo root -export interface CmdSpec { - display?: string; +export type CmdSpec = { + display?: string, // Tracked-but-not-substituted inputs: part of rule identity, but never // expanded into %f. Use for files a tool reads on its own (e.g. // tools/sheet readdirs the minisprite directories). - deps?: Input | Input[]; + deps?: Input | Input[], // IMPORTANT: set this on any rule whose output BYTES depend on input // NAMES (a tool that readdirs, or parses ids out of %f filenames, e.g. // the spritesheet builders). Identity is normally content-only, so // without this flag a same-bytes rename would leave the output stale. - nameSensitive?: boolean; - cmds: subst.Cmd[]; -} + nameSensitive?: boolean, + cmds: subst.Cmd[], +}; -export interface RuleDecl { +export type RuleDecl = { id: number; // registration order; identity for artifact inputs inputs: Input[]; // ordered (%f order; some rules are order-sensitive) - deps: Input[]; - outputs: Artifact[]; + deps: Input[], + outputs: Artifact[], cmds: string[]; // flattened PRE-substitution templates display: string | null; // nominally substituted; cosmetic displayTemplate: string | null; // pre-substitution; groups forEach rules - nameSensitive: boolean; -} + nameSensitive: boolean, +}; let decls: RuleDecl[] = []; // Declaring an identical rule twice returns the existing artifacts, so diff --git a/tools/build/cas.ts b/tools/build/cas.ts index cbe5f0ca..3829306b 100644 --- a/tools/build/cas.ts +++ b/tools/build/cas.ts @@ -31,10 +31,10 @@ export function casStat(casDir: string, digest: string, ext: string): bigint | n } } -export interface CasObject { +export type CasObject = { digest: string; // sha256 hex of the bytes - size: bigint; -} + size: bigint, +}; // Move tmpPath into the store, returning the content digest and size. An // existing object is trusted only if its bytes actually hash to the digest; diff --git a/tools/build/driver.ts b/tools/build/driver.ts index cac0f522..0f346ace 100644 --- a/tools/build/driver.ts +++ b/tools/build/driver.ts @@ -6,28 +6,28 @@ import * as executor from './executor.ts'; import {reconcileHashes} from './hash.ts'; import {type Store} from './store.ts'; -export interface BuildOpts { - root: string; - store: Store; +export type BuildOpts = { + root: string, + store: Store, casDir: string; // relative to root; substituted into commands - tmpDir: string; - jobs: number; - dryRun: boolean; - failFast: boolean; - verbose: boolean; + tmpDir: string, + jobs: number, + dryRun: boolean, + failFast: boolean, + verbose: boolean, // GC after a fully successful build: drop rules whose key is no longer // declared, sweep unreferenced CAS objects, prune the file cache. Only // safe when `decls` is the full rule universe (a partial build would GC // the other deploys' state), so the CLI sets it for union builds only. - gc: boolean; - signal: AbortSignal; - log?: (line: string) => void; - logError?: (line: string) => void; -} + gc: boolean, + signal: AbortSignal, + log?: (line: string) => void, + logError?: (line: string) => void, +}; -export interface DriveResult extends executor.BuildResult { - interrupted: boolean; -} +export type DriveResult = executor.BuildResult & { + interrupted: boolean, +}; export async function build(decls: readonly RuleDecl[], opts: BuildOpts): Promise { let log = opts.log ?? console.log; diff --git a/tools/build/exec.ts b/tools/build/exec.ts index d8b39fd3..1e991148 100644 --- a/tools/build/exec.ts +++ b/tools/build/exec.ts @@ -1,12 +1,12 @@ import {spawn} from 'node:child_process'; -export interface ExecResult { - code: number | null; - signal: NodeJS.Signals | null; - output: string; - durationMs: number; -} +export type ExecResult = { + code: number | null, + signal: NodeJS.Signals | null, + output: string, + durationMs: number, +}; let livePids = new Set(); diff --git a/tools/build/executor.ts b/tools/build/executor.ts index 856f4742..d4039960 100644 --- a/tools/build/executor.ts +++ b/tools/build/executor.ts @@ -18,26 +18,26 @@ export type RuleOutcome = | {status: 'failed', message: string} | {status: 'blocked'}; // a producer failed -export interface BuildResult { +export type BuildResult = { outcomes: Map; // no entry = not attempted (aborted) keys: Map; // only decls whose key resolved ok: boolean; // every decl clean or ran -} +}; -export interface ExecutorOpts { +export type ExecutorOpts = { root: string; // cwd for commands - store: Store; + store: Store, casDir: string; // relative to root (substituted into commands) - tmpDir: string; - jobs: number; - dryRun: boolean; - failFast: boolean; + tmpDir: string, + jobs: number, + dryRun: boolean, + failFast: boolean, verbose?: boolean; // annotate run lines with the dirty reason sourceHashes: Map; // every source input/dep, pre-reconciled - signal: AbortSignal; - log?: (line: string) => void; - logError?: (line: string) => void; -} + signal: AbortSignal, + log?: (line: string) => void, + logError?: (line: string) => void, +}; export function label(decl: artifact.RuleDecl): string { return decl.display ?? decl.cmds[0]!; @@ -54,27 +54,27 @@ class DryDirty extends Error {} class Aborted extends Error {} class Semaphore { - private available: number; - private waiters: (() => void)[] = []; + #available: number; + #waiters: (() => void)[] = []; constructor(n: number) { - this.available = n; + this.#available = n; } async acquire(): Promise { - if (this.available > 0) { - this.available--; + if (this.#available > 0) { + this.#available--; return; } - await new Promise(resolve => this.waiters.push(resolve)); + await new Promise(resolve => this.#waiters.push(resolve)); } release(): void { - let waiter = this.waiters.shift(); + let waiter = this.#waiters.shift(); if (waiter !== undefined) { waiter(); } else { - this.available++; + this.#available++; } } } @@ -85,144 +85,144 @@ class Semaphore { // The artifact graph is a DAG by construction (a rule can only reference // artifacts that already exist as values), so there is no cycle check. export class Executor { - private opts: ExecutorOpts; - private memo = new Map>(); - private inflightByKey = new Map>(); - private outcomes = new Map(); - private keys = new Map(); - private semaphore: Semaphore; - private failAc = new AbortController(); - private runSignal: AbortSignal; - private counter = 0; - private tmpSeq = 0; - private log: (line: string) => void; - private logError: (line: string) => void; + #opts: ExecutorOpts; + #memo = new Map>(); + #inflightByKey = new Map>(); + #outcomes = new Map(); + #keys = new Map(); + #semaphore: Semaphore; + #failAc = new AbortController(); + #runSignal: AbortSignal; + #counter = 0; + #tmpSeq = 0; + #log: (line: string) => void; + #logError: (line: string) => void; constructor(opts: ExecutorOpts) { - this.opts = opts; - this.semaphore = new Semaphore(opts.jobs); - this.runSignal = AbortSignal.any([opts.signal, this.failAc.signal]); - this.log = opts.log ?? console.log; - this.logError = opts.logError ?? console.error; + this.#opts = opts; + this.#semaphore = new Semaphore(opts.jobs); + this.#runSignal = AbortSignal.any([opts.signal, this.#failAc.signal]); + this.#log = opts.log ?? console.log; + this.#logError = opts.logError ?? console.error; } async build(decls: readonly artifact.RuleDecl[]): Promise { - await Promise.allSettled(decls.map(d => this.demand(d))); + await Promise.allSettled(decls.map(d => this.#demand(d))); let ok = decls.every(d => { - let status = this.outcomes.get(d)?.status; + let status = this.#outcomes.get(d)?.status; return status === 'clean' || status === 'ran'; }); - return {outcomes: this.outcomes, keys: this.keys, ok}; + return {outcomes: this.#outcomes, keys: this.#keys, ok}; } - private demand(decl: artifact.RuleDecl): Promise { - let p = this.memo.get(decl); + #demand(decl: artifact.RuleDecl): Promise { + let p = this.#memo.get(decl); if (p === undefined) { // Any non-sentinel escape (a store error, a resolve conflict, a // bug) must surface as a reported failure, not vanish into the // allSettled in build(). - p = this.demandInner(decl).catch(err => { + p = this.#demandInner(decl).catch(err => { if (err instanceof RuleFailed || err instanceof DryDirty || err instanceof Aborted) { throw err; } - this.outcomes.set(decl, {status: 'failed', message: 'internal error'}); - this.logError(`FAILED (internal error): ${label(decl)}`); - this.logError(indent(err instanceof Error ? err.stack ?? err.message : String(err))); - this.failAc.abort(); + this.#outcomes.set(decl, {status: 'failed', message: 'internal error'}); + this.#logError(`FAILED (internal error): ${label(decl)}`); + this.#logError(indent(err instanceof Error ? err.stack ?? err.message : String(err))); + this.#failAc.abort(); throw new RuleFailed(); }); - this.memo.set(decl, p); + this.#memo.set(decl, p); } return p; } - private async digestOf(i: artifact.Input): Promise { + async #digestOf(i: artifact.Input): Promise { if (typeof i !== 'string') { - await this.demand(i.decl); + await this.#demand(i.decl); return i.hash; } - let hash = this.opts.sourceHashes.get(i); + let hash = this.#opts.sourceHashes.get(i); if (hash === undefined) { throw new BuildError(`No hash for source ${i}`); } return hash.toString('hex'); } - private async demandInner(decl: artifact.RuleDecl): Promise { + async #demandInner(decl: artifact.RuleDecl): Promise { let digests: Map; try { let inputs = [...decl.inputs, ...decl.deps]; - let resolved = await Promise.all(inputs.map(i => this.digestOf(i))); + let resolved = await Promise.all(inputs.map(i => this.#digestOf(i))); digests = new Map(inputs.map((i, n) => [i, resolved[n]!])); } catch (err) { if (err instanceof RuleFailed) { - this.outcomes.set(decl, {status: 'blocked'}); + this.#outcomes.set(decl, {status: 'blocked'}); } else if (err instanceof DryDirty) { - this.outcomes.set(decl, {status: 'would-run', reason: 'blocked'}); - this.log(`would run (blocked by dirty producer): ${label(decl)}`); + this.#outcomes.set(decl, {status: 'would-run', reason: 'blocked'}); + this.#log(`would run (blocked by dirty producer): ${label(decl)}`); } throw err; } let key = artifact.computeKey(decl, i => digests.get(i)!); - this.keys.set(decl, key); + this.#keys.set(decl, key); // Byte-identical duplicate declarations share one execution. - let existing = this.inflightByKey.get(key); + let existing = this.#inflightByKey.get(key); if (existing !== undefined) { try { let shared = await existing; decl.outputs.forEach((o, n) => o.resolve(shared[n]!)); - this.outcomes.set(decl, {status: 'clean'}); + this.#outcomes.set(decl, {status: 'clean'}); return shared; } catch (err) { if (err instanceof RuleFailed) { - this.outcomes.set(decl, {status: 'blocked'}); + this.#outcomes.set(decl, {status: 'blocked'}); } else if (err instanceof DryDirty) { - this.outcomes.set(decl, {status: 'would-run', reason: 'blocked'}); + this.#outcomes.set(decl, {status: 'would-run', reason: 'blocked'}); } throw err; } } - let work = this.perform(decl, key); - this.inflightByKey.set(key, work); + let work = this.#perform(decl, key); + this.#inflightByKey.set(key, work); let result = await work; decl.outputs.forEach((o, n) => o.resolve(result[n]!)); return result; } - private async perform(decl: artifact.RuleDecl, key: string): Promise { - let {store, casDir} = this.opts; + async #perform(decl: artifact.RuleDecl, key: string): Promise { + let {store, casDir} = this.#opts; let stored = store.lookupRule(key); if (stored !== null && stored.length === decl.outputs.length && stored.every((o, n) => o.ext === decl.outputs[n]!.ext) && stored.every(o => cas.casStat(casDir, o.digest, o.ext) === o.size)) { - this.outcomes.set(decl, {status: 'clean'}); + this.#outcomes.set(decl, {status: 'clean'}); return stored.map(o => o.digest); } let reason: DirtyReason = stored === null ? 'new' : 'cas-missing'; - if (this.opts.dryRun) { - this.outcomes.set(decl, {status: 'would-run', reason}); - this.log(`would run (${reason}): ${label(decl)}`); + if (this.#opts.dryRun) { + this.#outcomes.set(decl, {status: 'would-run', reason}); + this.#log(`would run (${reason}): ${label(decl)}`); throw new DryDirty(); } - await this.semaphore.acquire(); + await this.#semaphore.acquire(); try { - if (this.runSignal.aborted) { + if (this.#runSignal.aborted) { throw new Aborted(); } - return await this.execute(decl, key, reason); + return await this.#execute(decl, key, reason); } finally { - this.semaphore.release(); + this.#semaphore.release(); } } - private async execute(decl: artifact.RuleDecl, key: string, reason: DirtyReason): Promise { - let {store, casDir, tmpDir, root} = this.opts; - let ruleTmp = pathlib.join(tmpDir, String(this.tmpSeq++)); + async #execute(decl: artifact.RuleDecl, key: string, reason: DirtyReason): Promise { + let {store, casDir, tmpDir, root} = this.#opts; + let ruleTmp = pathlib.join(tmpDir, String(this.#tmpSeq++)); fs.mkdirSync(ruleTmp, {recursive: true}); let tempOutputs = decl.outputs.map(o => pathlib.join(ruleTmp, o.filename)); let concreteInputs = decl.inputs.map( @@ -230,8 +230,8 @@ export class Executor { let command = decl.cmds.map(c => substitute(c, concreteInputs, tempOutputs)).join(' && '); try { - let result = await runShell(command, {cwd: root, signal: this.runSignal}); - if (this.runSignal.aborted && result.code !== 0) { + let result = await runShell(command, {cwd: root, signal: this.#runSignal}); + if (this.#runSignal.aborted && result.code !== 0) { throw new Aborted(); // killed by the abort, not a real failure; stays dirty } if (result.code === 0) { @@ -242,18 +242,18 @@ export class Executor { return {digest: object.digest, ext: o.ext, size: object.size}; }); store.recordRule(key, decl.cmds, outputs); - this.outcomes.set(decl, {status: 'ran', reason}); - this.log(`[${++this.counter}] ${label(decl)}` - + (this.opts.verbose ? ` (${reason})` : '')); + this.#outcomes.set(decl, {status: 'ran', reason}); + this.#log(`[${++this.#counter}] ${label(decl)}` + + (this.#opts.verbose ? ` (${reason})` : '')); if (result.output !== '') { - this.log(indent(result.output)); + this.#log(indent(result.output)); } return outputs.map(o => o.digest); } - return this.fail(decl, command, result.output, + return this.#fail(decl, command, result.output, `command succeeded but did not produce: ${missing.map(p => pathlib.basename(p)).join(' ')}`); } - return this.fail(decl, command, result.output, `exit status ${result.code ?? result.signal}`); + return this.#fail(decl, command, result.output, `exit status ${result.code ?? result.signal}`); } finally { fs.rmSync(ruleTmp, {recursive: true, force: true}); } @@ -261,15 +261,15 @@ export class Executor { // Nothing is recorded for a failure: failed and never-ran are the same // state, so the rule stays dirty. - private fail(decl: artifact.RuleDecl, command: string, output: string, message: string): never { - this.outcomes.set(decl, {status: 'failed', message}); - this.logError(`FAILED: ${label(decl)} (${message})`); - this.logError(` command: ${command}`); + #fail(decl: artifact.RuleDecl, command: string, output: string, message: string): never { + this.#outcomes.set(decl, {status: 'failed', message}); + this.#logError(`FAILED: ${label(decl)} (${message})`); + this.#logError(` command: ${command}`); if (output !== '') { - this.logError(indent(output)); + this.#logError(indent(output)); } - if (this.opts.failFast) { - this.failAc.abort(); + if (this.#opts.failFast) { + this.#failAc.abort(); } throw new RuleFailed(); } diff --git a/tools/build/hash.ts b/tools/build/hash.ts index 67b6b229..b9a4a56c 100644 --- a/tools/build/hash.ts +++ b/tools/build/hash.ts @@ -2,21 +2,21 @@ import * as fs from 'node:fs'; import {createHash} from 'node:crypto'; -export interface FileStat { - size: bigint; - mtimeNs: bigint; - hash: Buffer; -} +export type FileStat = { + size: bigint, + mtimeNs: bigint, + hash: Buffer, +}; export function hashFileSync(path: string): Buffer { return createHash('sha256').update(fs.readFileSync(path)).digest(); } -export interface ReconcileResult { +export type ReconcileResult = { hashes: Map; // current content hash for every extant path updated: Map; // cache entries that changed (to persist) missing: string[]; // paths that don't exist or aren't files -} +}; // Content hashes with a stat cache: only files whose (size, mtime_ns) changed // since the recorded cache entry are rehashed. mtime_ns exceeds 2^53, hence diff --git a/tools/build/helpers.ts b/tools/build/helpers.ts index 21383c82..1f981cce 100644 --- a/tools/build/helpers.ts +++ b/tools/build/helpers.ts @@ -65,10 +65,10 @@ export function base(x: string | Artifact): string { return typeof x === 'string' ? basenameNoExt(x) : x.name; } -export interface SpriteData { - id: string; - data: Record; -} +export type SpriteData = { + id: string, + data: Record, +}; // Port of util/sprites.lua spritedata. Lua used gmatch("[^-]+"), which skips // empty segments, hence the filter. @@ -111,11 +111,11 @@ export function trimimg(opts: {input?: string, output?: string} = {}): string { return `magick convert ${opts.input ?? '%f'} ${PNG_DETERMINISTIC} -trim ${opts.output ?? '%o'}`; } -interface CompressOpts { - pngquant?: string; - optipng?: string; - advpng?: string; -} +type CompressOpts = { + pngquant?: string, + optipng?: string, + advpng?: string, +}; function compressopts(program: string, copts: CompressOpts): void { copts.pngquant = getconfig(`${program}_PNGQUANT`) ?? copts.pngquant; diff --git a/tools/build/store.ts b/tools/build/store.ts index 837ac6e3..c7e46774 100644 --- a/tools/build/store.ts +++ b/tools/build/store.ts @@ -6,11 +6,11 @@ import {DatabaseSync} from 'node:sqlite'; import {BuildError} from './errors.ts'; import type {FileStat} from './hash.ts'; -export interface StoredOutput { +export type StoredOutput = { digest: string; // sha256 hex of the bytes; the CAS object is . - ext: string; + ext: string, size: bigint; // verified against the object on every clean check -} +}; let DDL = ` CREATE TABLE IF NOT EXISTS file_cache ( @@ -45,38 +45,38 @@ function userVersion(db: DatabaseSync): number { } export class Store { - private db: DatabaseSync; + #db: DatabaseSync; constructor(dbPath: string) { fs.mkdirSync(pathlib.dirname(dbPath), {recursive: true}); - this.db = new DatabaseSync(dbPath, {readBigInts: true}); - this.db.exec('PRAGMA journal_mode = WAL'); - this.db.exec('PRAGMA foreign_keys = ON'); - this.db.exec('PRAGMA synchronous = NORMAL'); - this.migrate(); + this.#db = new DatabaseSync(dbPath, {readBigInts: true}); + this.#db.exec('PRAGMA journal_mode = WAL'); + this.#db.exec('PRAGMA foreign_keys = ON'); + this.#db.exec('PRAGMA synchronous = NORMAL'); + this.#migrate(); } - private transaction(fn: () => T): T { - this.db.exec('BEGIN'); + #transaction(fn: () => T): T { + this.#db.exec('BEGIN'); try { let result = fn(); - this.db.exec('COMMIT'); + this.#db.exec('COMMIT'); return result; } catch (err) { - this.db.exec('ROLLBACK'); + this.#db.exec('ROLLBACK'); throw err; } } - private migrate(): void { - let version = userVersion(this.db); + #migrate(): void { + let version = userVersion(this.#db); if (version === 0) { - this.db.exec('BEGIN;' + DDL + 'PRAGMA user_version = 2; COMMIT;'); + this.#db.exec('BEGIN;' + DDL + 'PRAGMA user_version = 2; COMMIT;'); } else if (version === 1) { // v1 stored fixed-name outputs and per-input hashes; none of it // maps to the content-addressed model. Keep the file_cache (same // schema, saves rehashing every source) and drop the rest. - this.db.exec(`BEGIN; + this.#db.exec(`BEGIN; DROP TABLE rule_inputs; DROP TABLE rule_outputs; DROP TABLE rules; @@ -90,7 +90,7 @@ export class Store { loadFileCache(): Map { let result = new Map(); - let rows = this.db.prepare('SELECT path, size, mtime_ns, hash FROM file_cache').all() as + let rows = this.#db.prepare('SELECT path, size, mtime_ns, hash FROM file_cache').all() as unknown as {path: string, size: bigint, mtime_ns: bigint, hash: Uint8Array}[]; for (let row of rows) { let h = row.hash; @@ -101,11 +101,11 @@ export class Store { } saveFileCache(entries: Map): void { - let upsert = this.db.prepare(` + let upsert = this.#db.prepare(` INSERT INTO file_cache (path, size, mtime_ns, hash) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET size = excluded.size, mtime_ns = excluded.mtime_ns, hash = excluded.hash`); - this.transaction(() => { + this.#transaction(() => { for (let [path, stat] of entries) { upsert.run(path, stat.size, stat.mtimeNs, stat.hash); } @@ -113,10 +113,10 @@ export class Store { } pruneFileCache(live: Set): void { - let paths = this.db.prepare('SELECT path FROM file_cache').all() as + let paths = this.#db.prepare('SELECT path FROM file_cache').all() as unknown as {path: string}[]; - let del = this.db.prepare('DELETE FROM file_cache WHERE path = ?'); - this.transaction(() => { + let del = this.#db.prepare('DELETE FROM file_cache WHERE path = ?'); + this.#transaction(() => { for (let {path} of paths) { if (!live.has(path)) { del.run(path); @@ -126,13 +126,13 @@ export class Store { } lookupRule(key: string): StoredOutput[] | null { - return this.transaction(() => { - let rule = this.db.prepare('SELECT id FROM rules WHERE key = ?').get(key) as + return this.#transaction(() => { + let rule = this.#db.prepare('SELECT id FROM rules WHERE key = ?').get(key) as {id: bigint} | undefined; if (rule === undefined) { return null; } - let rows = this.db.prepare( + let rows = this.#db.prepare( 'SELECT digest, ext, size FROM rule_outputs WHERE rule_id = ? ORDER BY ord') .all(rule.id) as unknown as StoredOutput[]; // node:sqlite rows have a null prototype; return plain objects. @@ -143,14 +143,14 @@ export class Store { // One transaction per completed rule: an interrupted build only ever // contains fully-recorded rules. recordRule(key: string, cmds: string[], outputs: StoredOutput[]): void { - let upsert = this.db.prepare(` + let upsert = this.#db.prepare(` INSERT INTO rules (key, cmds) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET cmds = excluded.cmds RETURNING id`); - let delOutputs = this.db.prepare('DELETE FROM rule_outputs WHERE rule_id = ?'); - let insOutput = this.db.prepare( + let delOutputs = this.#db.prepare('DELETE FROM rule_outputs WHERE rule_id = ?'); + let insOutput = this.#db.prepare( 'INSERT INTO rule_outputs (rule_id, ord, digest, ext, size) VALUES (?, ?, ?, ?, ?)'); - this.transaction(() => { + this.#transaction(() => { let {id} = upsert.get(key, cmds.join('\n')) as {id: bigint}; delOutputs.run(id); outputs.forEach((o, i) => insOutput.run(id, i, o.digest, o.ext, o.size)); @@ -159,11 +159,11 @@ export class Store { // GC: drop every rule whose key is not live. Returns the removal count. deleteKeysNotIn(live: Set): number { - let keys = this.db.prepare('SELECT id, key FROM rules').all() as + let keys = this.#db.prepare('SELECT id, key FROM rules').all() as unknown as {id: bigint, key: string}[]; - let del = this.db.prepare('DELETE FROM rules WHERE id = ?'); + let del = this.#db.prepare('DELETE FROM rules WHERE id = ?'); let removed = 0; - this.transaction(() => { + this.#transaction(() => { for (let {id, key} of keys) { if (!live.has(key)) { del.run(id); @@ -176,14 +176,14 @@ export class Store { // Every CAS object referenced by some rule, as "." basenames. liveObjects(): Set { - let rows = this.db.prepare('SELECT digest, ext FROM rule_outputs').all() as + let rows = this.#db.prepare('SELECT digest, ext FROM rule_outputs').all() as unknown as {digest: string, ext: string}[]; return new Set(rows.map(r => `${r.digest}.${r.ext}`)); } close(): void { - this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); - this.db.close(); + this.#db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); + this.#db.close(); } } diff --git a/tools/build/test/executor.test.ts b/tools/build/test/executor.test.ts index 3a569d5d..56b577c1 100644 --- a/tools/build/test/executor.test.ts +++ b/tools/build/test/executor.test.ts @@ -13,13 +13,13 @@ import {Store} from '../store.ts'; beforeEach(resetDecls); -interface Env { - root: string; - dbPath: string; - casDir: string; - tmpDir: string; - execLog: string; -} +type Env = { + root: string, + dbPath: string, + casDir: string, + tmpDir: string, + execLog: string, +}; function setup(): Env { let root = fs.mkdtempSync(pathlib.join(os.tmpdir(), 'executor-test-')); diff --git a/tools/deploy/api.ts b/tools/deploy/api.ts index 081c86b5..a8057b5c 100644 --- a/tools/deploy/api.ts +++ b/tools/deploy/api.ts @@ -11,24 +11,24 @@ import * as pathlib from './path.ts'; // A source-tree file from ctx.list(); shaped like a deploy Path plus the // repo-relative path. -export interface SrcFile extends pathlib.Path { - path: string; -} +export type SrcFile = pathlib.Path & { + path: string, +}; export type CopySource = Artifact | SrcFile | string; // string = repo-relative path // The finish API: nice naming over built artifacts and raw sources. Ops are // queued in call order, which is the tar entry order. -export interface DeployCtx { - copy(src: CopySource, dst: string): void; - write(dst: string, data: string): void; - read(src: CopySource): string; - list(dir: string): SrcFile[]; +export type DeployCtx = { + copy(src: CopySource, dst: string): void, + write(dst: string, data: string): void, + read(src: CopySource): string, + list(dir: string): SrcFile[], // 8-char base32 content stamp. One source: the digest of its bytes, // byte-compatible with artifact hashes. Several: a digest of the sorted // per-source digests (order-insensitive). - hash(...srcs: CopySource[]): string; -} + hash(...srcs: CopySource[]): string, +}; export type DeployFn = (ctx: DeployCtx) => void | Promise; diff --git a/tools/deploy/config.ts b/tools/deploy/config.ts index 78388e74..4b76fda5 100644 --- a/tools/deploy/config.ts +++ b/tools/deploy/config.ts @@ -6,18 +6,18 @@ import JSON5 from 'json5'; import {BuildError} from '../build/errors.ts'; -export interface DeployEntry { - subset: string[]; - cmd: string; +export type DeployEntry = { + subset: string[], + cmd: string, // dir entries get their subset materialized into a temp directory whose // path replaces %d in cmd; tar entries get the subset tarred on stdin. - dir?: boolean; -} + dir?: boolean, +}; -export interface DeployTarget { - buildFile: string; - deploy: DeployEntry[]; -} +export type DeployTarget = { + buildFile: string, + deploy: DeployEntry[], +}; export type DeployConfig = Map; diff --git a/tools/deploy/index.ts b/tools/deploy/index.ts index d07416d0..26e6dcaf 100644 --- a/tools/deploy/index.ts +++ b/tools/deploy/index.ts @@ -27,19 +27,19 @@ let LOCK_PATH = '.build/lock.sqlite'; let CAS_DIR = '.build/cas'; let TMP_DIR = '.build/tmp'; -interface CommonOpts { - jobs: string; - dryRun?: boolean; - failFast?: boolean; - config: string; - verbose?: boolean; -} +type CommonOpts = { + jobs: string, + dryRun?: boolean, + failFast?: boolean, + config: string, + verbose?: boolean, +}; -interface VerbOpts extends CommonOpts { - output?: string; - link?: boolean; - tar?: boolean; -} +type VerbOpts = CommonOpts & { + output?: string, + link?: boolean, + tar?: boolean, +}; let USAGE = `usage: node tools/deploy/index.ts [options] diff --git a/tools/deploy/queue.ts b/tools/deploy/queue.ts index 483cb80e..62c475cd 100644 --- a/tools/deploy/queue.ts +++ b/tools/deploy/queue.ts @@ -28,17 +28,17 @@ type DebugEntry = { export type LogEntry = OpEntry | DebugEntry; export class ActionQueue { - private seen: Map; + #seen: Map; // Have an accessor for this in the future? idk public log: LogEntry[]; public valid: boolean; - private debugBuffer: unknown[]; + #debugBuffer: unknown[]; constructor() { - this.seen = new Map; + this.#seen = new Map; this.log = []; this.valid = true; - this.debugBuffer = []; + this.#debugBuffer = []; } throw(obj: unknown) { @@ -47,31 +47,31 @@ export class ActionQueue { } debug(obj: unknown) { - this.debugBuffer.push(obj); + this.#debugBuffer.push(obj); } gdebug(obj: unknown, stray: boolean) { this.log.push({type: 'Debug', obj, stray}); } - private pushOp(op: Op, dst: string) { + #pushOp(op: Op, dst: string) { dst = nodePath.normalize(dst); let entry: OpEntry = { type: 'Op', op, dst, valid: 'Success', - debugObjs: this.debugBuffer + debugObjs: this.#debugBuffer }; this.log.push(entry); - this.debugBuffer = []; + this.#debugBuffer = []; if (nodePath.isAbsolute(dst)) { this.valid = false; entry.valid = 'Absolute'; } else { - let lastEntry = this.seen.get(dst); + let lastEntry = this.#seen.get(dst); if (lastEntry === undefined) { - this.seen.set(dst, entry); + this.#seen.set(dst, entry); } else { this.valid = false; entry.valid = 'Multiple'; @@ -83,18 +83,18 @@ export class ActionQueue { } copy(src: string, dst: string) { - this.pushOp({type: 'Copy', src}, dst); + this.#pushOp({type: 'Copy', src}, dst); } write(data: string, dst: string) { - this.pushOp({type: 'Write', data}, dst); + this.#pushOp({type: 'Write', data}, dst); } skip() { - for (let obj of this.debugBuffer) { + for (let obj of this.#debugBuffer) { this.gdebug(obj, true); } - this.debugBuffer = []; + this.#debugBuffer = []; } print(level: 'errors' | 'all') {