@enspirit/emb
Version:
A replacement for our Makefile-for-monorepos
130 lines (129 loc) • 4.72 kB
JavaScript
import { CommandExecError } from '../../../index.js';
import { spawn } from 'node:child_process';
import { join } from 'node:path';
import { PassThrough } from 'node:stream';
import * as z from 'zod';
import { decodeBuildkitStatusResponse } from '../../index.js';
import { AbstractOperation } from '../../../operations/index.js';
/**
* https://docs.docker.com/reference/api/engine/version/v1.37/#tag/Image/operation/ImageBuild
*/
export const BuildImageOperationInputSchema = z.object({
//
context: z.string().describe('Path to the build context'),
dockerfile: z
.string()
.optional()
.default('Dockerfile')
.describe('Path within the build context to the Dockerfile.'),
src: z.array(z.string()),
tag: z.z
.string()
.optional()
.describe('latest')
.describe('A name and optional tag to apply to the image in the name:tag'),
//
buildArgs: z
.record(z.string(), z.string())
.optional()
.describe('Map of string pairs for build-time variables'),
labels: z
.record(z.string(), z.string())
.optional()
.describe('Arbitrary key/value labels to set on the image, as a JSON map of string pairs.'),
target: z.string().optional().describe('Target build stage'),
});
export class BuildImageOperation extends AbstractOperation {
out;
constructor(out) {
super(BuildImageOperationInputSchema);
this.out = out;
}
async _run(input) {
return this._buildWithDockerCLI(input);
}
async _buildWithDockerCLI(input) {
const args = [
'build',
input.context,
'-f',
join(input.context, input.dockerfile || 'Dockerfile'),
];
if (input.tag) {
args.push('--tag', input.tag);
}
if (input.target) {
args.push('--target', input.target);
}
Object.entries(input.buildArgs || []).forEach(([key, value]) => {
args.push('--build-arg', `${key.trim()}=${value.trim()}`);
});
Object.entries(input.labels || {}).forEach(([key, value]) => {
args.push('--label', `${key.trim()}=${value.trim()}`);
});
const logFile = await this.context.monorepo.store.createWriteStream(`logs/docker/build/${input.tag}.log`);
const tee = new PassThrough();
tee.pipe(logFile);
if (this.out) {
tee.pipe(this.out);
}
tee.write('Building image with opts: ' + JSON.stringify(args));
const child = await spawn('docker', args);
child.stderr.pipe(tee);
child.stdout.pipe(tee);
return new Promise((resolve, reject) => {
child.on('close', () => {
resolve();
});
child.on('exit', (code, signal) => {
if (code !== 0) {
reject(new CommandExecError('Docker build failed', code || -1, signal));
}
});
child.on('error', (err) => {
reject(err);
});
});
}
/**
* Experimental with dockerode and the docker API directly
*/
async _buildWithDockerode(input) {
const logFile = await this.context.monorepo.store.createWriteStream(`logs/docker/build/${input.tag}.log`);
const stream = await this.context.docker.buildImage({
context: input.context,
src: [...input.src],
}, {
buildargs: input.buildArgs,
dockerfile: input.dockerfile,
labels: input.labels,
t: input.tag,
target: input.target,
version: '2',
});
return new Promise((resolve, reject) => {
this.context.docker.modem.followProgress(stream, (err, _traces) => {
return err ? reject(err) : resolve();
}, async (trace) => {
if (trace.error) {
logFile.write(trace.error + '\n');
this.out?.write(trace.error + '\n');
reject(trace.error);
}
else {
try {
const { vertexes } = await decodeBuildkitStatusResponse(trace.aux);
vertexes.forEach((v) => {
// logStream.write(JSON.stringify(v) + '\n');
logFile.write(v.name + '\n');
this.out?.write(v.name + '\n');
});
}
catch (error) {
console.error(error);
}
}
});
});
}
}