mirror of
https://github.com/smogon/sprites.git
synced 2026-10-03 06:39:02 -05:00
A served name carries a content hash, so a reader that composes a fixed path can't find one. The way out is to publish the set twice, once under the stamped names and once under the bare ones, and a link is the one thing the queue couldn't say: Copy dereferences and Write has bytes. Add a Symlink op beside them. What ctx.symlink takes is not a link target but the destination of the thing being named, since a published tree is unpacked somewhere else and its halves can land apart; the path between the two is computed here and is what gets written. It takes the same duplicate-dst and absolute-dst checks the other ops do, plus one of its own -- resolved lexically against the directory it sits in, a target has to stay inside the tree, since the receiving side extracts as root and a link that walks out of it is the same write to anywhere that an absolute member name is. run() makes a real symlink in every mode, clearing the name first so a rerun over an existing tree behaves the way copy and write already do, and pack() emits a symlink member with no body, which is what a stream reader on the other end sees. refactor digests bytes, and a link has none; what it publishes is the name it points at, so that is what it records and what a retarget shows up as. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
|
|
import * as crypto from 'node:crypto';
|
|
import * as fs from 'node:fs/promises';
|
|
import * as nodePath from 'node:path';
|
|
|
|
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 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 type DeployCtx = {
|
|
// copy and write queue ops without touching the disk, so they are sync;
|
|
// read, list, and hash do file I/O.
|
|
copy(src: CopySource, dst: string): void,
|
|
write(dst: string, data: string): void,
|
|
// A second name for something else this tree publishes, carried as a link
|
|
// rather than a second copy. `names` is that thing's destination, not a
|
|
// link target: what gets written is the path from `dst` to it, since the
|
|
// two ends can be moved apart by whoever unpacks the tree.
|
|
symlink(dst: string, names: string): void,
|
|
read(src: CopySource): Promise<string>,
|
|
list(dir: string): Promise<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[]): Promise<string>,
|
|
};
|
|
|
|
export type DeployFn = (ctx: DeployCtx) => void | Promise<void>;
|
|
|
|
// Like rule(): a buildFile registers free-floating deploy blocks next to the
|
|
// rules they ship. They run in registration order after the build, sharing
|
|
// one ctx (and so one output tree) per buildFile.
|
|
let deploys: DeployFn[] = [];
|
|
|
|
export function deploy(fn: DeployFn): void {
|
|
deploys.push(fn);
|
|
}
|
|
|
|
export function getDeploys(): readonly DeployFn[] {
|
|
return deploys;
|
|
}
|
|
|
|
export function resetDeploys(): void {
|
|
deploys.length = 0;
|
|
}
|
|
|
|
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.
|
|
let srcPath = (src: CopySource): string => {
|
|
if (src instanceof Artifact) {
|
|
return casPath(casDir, src.hash, src.ext);
|
|
}
|
|
return typeof src === 'string' ? src : src.path;
|
|
};
|
|
let digestOf = async (src: CopySource): Promise<Buffer> => {
|
|
if (src instanceof Artifact) {
|
|
return Buffer.from(src.hash, 'hex');
|
|
}
|
|
return crypto.createHash('sha256').update(await fs.readFile(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);
|
|
},
|
|
symlink(dst: string, names: string): void {
|
|
queue.symlink(nodePath.relative(nodePath.dirname(nodePath.normalize(dst)), names), dst);
|
|
},
|
|
async read(src: CopySource): Promise<string> {
|
|
return await fs.readFile(srcPath(src), 'utf8');
|
|
},
|
|
async list(dir: string): Promise<SrcFile[]> {
|
|
let result = [];
|
|
// Files only, no dotfiles: the same filtering the build-side
|
|
// glob applies to rule inputs.
|
|
let ents = await fs.readdir(dir, {withFileTypes: true});
|
|
for (let ent of ents.sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
if (ent.name.startsWith('.') || (!ent.isFile() && !ent.isSymbolicLink())) {
|
|
continue;
|
|
}
|
|
let p = pathlib.path(ent.name, {dir});
|
|
result.push({...p, path: pathlib.format(p)});
|
|
}
|
|
return result;
|
|
},
|
|
async hash(...srcs: CopySource[]): Promise<string> {
|
|
let [only] = srcs;
|
|
if (only !== undefined && srcs.length === 1) {
|
|
return shortHash(await digestOf(only));
|
|
}
|
|
let digests = (await Promise.all(srcs.map(digestOf))).sort(Buffer.compare);
|
|
let h = crypto.createHash('sha256');
|
|
for (let d of digests) {
|
|
h.update(d);
|
|
}
|
|
return shortHash(h.digest());
|
|
},
|
|
};
|
|
}
|