Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Notable changes.

### [0.88.0]
- Add WSLc support (https://github.com/devcontainers/cli/pull/1249)
- Add an internal Docker CLI argument-prefix option for provider-specific target selection.

## May 2026

Expand Down
23 changes: 23 additions & 0 deletions src/spec-common/dockerPathArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export function parseDockerPathArgs(value: string | undefined): string[] | undefined {
if (value === undefined) {
return undefined;
}

let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error('--docker-path-args must be a JSON array of strings.');
}

if (!Array.isArray(parsed) || !parsed.every(arg => typeof arg === 'string')) {
throw new Error('--docker-path-args must be a JSON array of strings.');
}

return parsed;
}
4 changes: 2 additions & 2 deletions src/spec-node/configContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
const configWithRaw = addSubstitution(configs.config, config => beforeContainerSubstitute(envListToObj(idLabels), config));
const { config } = configWithRaw;

const { dockerCLI, dockerComposeCLI } = params;
const { dockerCLI, dockerPathArgs, dockerComposeCLI } = params;
const { env } = common;
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
await ensureNoDisallowedFeatures(cliParams, config, additionalFeatures, idLabels);

await runInitializeCommand({ ...params, common: { ...common, output: common.lifecycleHook.output } }, config.initializeCommand, common.lifecycleHook.onDidInput);
Expand Down
7 changes: 6 additions & 1 deletion src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Event } from '../spec-utils/event';

export interface ProvisionOptions {
dockerPath: string | undefined;
dockerPathArgs?: string[];
dockerComposePath: string | undefined;
containerDataFolder: string | undefined;
containerSystemDataFolder: string | undefined;
Expand Down Expand Up @@ -168,6 +169,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
const dockerComposePath = options.dockerComposePath || 'docker-compose';
const dockerComposeCLI = dockerComposeCLIConfig({
exec: cliHost.exec,
args: options.dockerPathArgs,
env: cliHost.env,
output: common.output,
}, dockerPath, dockerComposePath);
Expand Down Expand Up @@ -206,18 +208,20 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
const buildKitVersion = options.useBuildKit === 'never' ? undefined : (await dockerBuildKitVersion({
cliHost,
dockerCLI: dockerPath,
dockerPathArgs: options.dockerPathArgs,
dockerComposeCLI,
env: cliHost.env,
output,
buildPlatformInfo,
targetPlatformInfo
}));

const cliVariant = await lookupCLIVariant({ exec: cliHost.exec, cmd: dockerPath, env: cliHost.env, output });
const cliVariant = await lookupCLIVariant({ exec: cliHost.exec, cmd: dockerPath, args: options.dockerPathArgs, env: cliHost.env, output });

const dockerEngineVer = await dockerEngineVersion({
cliHost,
dockerCLI: dockerPath,
dockerPathArgs: options.dockerPathArgs,
dockerComposeCLI,
env: cliHost.env,
output,
Expand All @@ -229,6 +233,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
common,
parsedAuthority,
dockerCLI: dockerPath,
dockerPathArgs: options.dockerPathArgs,
cliVariant,
dockerComposeCLI: dockerComposeCLI,
dockerEnv: cliHost.env,
Expand Down
22 changes: 21 additions & 1 deletion src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { featuresUpgradeHandler, featuresUpgradeOptions } from './upgradeCommand
import { readFeaturesConfig } from './featureUtils';
import { featuresGenerateDocsHandler, featuresGenerateDocsOptions } from './featuresCLI/generateDocs';
import { templatesGenerateDocsHandler, templatesGenerateDocsOptions } from './templatesCLI/generateDocs';
import { parseDockerPathArgs } from '../spec-common/dockerPathArgs';
import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI';
import { templateMetadataHandler, templateMetadataOptions } from './templatesCLI/metadata';

Expand Down Expand Up @@ -100,6 +101,7 @@ export type UnpackArgv<T> = T extends Argv<infer U> ? U : T;
function provisionOptions(y: Argv) {
return y.options({
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
Expand Down Expand Up @@ -185,6 +187,7 @@ function provisionHandler(args: ProvisionArgs) {
async function provision({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'docker-compose-path': dockerComposePath,
'container-data-folder': containerDataFolder,
'container-system-data-folder': containerSystemDataFolder,
Expand Down Expand Up @@ -246,6 +249,7 @@ async function provision({

const options: ProvisionOptions = {
dockerPath,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposePath,
containerDataFolder,
containerSystemDataFolder,
Expand Down Expand Up @@ -354,6 +358,7 @@ async function doProvision(options: ProvisionOptions, providedIdLabels: string[]
function setUpOptions(y: Argv) {
return y.options({
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
'container-id': { type: 'string', required: true, description: 'Id of the container.' },
Expand Down Expand Up @@ -402,6 +407,7 @@ async function setUp(args: SetUpArgs) {
async function doSetUp({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'container-data-folder': containerDataFolder,
'container-system-data-folder': containerSystemDataFolder,
'container-id': containerId,
Expand Down Expand Up @@ -431,6 +437,7 @@ async function doSetUp({
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
const params = await createDockerParams({
dockerPath,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposePath: undefined,
containerSessionDataFolder,
containerDataFolder,
Expand Down Expand Up @@ -524,6 +531,7 @@ function buildOptions(y: Argv) {
return y.options({
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If not provided, defaults to the current directory.' },
'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' },
Expand Down Expand Up @@ -580,6 +588,7 @@ async function build(args: BuildArgs) {
async function doBuild({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'docker-compose-path': dockerComposePath,
'workspace-folder': workspaceFolderArg,
config: configParam,
Expand Down Expand Up @@ -618,6 +627,7 @@ async function doBuild({
const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record<string, string | boolean | Record<string, string | boolean>> : {};
const params = await createDockerParams({
dockerPath,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposePath,
containerDataFolder: undefined,
containerSystemDataFolder: undefined,
Expand Down Expand Up @@ -676,7 +686,7 @@ async function doBuild({
throw new ContainerError({ description: '--push true cannot be used with --output.' });
}

const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerPathArgs: params.dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined);

// Support multiple use of `--image-name`
Expand Down Expand Up @@ -787,6 +797,7 @@ function runUserCommandsOptions(y: Argv) {
return y.options({
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
Expand Down Expand Up @@ -848,6 +859,7 @@ async function runUserCommands(args: RunUserCommandsArgs) {
async function doRunUserCommands({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'docker-compose-path': dockerComposePath,
'container-data-folder': containerDataFolder,
'container-system-data-folder': containerSystemDataFolder,
Expand Down Expand Up @@ -892,6 +904,7 @@ async function doRunUserCommands({

const params = await createDockerParams({
dockerPath,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposePath,
containerDataFolder,
containerSystemDataFolder,
Expand Down Expand Up @@ -994,6 +1007,7 @@ function readConfigurationOptions(y: Argv) {
return y.options({
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If --container-id, --id-label, and --workspace-folder are not provided, this defaults to the current directory.' },
'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' },
Expand Down Expand Up @@ -1032,6 +1046,7 @@ function readConfigurationHandler(args: ReadConfigurationArgs) {
async function readConfiguration({
// 'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'docker-compose-path': dockerComposePath,
'workspace-folder': workspaceFolderArg,
'mount-workspace-git-root': mountWorkspaceGitRoot,
Expand Down Expand Up @@ -1090,6 +1105,7 @@ async function readConfiguration({
const dockerCLI = dockerPath || 'docker';
const dockerComposeCLI = dockerComposeCLIConfig({
exec: cliHost.exec,
args: parseDockerPathArgs(dockerPathArgs),
env: cliHost.env,
output,
}, dockerCLI, dockerComposePath || 'docker-compose');
Expand All @@ -1100,6 +1116,7 @@ async function readConfiguration({
const params: DockerCLIParameters = {
cliHost,
dockerCLI,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposeCLI,
env: cliHost.env,
output,
Expand Down Expand Up @@ -1251,6 +1268,7 @@ function execOptions(y: Argv) {
return y.options({
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
'docker-path': { type: 'string', description: 'Docker CLI path.' },
'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' },
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
Expand Down Expand Up @@ -1313,6 +1331,7 @@ async function exec(args: ExecArgs) {
export async function doExec({
'user-data-folder': persistedFolder,
'docker-path': dockerPath,
'docker-path-args': dockerPathArgs,
'docker-compose-path': dockerComposePath,
'container-data-folder': containerDataFolder,
'container-system-data-folder': containerSystemDataFolder,
Expand Down Expand Up @@ -1346,6 +1365,7 @@ export async function doExec({
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;
const params = await createDockerParams({
dockerPath,
dockerPathArgs: parseDockerPathArgs(dockerPathArgs),
dockerComposePath,
containerDataFolder,
containerSystemDataFolder,
Expand Down
14 changes: 8 additions & 6 deletions src/spec-node/dockerCompose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const projectLabel = 'com.docker.compose.project';
const serviceLabel = 'com.docker.compose.service';

export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: SubstitutedConfig<DevContainerFromDockerComposeConfig>, idLabels: string[], additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>): Promise<ResolverResult> {
const { common, dockerCLI, dockerComposeCLI } = params;
const { common, dockerCLI, dockerPathArgs, dockerComposeCLI } = params;
const { cliHost, env, output } = common;
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config.config), idLabels, additionalFeatures);
}

Expand Down Expand Up @@ -151,11 +151,11 @@ export function getBuildInfoForService(composeService: any, cliHostPath: typeof

export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConfig<DevContainerFromDockerComposeConfig>, projectName: string, params: DockerResolverParameters, localComposeFiles: string[], envFile: string | undefined, composeGlobalArgs: string[], runServices: string[], noCache: boolean, overrideFilePath: string, overrideFilePrefix: string, versionPrefix: string, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>, canAddLabelsToContainer: boolean, additionalCacheFroms?: string[], noBuild?: boolean) {

const { common, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc } = params;
const { common, dockerCLI, dockerPathArgs, dockerComposeCLI: dockerComposeCLIFunc } = params;
const { cliHost, env, output } = common;
const { config } = configWithRaw;

const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const composeConfig = await readDockerComposeConfig(cliParams, localComposeFiles, envFile);
const composeService = composeConfig.services[config.service];

Expand Down Expand Up @@ -705,16 +705,18 @@ export function dockerComposeCLIConfig(params: Omit<PartialExecParameters, 'cmd'
let result: Promise<DockerComposeCLI>;
return () => {
return result || (result = (async () => {
const { args: dockerPathArgs, ...baseParams } = params;
let v2 = true;
let stdout: Buffer;
try {
stdout = (await dockerComposeCLI({
...params,
...baseParams,
cmd: dockerCLICmd,
args: dockerPathArgs,
}, 'compose', 'version', '--short')).stdout;
} catch (err) {
stdout = (await dockerComposeCLI({
...params,
...baseParams,
cmd: dockerComposeCLICmd,
}, 'version', '--short')).stdout;
v2 = false;
Expand Down
1 change: 1 addition & 0 deletions src/spec-node/featuresCLI/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const staticProvisionParams = {
export const staticExecParams = {
'user-data-folder': undefined,
'docker-path': undefined,
'docker-path-args': undefined,
'docker-compose-path': undefined,
'container-data-folder': undefined,
'container-system-data-folder': undefined,
Expand Down
4 changes: 2 additions & 2 deletions src/spec-node/imageMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export interface ImageBuildInfo {
}

export async function getImageBuildInfo(params: DockerResolverParameters | DockerCLIParameters, configWithRaw: SubstitutedConfig<DevContainerConfig>): Promise<ImageBuildInfo> {
const { dockerCLI, dockerComposeCLI } = params;
const { dockerCLI, dockerPathArgs, dockerComposeCLI } = params;
const { cliHost, output } = 'cliHost' in params ? params : params.common;

const { config } = configWithRaw;
Expand All @@ -350,7 +350,7 @@ export async function getImageBuildInfo(params: DockerResolverParameters | Docke
const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env');
const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined;
const composeFiles = await getDockerComposeFilePaths(cliHost, config, cliHost.env, cliHost.cwd);
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env: cliHost.env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };
const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env: cliHost.env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo };

const composeConfig = await readDockerComposeConfig(buildParams, composeFiles, envFile);
const services = Object.keys(composeConfig.services || {});
Expand Down
Loading