Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfc7798d49 | |||
| 37dfde005e |
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-06 - 1.11.0 - feat(docker)
|
||||||
|
start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network
|
||||||
|
|
||||||
|
- Introduce a temporary local registry (localhost:5234) with start/stop helpers and push support to expose local images for buildx
|
||||||
|
- Add Dockerfile.needsLocalRegistry to decide when a local registry is required (local base dependencies + multi-platform or platform option)
|
||||||
|
- Push built images to the local registry and set localRegistryTag on Dockerfile instances for BuildKit build-context usage
|
||||||
|
- Tag built images in the host daemon for dependent Dockerfiles to resolve local FROM references
|
||||||
|
- Integrate registry lifecycle into Dockerfile.buildDockerfiles and TsDockerManager build flows (start before builds, stop after)
|
||||||
|
- Ensure buildx builder is created with --driver-opt network=host and recreate existing builder if it lacks host network to allow registry access from build containers
|
||||||
|
|
||||||
## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
|
## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
|
||||||
support using a local base image as a build context in buildx commands
|
support using a local base image as a build context in buildx commands
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsdocker",
|
"name": "@git.zone/tsdocker",
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "develop npm modules cross platform with docker",
|
"description": "develop npm modules cross platform with docker",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsdocker',
|
name: '@git.zone/tsdocker',
|
||||||
version: '1.10.0',
|
version: '1.11.0',
|
||||||
description: 'develop npm modules cross platform with docker'
|
description: 'develop npm modules cross platform with docker'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const smartshellInstance = new plugins.smartshell.Smartshell({
|
|||||||
executor: 'bash',
|
executor: 'bash',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const LOCAL_REGISTRY_PORT = 5234;
|
||||||
|
const LOCAL_REGISTRY_HOST = `localhost:${LOCAL_REGISTRY_PORT}`;
|
||||||
|
const LOCAL_REGISTRY_CONTAINER = 'tsdocker-local-registry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Dockerfile represents a Dockerfile on disk
|
* Class Dockerfile represents a Dockerfile on disk
|
||||||
*/
|
*/
|
||||||
@@ -135,6 +139,53 @@ export class Dockerfile {
|
|||||||
return sortedDockerfileArray;
|
return sortedDockerfileArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Determines if a local registry is needed for buildx dependency resolution. */
|
||||||
|
public static needsLocalRegistry(
|
||||||
|
dockerfiles: Dockerfile[],
|
||||||
|
options?: { platform?: string },
|
||||||
|
): boolean {
|
||||||
|
const hasLocalDeps = dockerfiles.some(df => df.localBaseImageDependent);
|
||||||
|
if (!hasLocalDeps) return false;
|
||||||
|
const config = dockerfiles[0]?.managerRef?.config;
|
||||||
|
return !!options?.platform || !!(config?.platforms && config.platforms.length > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts a temporary registry:2 container on port 5234. */
|
||||||
|
public static async startLocalRegistry(): Promise<void> {
|
||||||
|
await smartshellInstance.execSilent(
|
||||||
|
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
|
||||||
|
);
|
||||||
|
const result = await smartshellInstance.execSilent(
|
||||||
|
`docker run -d --name ${LOCAL_REGISTRY_CONTAINER} -p ${LOCAL_REGISTRY_PORT}:5000 registry:2`
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to start local registry: ${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
// registry:2 starts near-instantly; brief wait for readiness
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops and removes the temporary local registry container. */
|
||||||
|
public static async stopLocalRegistry(): Promise<void> {
|
||||||
|
await smartshellInstance.execSilent(
|
||||||
|
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
|
||||||
|
);
|
||||||
|
logger.log('info', 'Stopped local registry');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pushes a built image to the local registry for buildx consumption. */
|
||||||
|
public static async pushToLocalRegistry(dockerfile: Dockerfile): Promise<void> {
|
||||||
|
const registryTag = `${LOCAL_REGISTRY_HOST}/${dockerfile.buildTag}`;
|
||||||
|
await smartshellInstance.execSilent(`docker tag ${dockerfile.buildTag} ${registryTag}`);
|
||||||
|
const result = await smartshellInstance.execSilent(`docker push ${registryTag}`);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to push to local registry: ${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
dockerfile.localRegistryTag = registryTag;
|
||||||
|
logger.log('info', `Pushed ${dockerfile.buildTag} to local registry as ${registryTag}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the corresponding real docker image for each Dockerfile class instance
|
* Builds the corresponding real docker image for each Dockerfile class instance
|
||||||
*/
|
*/
|
||||||
@@ -144,7 +195,13 @@ export class Dockerfile {
|
|||||||
): Promise<Dockerfile[]> {
|
): Promise<Dockerfile[]> {
|
||||||
const total = sortedArrayArg.length;
|
const total = sortedArrayArg.length;
|
||||||
const overallStart = Date.now();
|
const overallStart = Date.now();
|
||||||
|
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
|
||||||
|
|
||||||
|
if (useRegistry) {
|
||||||
|
await Dockerfile.startLocalRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const dockerfileArg = sortedArrayArg[i];
|
const dockerfileArg = sortedArrayArg[i];
|
||||||
const progress = `(${i + 1}/${total})`;
|
const progress = `(${i + 1}/${total})`;
|
||||||
@@ -153,8 +210,7 @@ export class Dockerfile {
|
|||||||
const elapsed = await dockerfileArg.build(options);
|
const elapsed = await dockerfileArg.build(options);
|
||||||
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
||||||
|
|
||||||
// Tag the built image with the full base image references used by dependent Dockerfiles,
|
// Tag in host daemon for standard docker build compatibility
|
||||||
// so their FROM lines resolve to the locally-built image instead of pulling from a registry.
|
|
||||||
const dependentBaseImages = new Set<string>();
|
const dependentBaseImages = new Set<string>();
|
||||||
for (const other of sortedArrayArg) {
|
for (const other of sortedArrayArg) {
|
||||||
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
||||||
@@ -165,6 +221,16 @@ export class Dockerfile {
|
|||||||
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
||||||
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push to local registry for buildx dependency resolution
|
||||||
|
if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
|
||||||
|
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (useRegistry) {
|
||||||
|
await Dockerfile.stopLocalRegistry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
||||||
@@ -366,6 +432,7 @@ export class Dockerfile {
|
|||||||
public baseImage: string;
|
public baseImage: string;
|
||||||
public localBaseImageDependent: boolean;
|
public localBaseImageDependent: boolean;
|
||||||
public localBaseDockerfile!: Dockerfile;
|
public localBaseDockerfile!: Dockerfile;
|
||||||
|
public localRegistryTag?: string;
|
||||||
|
|
||||||
constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
|
constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
|
||||||
this.managerRef = managerRefArg;
|
this.managerRef = managerRefArg;
|
||||||
@@ -412,9 +479,12 @@ export class Dockerfile {
|
|||||||
let buildContextFlag = '';
|
let buildContextFlag = '';
|
||||||
if (this.localBaseImageDependent && this.localBaseDockerfile) {
|
if (this.localBaseImageDependent && this.localBaseDockerfile) {
|
||||||
const fromImage = this.baseImage;
|
const fromImage = this.baseImage;
|
||||||
const localTag = this.localBaseDockerfile.buildTag;
|
if (this.localBaseDockerfile.localRegistryTag) {
|
||||||
buildContextFlag = ` --build-context "${fromImage}=docker-image://${localTag}"`;
|
// BuildKit pulls from the local registry (reachable via host network)
|
||||||
logger.log('info', `Using local build context: ${fromImage} -> docker-image://${localTag}`);
|
const registryTag = this.localBaseDockerfile.localRegistryTag;
|
||||||
|
buildContextFlag = ` --build-context "${fromImage}=docker-image://${registryTag}"`;
|
||||||
|
logger.log('info', `Using local registry build context: ${fromImage} -> docker-image://${registryTag}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let buildCommand: string;
|
let buildCommand: string;
|
||||||
|
|||||||
@@ -148,17 +148,21 @@ export class TsDockerManager {
|
|||||||
|
|
||||||
const total = toBuild.length;
|
const total = toBuild.length;
|
||||||
const overallStart = Date.now();
|
const overallStart = Date.now();
|
||||||
|
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
|
||||||
|
|
||||||
|
if (useRegistry) {
|
||||||
|
await Dockerfile.startLocalRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const dockerfileArg = toBuild[i];
|
const dockerfileArg = toBuild[i];
|
||||||
const progress = `(${i + 1}/${total})`;
|
const progress = `(${i + 1}/${total})`;
|
||||||
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
|
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
|
||||||
|
|
||||||
if (skip) {
|
if (skip) {
|
||||||
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
|
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
|
||||||
continue;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// Cache miss — build this Dockerfile
|
|
||||||
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
|
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
|
||||||
const elapsed = await dockerfileArg.build({
|
const elapsed = await dockerfileArg.build({
|
||||||
platform: options?.platform,
|
platform: options?.platform,
|
||||||
@@ -167,15 +171,11 @@ export class TsDockerManager {
|
|||||||
verbose: options?.verbose,
|
verbose: options?.verbose,
|
||||||
});
|
});
|
||||||
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
||||||
|
|
||||||
const imageId = await dockerfileArg.getId();
|
const imageId = await dockerfileArg.getId();
|
||||||
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
|
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
// Tag for dependents IMMEDIATELY (not after all builds)
|
||||||
|
|
||||||
// Perform dependency tagging for all Dockerfiles (even cache hits, since tags may be stale)
|
|
||||||
for (const dockerfileArg of toBuild) {
|
|
||||||
const dependentBaseImages = new Set<string>();
|
const dependentBaseImages = new Set<string>();
|
||||||
for (const other of toBuild) {
|
for (const other of toBuild) {
|
||||||
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
||||||
@@ -186,8 +186,19 @@ export class TsDockerManager {
|
|||||||
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
||||||
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
|
||||||
|
if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
|
||||||
|
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (useRegistry) {
|
||||||
|
await Dockerfile.stopLocalRegistry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Total build time: ${formatDuration(Date.now() - overallStart)}`);
|
||||||
cache.save();
|
cache.save();
|
||||||
} else {
|
} else {
|
||||||
// === STANDARD MODE: build all via static helper ===
|
// === STANDARD MODE: build all via static helper ===
|
||||||
@@ -226,20 +237,27 @@ export class TsDockerManager {
|
|||||||
*/
|
*/
|
||||||
private async ensureBuildx(): Promise<void> {
|
private async ensureBuildx(): Promise<void> {
|
||||||
logger.log('info', 'Setting up Docker buildx for multi-platform builds...');
|
logger.log('info', 'Setting up Docker buildx for multi-platform builds...');
|
||||||
|
|
||||||
// Check if a buildx builder exists
|
|
||||||
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null');
|
const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null');
|
||||||
|
|
||||||
if (inspectResult.exitCode !== 0) {
|
if (inspectResult.exitCode !== 0) {
|
||||||
// Create a new buildx builder
|
logger.log('info', 'Creating new buildx builder with host network...');
|
||||||
logger.log('info', 'Creating new buildx builder...');
|
await smartshellInstance.exec(
|
||||||
await smartshellInstance.exec('docker buildx create --name tsdocker-builder --use');
|
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
|
||||||
|
);
|
||||||
|
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
||||||
|
} else {
|
||||||
|
const inspectOutput = inspectResult.stdout || '';
|
||||||
|
if (!inspectOutput.includes('network=host')) {
|
||||||
|
logger.log('info', 'Recreating buildx builder with host network (migration)...');
|
||||||
|
await smartshellInstance.exec('docker buildx rm tsdocker-builder 2>/dev/null');
|
||||||
|
await smartshellInstance.exec(
|
||||||
|
'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
|
||||||
|
);
|
||||||
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
||||||
} else {
|
} else {
|
||||||
// Use existing builder
|
|
||||||
await smartshellInstance.exec('docker buildx use tsdocker-builder');
|
await smartshellInstance.exec('docker buildx use tsdocker-builder');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
logger.log('ok', 'Docker buildx ready');
|
logger.log('ok', 'Docker buildx ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user