import 'zx/globals'; import { SSH } from './ssh.js' type DrvPath = string type OutPath = string type NixPath = DrvPath | OutPath type ShownDerivation = { args: Array, builder: string, env: Record, inputDrvs: Record, inputSrcs: Array, outputs: Record, system: "x86-64_linux" }; type ShowDerivationOutput = Record type OutputSpec = { out: R } & Record type BuildOutput = { drvPath: DrvPath, outputs: OutputSpec, startTime?: number, stopTime?: number } export class Expression { expr: string constructor(expr: string) { this.expr = expr } async derive(): Promise { const {drvPath} = await nix.derive(this.expr) const drv = await nix.showDerivation(drvPath) return new Derivation(drvPath, drv[drvPath]) } async build(): Promise> { const outputs = await nix.build(this.expr) const drvMetaJson = await nix.showDerivation(outputs.drvPath) const [drvPath, drvMeta] = Object.entries(drvMetaJson)[0] const drv = new Derivation(drvPath, drvMeta) const ret: Record = {} for (const [k, v] of Object.entries(outputs.outputs)) { ret[k] = new BuiltOutput(v, drv) } return Object.assign(ret, { out: ret.out }) } } export class Derivation { path: string //outputs?: Array meta: ShownDerivation constructor(path: string, meta: ShownDerivation) { this.path = path this.meta = meta } async build(): Promise<{out: BuiltOutput} & Array> { const outputs: BuildOutput = await nix.build(this.path) const ret = Object.values(outputs.outputs).map(x => new BuiltOutput(x, this)) return Object.assign(ret, { out: new BuiltOutput(this.meta.outputs.out.path, this) }) } } export class BuiltOutput { path: string drv: Derivation constructor(path: string, drv: Derivation) { this.path = path this.drv = drv } async copy(target: SSH): Promise { return nix.copy(this.path, target) } } function dedupe(fn: (xs: A[], ...rest: Rest) => Promise): { (x: A, ...rest: Rest): Promise; (x: A[], ...rest: Rest): Promise; } { type QueueEntry = { x: A, resolve: (res: B) => void, reject: (err: any) => void } const queues = new Map() function inner(x: A, ...rest: Rest): Promise function inner(x: A[], ...rest: Rest): Promise function inner(x: A | A[], ...rest: Rest) { // todo: also dedupe array results if (Array.isArray(x)) return fn(x, ...rest) // map queue by rest const stringified = JSON.stringify(rest) const had = queues.has(stringified) const queue = queues.get(stringified) || [] const ret = new Promise((resolve, reject) => { queue.push({x, resolve, reject}) }) if (!had) { queues.set(stringified, queue) setImmediate(() => { queues.delete(stringified) fn(queue.map(({x}) => x), ...rest) .then(results => { for (const [i, {resolve}] of queue.entries()) resolve(results[i]) }) .catch(e => { for (const {reject} of queue) reject(e) }) }) } return ret } return inner } //function nixBuild(attr: string[]): Promise //function nixBuild(attr: string): Promise async function nixBuild(attr: string[]): Promise { const tmp = (await $`mktemp -d`).stdout.trim() process.on('exit', () => fs.removeSync(tmp)) const ret = JSON.parse((await $`nom build --json ${attr} -o ${tmp}/result`).stdout) if (Array.isArray(attr)) return ret return ret[0] } namespace nix { export const build = dedupe(nixBuild) export async function showDerivation(path: NixPath): Promise { return JSON.parse((await $`nix show-derivation ${path}`.quiet()).stdout) } export const derive = dedupe(async (attr: string[]): Promise => { return JSON.parse((await $`nom build --json --dry-run ${attr}`).stdout) }) export const copy = dedupe(async (attrs: string[], target: SSH): Promise => { process.env.NIX_SSHOPTS = "-o compression=no"; await $`nix copy ${attrs} -s --to ssh://${target.host}` return Array(attrs.length) }) }