Files
sprites/tools/deploy/api.ts
Christopher Monsanto bc39c94851 Fix review findings in the content-addressed core
- casInsert verifies an existing object's bytes before trusting it, fsyncs
  before the rename, and the store records object sizes so a crash-truncated
  object reads as dirty instead of clean forever.
- The driver hashes sources over the producer closure, so partial builds
  (inspect) that pull in producers outside the demanded set work.
- Unknown executor errors are reported and abort the build instead of
  vanishing into allSettled.
- %b/%B expand to nominal names at declaration (never CAS basenames), which
  also puts name-dependence in the identity key; cmds are key-joined with
  NUL so a multi-line command cannot collide with split commands. This
  churns every rule key once.
- Rules must declare at least one output; output names reject shell-hostile
  characters.
- run --link copies read-only (CAS) sources instead of hardlinking 0444
  modes into deploy trees; ctx.list filters dotfiles and directories like
  the build-side glob; inspect resolves targets against the invoking cwd.
- The registry dedupes identical declarations (same cmds/inputs/deps/
  outputs/display), returning the existing artifacts, so shared rule sets
  are plain functions and memo() is gone; ps.deploy.ts no longer declares
  the gen5 gif set it never ships.
- rule() returns a tuple typed by its literal output list (single string
  output returns the Artifact directly), so destructuring needs no
  non-null assertions.
- Manifest is a class carrying the deploy ctx: set() rejects duplicate
  keys, write(dst) emits the sorted JSON, and spritecopy/itemspritecopy/
  stampcopy take just the manifest. The unstamped smogon sets (xyitems,
  fbsprites, twittersprites) are deprecated and commented out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 01:13:12 -04:00

98 lines
3.4 KiB
TypeScript

import crypto from 'crypto';
import fs from 'fs';
import b32encode from 'base32-encode';
import {Artifact} from '../build/artifact.ts';
import {casPath} from '../build/cas.ts';
import {type ActionQueue} from './queue.ts';
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 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 (the tar entry order), so __key-style entries must be
// written first.
export interface 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;
}
export interface DeploySpec {
finish : (ctx : DeployCtx) => void | Promise<void>;
}
// Identity helper: a deploy module declares its rules at top level and
// `export default defineDeploy({finish})`.
export function defineDeploy(spec : DeploySpec) : DeploySpec {
return spec;
}
function shortHash(digest : Buffer) : string {
return b32encode(digest, 'RFC4648').slice(0, 8);
}
export function makeCtx(casDir : string, queue : ActionQueue) : DeployCtx {
// Every path here is repo-root-relative; the CLI chdirs to the root.
const srcPath = (src : CopySource) : string => {
if (src instanceof Artifact) {
return casPath(casDir, src.hash, src.ext);
}
return typeof src === 'string' ? src : src.path;
};
const digestOf = (src : CopySource) : Buffer => {
if (src instanceof Artifact) {
return Buffer.from(src.hash, 'hex');
}
return crypto.createHash('sha256').update(fs.readFileSync(srcPath(src))).digest();
};
return {
copy(src : CopySource, dst : string) : void {
queue.copy(srcPath(src), dst);
},
write(dst : string, data : string) : void {
queue.write(data, dst);
},
read(src : CopySource) : string {
return fs.readFileSync(srcPath(src), 'utf8');
},
list(dir : string) : SrcFile[] {
const result = [];
// Files only, no dotfiles: the same filtering the build-side
// glob applies to rule inputs.
for (const ent of fs.readdirSync(dir, {withFileTypes: true}).sort((a, b) => a.name < b.name ? -1 : 1)) {
if (ent.name.startsWith('.') || (!ent.isFile() && !ent.isSymbolicLink())) {
continue;
}
const p = pathlib.path(ent.name, {dir});
result.push({...p, path: pathlib.format(p)});
}
return result;
},
hash(...srcs : CopySource[]) : string {
if (srcs.length === 1) {
return shortHash(digestOf(srcs[0]!));
}
const digests = srcs.map(digestOf).sort(Buffer.compare);
const h = crypto.createHash('sha256');
for (const d of digests) {
h.update(d);
}
return shortHash(h.digest());
},
};
}