@git.zone/tsdocker 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dockercontext.d.ts +14 -0
- package/dist_ts/classes.dockercontext.js +59 -0
- package/dist_ts/classes.dockerfile.d.ts +15 -1
- package/dist_ts/classes.dockerfile.js +111 -21
- package/dist_ts/classes.tsdockermanager.d.ts +3 -1
- package/dist_ts/classes.tsdockermanager.js +117 -40
- package/dist_ts/interfaces/index.d.ts +9 -0
- package/dist_ts/tsdocker.cli.js +25 -7
- package/package.json +1 -1
- package/readme.hints.md +12 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dockercontext.ts +69 -0
- package/ts/classes.dockerfile.ts +123 -23
- package/ts/classes.tsdockermanager.ts +121 -39
- package/ts/interfaces/index.ts +10 -0
- package/ts/tsdocker.cli.ts +24 -6
package/ts/classes.dockerfile.ts
CHANGED
|
@@ -151,7 +151,7 @@ export class Dockerfile {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/** Starts a temporary registry:2 container on port 5234. */
|
|
154
|
-
public static async startLocalRegistry(): Promise<void> {
|
|
154
|
+
public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
|
|
155
155
|
await smartshellInstance.execSilent(
|
|
156
156
|
`docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
|
|
157
157
|
);
|
|
@@ -164,6 +164,9 @@ export class Dockerfile {
|
|
|
164
164
|
// registry:2 starts near-instantly; brief wait for readiness
|
|
165
165
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
166
166
|
logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`);
|
|
167
|
+
if (isRootless) {
|
|
168
|
+
logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`);
|
|
169
|
+
}
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
/** Stops and removes the temporary local registry container. */
|
|
@@ -186,45 +189,142 @@ export class Dockerfile {
|
|
|
186
189
|
logger.log('info', `Pushed ${dockerfile.buildTag} to local registry as ${registryTag}`);
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Groups topologically sorted Dockerfiles into dependency levels.
|
|
194
|
+
* Level 0 = no local dependencies; level N = depends on something in level N-1.
|
|
195
|
+
* Images within the same level are independent and can build in parallel.
|
|
196
|
+
*/
|
|
197
|
+
public static computeLevels(sortedDockerfiles: Dockerfile[]): Dockerfile[][] {
|
|
198
|
+
const levelMap = new Map<Dockerfile, number>();
|
|
199
|
+
for (const df of sortedDockerfiles) {
|
|
200
|
+
if (!df.localBaseImageDependent || !df.localBaseDockerfile) {
|
|
201
|
+
levelMap.set(df, 0);
|
|
202
|
+
} else {
|
|
203
|
+
const depLevel = levelMap.get(df.localBaseDockerfile) ?? 0;
|
|
204
|
+
levelMap.set(df, depLevel + 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const maxLevel = Math.max(...Array.from(levelMap.values()), 0);
|
|
208
|
+
const levels: Dockerfile[][] = [];
|
|
209
|
+
for (let l = 0; l <= maxLevel; l++) {
|
|
210
|
+
levels.push(sortedDockerfiles.filter(df => levelMap.get(df) === l));
|
|
211
|
+
}
|
|
212
|
+
return levels;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Runs async tasks with bounded concurrency (worker-pool pattern).
|
|
217
|
+
* Fast-fail: if any task throws, Promise.all rejects immediately.
|
|
218
|
+
*/
|
|
219
|
+
public static async runWithConcurrency<T>(
|
|
220
|
+
tasks: (() => Promise<T>)[],
|
|
221
|
+
concurrency: number,
|
|
222
|
+
): Promise<T[]> {
|
|
223
|
+
const results: T[] = new Array(tasks.length);
|
|
224
|
+
let nextIndex = 0;
|
|
225
|
+
async function worker(): Promise<void> {
|
|
226
|
+
while (true) {
|
|
227
|
+
const idx = nextIndex++;
|
|
228
|
+
if (idx >= tasks.length) break;
|
|
229
|
+
results[idx] = await tasks[idx]();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const workers = Array.from(
|
|
233
|
+
{ length: Math.min(concurrency, tasks.length) },
|
|
234
|
+
() => worker(),
|
|
235
|
+
);
|
|
236
|
+
await Promise.all(workers);
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
239
|
+
|
|
189
240
|
/**
|
|
190
241
|
* Builds the corresponding real docker image for each Dockerfile class instance
|
|
191
242
|
*/
|
|
192
243
|
public static async buildDockerfiles(
|
|
193
244
|
sortedArrayArg: Dockerfile[],
|
|
194
|
-
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean },
|
|
245
|
+
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number },
|
|
195
246
|
): Promise<Dockerfile[]> {
|
|
196
247
|
const total = sortedArrayArg.length;
|
|
197
248
|
const overallStart = Date.now();
|
|
198
249
|
const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
|
|
199
250
|
|
|
200
251
|
if (useRegistry) {
|
|
201
|
-
await Dockerfile.startLocalRegistry();
|
|
252
|
+
await Dockerfile.startLocalRegistry(options?.isRootless);
|
|
202
253
|
}
|
|
203
254
|
|
|
204
255
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const dependentBaseImages = new Set<string>();
|
|
215
|
-
for (const other of sortedArrayArg) {
|
|
216
|
-
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
|
217
|
-
dependentBaseImages.add(other.baseImage);
|
|
218
|
-
}
|
|
256
|
+
if (options?.parallel) {
|
|
257
|
+
// === PARALLEL MODE: build independent images concurrently within each level ===
|
|
258
|
+
const concurrency = options.parallelConcurrency ?? 4;
|
|
259
|
+
const levels = Dockerfile.computeLevels(sortedArrayArg);
|
|
260
|
+
|
|
261
|
+
logger.log('info', `Parallel build: ${levels.length} level(s), concurrency ${concurrency}`);
|
|
262
|
+
for (let l = 0; l < levels.length; l++) {
|
|
263
|
+
const level = levels[l];
|
|
264
|
+
logger.log('info', ` Level ${l} (${level.length}): ${level.map(df => df.cleanTag).join(', ')}`);
|
|
219
265
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
|
|
267
|
+
let built = 0;
|
|
268
|
+
for (let l = 0; l < levels.length; l++) {
|
|
269
|
+
const level = levels[l];
|
|
270
|
+
logger.log('info', `--- Level ${l}: building ${level.length} image(s) in parallel ---`);
|
|
271
|
+
|
|
272
|
+
const tasks = level.map((df) => {
|
|
273
|
+
const myIndex = ++built;
|
|
274
|
+
return async () => {
|
|
275
|
+
const progress = `(${myIndex}/${total})`;
|
|
276
|
+
logger.log('info', `${progress} Building ${df.cleanTag}...`);
|
|
277
|
+
const elapsed = await df.build(options);
|
|
278
|
+
logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
|
|
279
|
+
return df;
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await Dockerfile.runWithConcurrency(tasks, concurrency);
|
|
284
|
+
|
|
285
|
+
// After the entire level completes, tag + push for dependency resolution
|
|
286
|
+
for (const df of level) {
|
|
287
|
+
const dependentBaseImages = new Set<string>();
|
|
288
|
+
for (const other of sortedArrayArg) {
|
|
289
|
+
if (other.localBaseDockerfile === df && other.baseImage !== df.buildTag) {
|
|
290
|
+
dependentBaseImages.add(other.baseImage);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const fullTag of dependentBaseImages) {
|
|
294
|
+
logger.log('info', `Tagging ${df.buildTag} as ${fullTag} for local dependency resolution`);
|
|
295
|
+
await smartshellInstance.exec(`docker tag ${df.buildTag} ${fullTag}`);
|
|
296
|
+
}
|
|
297
|
+
if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === df)) {
|
|
298
|
+
await Dockerfile.pushToLocalRegistry(df);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
223
301
|
}
|
|
302
|
+
} else {
|
|
303
|
+
// === SEQUENTIAL MODE: build one at a time ===
|
|
304
|
+
for (let i = 0; i < total; i++) {
|
|
305
|
+
const dockerfileArg = sortedArrayArg[i];
|
|
306
|
+
const progress = `(${i + 1}/${total})`;
|
|
307
|
+
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
|
|
308
|
+
|
|
309
|
+
const elapsed = await dockerfileArg.build(options);
|
|
310
|
+
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
|
311
|
+
|
|
312
|
+
// Tag in host daemon for standard docker build compatibility
|
|
313
|
+
const dependentBaseImages = new Set<string>();
|
|
314
|
+
for (const other of sortedArrayArg) {
|
|
315
|
+
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
|
316
|
+
dependentBaseImages.add(other.baseImage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const fullTag of dependentBaseImages) {
|
|
320
|
+
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
|
321
|
+
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
|
322
|
+
}
|
|
224
323
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
324
|
+
// Push to local registry for buildx dependency resolution
|
|
325
|
+
if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
|
|
326
|
+
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
|
327
|
+
}
|
|
228
328
|
}
|
|
229
329
|
}
|
|
230
330
|
} finally {
|
|
@@ -5,6 +5,7 @@ import { Dockerfile } from './classes.dockerfile.js';
|
|
|
5
5
|
import { DockerRegistry } from './classes.dockerregistry.js';
|
|
6
6
|
import { RegistryStorage } from './classes.registrystorage.js';
|
|
7
7
|
import { TsDockerCache } from './classes.tsdockercache.js';
|
|
8
|
+
import { DockerContext } from './classes.dockercontext.js';
|
|
8
9
|
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
|
|
9
10
|
|
|
10
11
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
@@ -18,17 +19,27 @@ export class TsDockerManager {
|
|
|
18
19
|
public registryStorage: RegistryStorage;
|
|
19
20
|
public config: ITsDockerConfig;
|
|
20
21
|
public projectInfo: any;
|
|
22
|
+
public dockerContext: DockerContext;
|
|
21
23
|
private dockerfiles: Dockerfile[] = [];
|
|
22
24
|
|
|
23
25
|
constructor(config: ITsDockerConfig) {
|
|
24
26
|
this.config = config;
|
|
25
27
|
this.registryStorage = new RegistryStorage();
|
|
28
|
+
this.dockerContext = new DockerContext();
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Prepares the manager by loading project info and registries
|
|
30
33
|
*/
|
|
31
|
-
public async prepare(): Promise<void> {
|
|
34
|
+
public async prepare(contextArg?: string): Promise<void> {
|
|
35
|
+
// Detect Docker context
|
|
36
|
+
if (contextArg) {
|
|
37
|
+
this.dockerContext.setContext(contextArg);
|
|
38
|
+
}
|
|
39
|
+
await this.dockerContext.detect();
|
|
40
|
+
this.dockerContext.logContextInfo();
|
|
41
|
+
this.dockerContext.logRootlessWarnings();
|
|
42
|
+
|
|
32
43
|
// Load project info
|
|
33
44
|
try {
|
|
34
45
|
const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd);
|
|
@@ -156,6 +167,16 @@ export class TsDockerManager {
|
|
|
156
167
|
logger.log('info', 'Cache: disabled (--no-cache)');
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
if (options?.parallel) {
|
|
171
|
+
const concurrency = options.parallelConcurrency ?? 4;
|
|
172
|
+
const levels = Dockerfile.computeLevels(toBuild);
|
|
173
|
+
logger.log('info', `Parallel build: ${levels.length} level(s), concurrency ${concurrency}`);
|
|
174
|
+
for (let l = 0; l < levels.length; l++) {
|
|
175
|
+
const level = levels[l];
|
|
176
|
+
logger.log('info', ` Level ${l} (${level.length}): ${level.map(df => df.cleanTag).join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
|
|
160
181
|
|
|
161
182
|
if (options?.cached) {
|
|
@@ -169,45 +190,101 @@ export class TsDockerManager {
|
|
|
169
190
|
const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
|
|
170
191
|
|
|
171
192
|
if (useRegistry) {
|
|
172
|
-
await Dockerfile.startLocalRegistry();
|
|
193
|
+
await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
logger.log('info',
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
if (options?.parallel) {
|
|
198
|
+
// === PARALLEL CACHED MODE ===
|
|
199
|
+
const concurrency = options.parallelConcurrency ?? 4;
|
|
200
|
+
const levels = Dockerfile.computeLevels(toBuild);
|
|
201
|
+
|
|
202
|
+
let built = 0;
|
|
203
|
+
for (let l = 0; l < levels.length; l++) {
|
|
204
|
+
const level = levels[l];
|
|
205
|
+
logger.log('info', `--- Level ${l}: building ${level.length} image(s) in parallel ---`);
|
|
206
|
+
|
|
207
|
+
const tasks = level.map((df) => {
|
|
208
|
+
const myIndex = ++built;
|
|
209
|
+
return async () => {
|
|
210
|
+
const progress = `(${myIndex}/${total})`;
|
|
211
|
+
const skip = await cache.shouldSkipBuild(df.cleanTag, df.content);
|
|
212
|
+
|
|
213
|
+
if (skip) {
|
|
214
|
+
logger.log('ok', `${progress} Skipped ${df.cleanTag} (cached)`);
|
|
215
|
+
} else {
|
|
216
|
+
logger.log('info', `${progress} Building ${df.cleanTag}...`);
|
|
217
|
+
const elapsed = await df.build({
|
|
218
|
+
platform: options?.platform,
|
|
219
|
+
timeout: options?.timeout,
|
|
220
|
+
noCache: options?.noCache,
|
|
221
|
+
verbose: options?.verbose,
|
|
222
|
+
});
|
|
223
|
+
logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
|
|
224
|
+
const imageId = await df.getId();
|
|
225
|
+
cache.recordBuild(df.cleanTag, df.content, imageId, df.buildTag);
|
|
226
|
+
}
|
|
227
|
+
return df;
|
|
228
|
+
};
|
|
190
229
|
});
|
|
191
|
-
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
|
192
|
-
const imageId = await dockerfileArg.getId();
|
|
193
|
-
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
|
|
194
|
-
}
|
|
195
230
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
dependentBaseImages
|
|
231
|
+
await Dockerfile.runWithConcurrency(tasks, concurrency);
|
|
232
|
+
|
|
233
|
+
// After the entire level completes, tag + push for dependency resolution
|
|
234
|
+
for (const df of level) {
|
|
235
|
+
const dependentBaseImages = new Set<string>();
|
|
236
|
+
for (const other of toBuild) {
|
|
237
|
+
if (other.localBaseDockerfile === df && other.baseImage !== df.buildTag) {
|
|
238
|
+
dependentBaseImages.add(other.baseImage);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const fullTag of dependentBaseImages) {
|
|
242
|
+
logger.log('info', `Tagging ${df.buildTag} as ${fullTag} for local dependency resolution`);
|
|
243
|
+
await smartshellInstance.exec(`docker tag ${df.buildTag} ${fullTag}`);
|
|
244
|
+
}
|
|
245
|
+
if (useRegistry && toBuild.some(other => other.localBaseDockerfile === df)) {
|
|
246
|
+
await Dockerfile.pushToLocalRegistry(df);
|
|
247
|
+
}
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
} else {
|
|
251
|
+
// === SEQUENTIAL CACHED MODE ===
|
|
252
|
+
for (let i = 0; i < total; i++) {
|
|
253
|
+
const dockerfileArg = toBuild[i];
|
|
254
|
+
const progress = `(${i + 1}/${total})`;
|
|
255
|
+
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
|
|
256
|
+
|
|
257
|
+
if (skip) {
|
|
258
|
+
logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
|
|
259
|
+
} else {
|
|
260
|
+
logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
|
|
261
|
+
const elapsed = await dockerfileArg.build({
|
|
262
|
+
platform: options?.platform,
|
|
263
|
+
timeout: options?.timeout,
|
|
264
|
+
noCache: options?.noCache,
|
|
265
|
+
verbose: options?.verbose,
|
|
266
|
+
});
|
|
267
|
+
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
|
|
268
|
+
const imageId = await dockerfileArg.getId();
|
|
269
|
+
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Tag for dependents IMMEDIATELY (not after all builds)
|
|
273
|
+
const dependentBaseImages = new Set<string>();
|
|
274
|
+
for (const other of toBuild) {
|
|
275
|
+
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
|
276
|
+
dependentBaseImages.add(other.baseImage);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const fullTag of dependentBaseImages) {
|
|
280
|
+
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
|
281
|
+
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
|
282
|
+
}
|
|
207
283
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
284
|
+
// Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
|
|
285
|
+
if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
|
|
286
|
+
await Dockerfile.pushToLocalRegistry(dockerfileArg);
|
|
287
|
+
}
|
|
211
288
|
}
|
|
212
289
|
}
|
|
213
290
|
} finally {
|
|
@@ -225,6 +302,9 @@ export class TsDockerManager {
|
|
|
225
302
|
timeout: options?.timeout,
|
|
226
303
|
noCache: options?.noCache,
|
|
227
304
|
verbose: options?.verbose,
|
|
305
|
+
isRootless: this.dockerContext.contextInfo?.isRootless,
|
|
306
|
+
parallel: options?.parallel,
|
|
307
|
+
parallelConcurrency: options?.parallelConcurrency,
|
|
228
308
|
});
|
|
229
309
|
}
|
|
230
310
|
|
|
@@ -254,30 +334,32 @@ export class TsDockerManager {
|
|
|
254
334
|
* Ensures Docker buildx is set up for multi-architecture builds
|
|
255
335
|
*/
|
|
256
336
|
private async ensureBuildx(): Promise<void> {
|
|
337
|
+
const builderName = this.dockerContext.getBuilderName();
|
|
257
338
|
const platforms = this.config.platforms?.join(', ') || 'default';
|
|
258
|
-
logger.log('info', `Setting up Docker buildx
|
|
259
|
-
|
|
339
|
+
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
|
|
340
|
+
logger.log('info', `Builder: ${builderName}`);
|
|
341
|
+
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
|
|
260
342
|
|
|
261
343
|
if (inspectResult.exitCode !== 0) {
|
|
262
344
|
logger.log('info', 'Creating new buildx builder with host network...');
|
|
263
345
|
await smartshellInstance.exec(
|
|
264
|
-
|
|
346
|
+
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
|
|
265
347
|
);
|
|
266
348
|
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
|
267
349
|
} else {
|
|
268
350
|
const inspectOutput = inspectResult.stdout || '';
|
|
269
351
|
if (!inspectOutput.includes('network=host')) {
|
|
270
352
|
logger.log('info', 'Recreating buildx builder with host network (migration)...');
|
|
271
|
-
await smartshellInstance.exec(
|
|
353
|
+
await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
|
|
272
354
|
await smartshellInstance.exec(
|
|
273
|
-
|
|
355
|
+
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
|
|
274
356
|
);
|
|
275
357
|
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
|
276
358
|
} else {
|
|
277
|
-
await smartshellInstance.exec(
|
|
359
|
+
await smartshellInstance.exec(`docker buildx use ${builderName}`);
|
|
278
360
|
}
|
|
279
361
|
}
|
|
280
|
-
logger.log('ok', `Docker buildx ready (platforms: ${platforms})`);
|
|
362
|
+
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
|
|
281
363
|
}
|
|
282
364
|
|
|
283
365
|
/**
|
package/ts/interfaces/index.ts
CHANGED
|
@@ -79,6 +79,9 @@ export interface IBuildCommandOptions {
|
|
|
79
79
|
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
|
|
80
80
|
cached?: boolean; // Skip builds when Dockerfile content hasn't changed
|
|
81
81
|
verbose?: boolean; // Stream raw docker build output (default: silent)
|
|
82
|
+
context?: string; // Explicit Docker context name (--context flag)
|
|
83
|
+
parallel?: boolean; // Enable parallel builds within dependency levels
|
|
84
|
+
parallelConcurrency?: number; // Max concurrent builds per level (default 4)
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
export interface ICacheEntry {
|
|
@@ -92,3 +95,10 @@ export interface ICacheData {
|
|
|
92
95
|
version: 1;
|
|
93
96
|
entries: { [cleanTag: string]: ICacheEntry };
|
|
94
97
|
}
|
|
98
|
+
|
|
99
|
+
export interface IDockerContextInfo {
|
|
100
|
+
name: string; // 'default', 'rootless', 'colima', etc.
|
|
101
|
+
endpoint: string; // 'unix:///var/run/docker.sock'
|
|
102
|
+
isRootless: boolean;
|
|
103
|
+
dockerHost?: string; // value of DOCKER_HOST env var, if set
|
|
104
|
+
}
|
package/ts/tsdocker.cli.ts
CHANGED
|
@@ -33,7 +33,7 @@ export let run = () => {
|
|
|
33
33
|
try {
|
|
34
34
|
const config = await ConfigModule.run();
|
|
35
35
|
const manager = new TsDockerManager(config);
|
|
36
|
-
await manager.prepare();
|
|
36
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
37
37
|
|
|
38
38
|
const buildOptions: IBuildCommandOptions = {};
|
|
39
39
|
const patterns = argvArg._.slice(1) as string[];
|
|
@@ -55,6 +55,12 @@ export let run = () => {
|
|
|
55
55
|
if (argvArg.verbose) {
|
|
56
56
|
buildOptions.verbose = true;
|
|
57
57
|
}
|
|
58
|
+
if (argvArg.parallel) {
|
|
59
|
+
buildOptions.parallel = true;
|
|
60
|
+
if (typeof argvArg.parallel === 'number') {
|
|
61
|
+
buildOptions.parallelConcurrency = argvArg.parallel;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
58
64
|
|
|
59
65
|
await manager.build(buildOptions);
|
|
60
66
|
logger.log('success', 'Build completed successfully');
|
|
@@ -72,7 +78,7 @@ export let run = () => {
|
|
|
72
78
|
try {
|
|
73
79
|
const config = await ConfigModule.run();
|
|
74
80
|
const manager = new TsDockerManager(config);
|
|
75
|
-
await manager.prepare();
|
|
81
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
76
82
|
|
|
77
83
|
// Login first
|
|
78
84
|
await manager.login();
|
|
@@ -95,6 +101,12 @@ export let run = () => {
|
|
|
95
101
|
if (argvArg.verbose) {
|
|
96
102
|
buildOptions.verbose = true;
|
|
97
103
|
}
|
|
104
|
+
if (argvArg.parallel) {
|
|
105
|
+
buildOptions.parallel = true;
|
|
106
|
+
if (typeof argvArg.parallel === 'number') {
|
|
107
|
+
buildOptions.parallelConcurrency = argvArg.parallel;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
98
110
|
|
|
99
111
|
// Build images first (if not already built)
|
|
100
112
|
await manager.build(buildOptions);
|
|
@@ -124,7 +136,7 @@ export let run = () => {
|
|
|
124
136
|
|
|
125
137
|
const config = await ConfigModule.run();
|
|
126
138
|
const manager = new TsDockerManager(config);
|
|
127
|
-
await manager.prepare();
|
|
139
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
128
140
|
|
|
129
141
|
// Login first
|
|
130
142
|
await manager.login();
|
|
@@ -144,7 +156,7 @@ export let run = () => {
|
|
|
144
156
|
try {
|
|
145
157
|
const config = await ConfigModule.run();
|
|
146
158
|
const manager = new TsDockerManager(config);
|
|
147
|
-
await manager.prepare();
|
|
159
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
148
160
|
|
|
149
161
|
// Build images first
|
|
150
162
|
const buildOptions: IBuildCommandOptions = {};
|
|
@@ -157,6 +169,12 @@ export let run = () => {
|
|
|
157
169
|
if (argvArg.verbose) {
|
|
158
170
|
buildOptions.verbose = true;
|
|
159
171
|
}
|
|
172
|
+
if (argvArg.parallel) {
|
|
173
|
+
buildOptions.parallel = true;
|
|
174
|
+
if (typeof argvArg.parallel === 'number') {
|
|
175
|
+
buildOptions.parallelConcurrency = argvArg.parallel;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
160
178
|
await manager.build(buildOptions);
|
|
161
179
|
|
|
162
180
|
// Run tests
|
|
@@ -175,7 +193,7 @@ export let run = () => {
|
|
|
175
193
|
try {
|
|
176
194
|
const config = await ConfigModule.run();
|
|
177
195
|
const manager = new TsDockerManager(config);
|
|
178
|
-
await manager.prepare();
|
|
196
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
179
197
|
await manager.login();
|
|
180
198
|
logger.log('success', 'Login completed successfully');
|
|
181
199
|
} catch (err) {
|
|
@@ -191,7 +209,7 @@ export let run = () => {
|
|
|
191
209
|
try {
|
|
192
210
|
const config = await ConfigModule.run();
|
|
193
211
|
const manager = new TsDockerManager(config);
|
|
194
|
-
await manager.prepare();
|
|
212
|
+
await manager.prepare(argvArg.context as string | undefined);
|
|
195
213
|
await manager.list();
|
|
196
214
|
} catch (err) {
|
|
197
215
|
logger.log('error', `List failed: ${(err as Error).message}`);
|