From 0859e15cfdd196b08027afb6b90ab1b46b255f8c Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 20 May 2026 14:19:28 +0000 Subject: [PATCH 1/4] Support for building from Dockerfile.in-style templates using a new dockerfilePreprocessor in devcontainer.json --- src/spec-configuration/configuration.ts | 7 + src/spec-node/dockerCompose.ts | 6 +- src/spec-node/dockerfilePreprocessor.ts | 85 +++++++ src/spec-node/singleContainer.ts | 8 +- .../.devcontainer.json | 17 ++ .../Dockerfile.in | 9 + .../configure.ac | 11 + .../.devcontainer.json | 16 ++ .../CMakeLists.txt | 11 + .../Dockerfile.in | 9 + .../.devcontainer.json | 16 ++ .../CMakeLists.txt | 11 + .../Dockerfile.in | 9 + .../.devcontainer.json | 16 ++ .../dockerfile-cpp-preprocessor/Dockerfile.in | 16 ++ .../common.Dockerfile | 4 + .../dockerfile-cpp-preprocessor/test.sh | 3 + .../tools.Dockerfile | 2 + .../.devcontainer.json | 16 ++ .../Dockerfile.in | 9 + .../dockerfile-meson-preprocessor/meson.build | 11 + src/test/dockerfilePreprocessor.test.ts | 218 ++++++++++++++++++ 22 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 src/spec-node/dockerfilePreprocessor.ts create mode 100644 src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json create mode 100644 src/test/configs/dockerfile-autoconf-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockerfile-autoconf-preprocessor/configure.ac create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json create mode 100644 src/test/configs/dockerfile-cmake2-preprocessor/CMakeLists.txt create mode 100644 src/test/configs/dockerfile-cmake2-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/common.Dockerfile create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/test.sh create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/tools.Dockerfile create mode 100644 src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json create mode 100644 src/test/configs/dockerfile-meson-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockerfile-meson-preprocessor/meson.build create mode 100644 src/test/dockerfilePreprocessor.test.ts diff --git a/src/spec-configuration/configuration.ts b/src/spec-configuration/configuration.ts index 5995e7e2b..2f8e36255 100644 --- a/src/spec-configuration/configuration.ts +++ b/src/spec-configuration/configuration.ts @@ -38,6 +38,11 @@ export interface DevContainerFeature { options: boolean | string | Record; } +export interface DockerfilePreprocessor { + commands?: string[]; + output?: string; +} + export interface DevContainerFromImageConfig { configFilePath?: URI; image?: string; // Only optional when setting up an existing container as a dev container. @@ -111,6 +116,7 @@ export type DevContainerFromDockerfileConfig = { overrideFeatureInstallOrder?: string[]; hostRequirements?: HostRequirements; customizations?: Record; + dockerfilePreprocessor?: DockerfilePreprocessor; } & ( { dockerFile: string; @@ -169,6 +175,7 @@ export interface DevContainerFromDockerComposeConfig { overrideFeatureInstallOrder?: string[]; hostRequirements?: HostRequirements; customizations?: Record; + dockerfilePreprocessor?: DockerfilePreprocessor; } interface DevContainerVSCodeConfig { diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 8093464cc..5c11fb5c0 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -19,6 +19,7 @@ import { Mount, parseMount } from '../spec-configuration/containerFeaturesConfig import path from 'path'; import { getDevcontainerMetadata, getImageBuildInfoFromDockerfile, getImageBuildInfoFromImage, getImageMetadataFromContainer, ImageBuildInfo, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata'; import { ensureDockerfileHasFinalStageName } from './dockerfileUtils'; +import { preprocessDockerExtensionFile } from './dockerfilePreprocessor'; import { randomUUID } from 'crypto'; const projectLabel = 'com.docker.compose.project'; @@ -166,7 +167,10 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf const serviceInfo = getBuildInfoForService(composeService, cliHost.path, localComposeFiles); if (serviceInfo.build) { const { context, dockerfilePath, target } = serviceInfo.build; - const resolvedDockerfilePath = cliHost.path.isAbsolute(dockerfilePath) ? dockerfilePath : path.resolve(context, dockerfilePath); + let resolvedDockerfilePath = cliHost.path.isAbsolute(dockerfilePath) ? dockerfilePath : path.resolve(context, dockerfilePath); + if (resolvedDockerfilePath.toLowerCase().endsWith('.in')) { + resolvedDockerfilePath = await preprocessDockerExtensionFile(common, config, resolvedDockerfilePath); + } const originalDockerfile = (await cliHost.readFile(resolvedDockerfilePath)).toString(); dockerfile = originalDockerfile; if (target) { diff --git a/src/spec-node/dockerfilePreprocessor.ts b/src/spec-node/dockerfilePreprocessor.ts new file mode 100644 index 000000000..c6df7ad5f --- /dev/null +++ b/src/spec-node/dockerfilePreprocessor.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig } from '../spec-configuration/configuration'; +import { ContainerError, toErrorText } from '../spec-common/errors'; +import { CLIHost, runCommandNoPty } from '../spec-common/commonUtils'; +import { Log, LogLevel, makeLog } from '../spec-utils/log'; + +export function getDockerfilePreprocessedPath(dockerfilePath: string, output?: string): string | undefined { + if (!dockerfilePath.toLowerCase().endsWith('.in')) { + return undefined; + } + if (output) { + return path.isAbsolute(output) ? output : path.join(path.dirname(dockerfilePath), output); + } + return dockerfilePath.slice(0, -3); +} + +export async function preprocessDockerExtensionFile( + params: { cliHost: CLIHost; output: Log }, + config: Pick, + dockerfilePath: string +): Promise { + const outputDockerfilePath = getDockerfilePreprocessedPath(dockerfilePath, config.dockerfilePreprocessor?.output); + if (!outputDockerfilePath) { + return dockerfilePath; + } + + const commands = (config.dockerfilePreprocessor?.commands || []).map(command => command.trim()).filter(command => command.length > 0); + if (!commands.length) { + throw new ContainerError({ + description: `Dockerfile preprocessor commands are required to build from '${dockerfilePath}'. Set 'dockerfilePreprocessor.commands' in devcontainer.json.`, + data: { fileWithError: dockerfilePath }, + }); + } + + const { cliHost, output } = params; + const infoOutput = makeLog(output, LogLevel.Info); + const isWindows = cliHost.platform === 'win32'; + const shell = isWindows ? [cliHost.env.ComSpec || 'cmd.exe', '/c'] : ['/bin/sh', '-c']; + + const env = { + ...cliHost.env, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: dockerfilePath, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputDockerfilePath, + input_file: dockerfilePath, + output_file: outputDockerfilePath, + }; + + try { + infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${outputDockerfilePath}'`); + for (const command of commands) { + await runCommandNoPty({ + exec: cliHost.exec, + cmd: shell[0], + args: [shell[1], command], + cwd: path.dirname(dockerfilePath), + env, + output: infoOutput, + print: 'continuous', + }); + } + } catch (err) { + throw new ContainerError({ + description: `Dockerfile preprocessing failed while running '${commands[commands.length - 1]}'.`, + originalError: { + ...err, + message: `${err?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(err?.stderr || err?.cmdOutput || '')}`.trim(), + }, + data: { fileWithError: dockerfilePath }, + }); + } + + if (!await cliHost.isFile(outputDockerfilePath)) { + throw new ContainerError({ + description: `Dockerfile preprocessing did not produce '${outputDockerfilePath}'.`, + data: { fileWithError: dockerfilePath }, + }); + } + + return outputDockerfilePath; +} diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 1c3669f74..a402e3892 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -13,6 +13,7 @@ import { LogLevel, Log, makeLog } from '../spec-utils/log'; import { extendImage, getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures'; import { getDevcontainerMetadata, getImageBuildInfoFromDockerfile, getImageMetadataFromContainer, ImageMetadataEntry, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata'; import { ensureDockerfileHasFinalStageName, generateMountCommand } from './dockerfileUtils'; +import { preprocessDockerExtensionFile } from './dockerfilePreprocessor'; export const hostFolderLabel = 'devcontainer.local_folder'; // used to label containers created from a workspace/folder export const configFileLabel = 'devcontainer.config_file'; @@ -125,8 +126,11 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config const { cliHost, output } = buildParams.common; const { config } = configWithRaw; const dockerfileUri = getDockerfilePath(cliHost, config); - const dockerfilePath = await uriToWSLFsPath(dockerfileUri, cliHost); - if (!cliHost.isFile(dockerfilePath)) { + let dockerfilePath = await uriToWSLFsPath(dockerfileUri, cliHost); + if (dockerfilePath.toLowerCase().endsWith('.in')) { + dockerfilePath = await preprocessDockerExtensionFile(buildParams.common, config, dockerfilePath); + } + if (!await cliHost.isFile(dockerfilePath)) { throw new ContainerError({ description: `Dockerfile (${dockerfilePath}) not found.` }); } diff --git a/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json new file mode 100644 index 000000000..13764eed5 --- /dev/null +++ b/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json @@ -0,0 +1,17 @@ +{ + "build": { + "dockerfile": "Dockerfile.in" + }, + "dockerfilePreprocessor": { + "commands": [ + "autoconf", + "./configure" + ], + "output": "Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/src/test/configs/dockerfile-autoconf-preprocessor/Dockerfile.in b/src/test/configs/dockerfile-autoconf-preprocessor/Dockerfile.in new file mode 100644 index 000000000..624fb764c --- /dev/null +++ b/src/test/configs/dockerfile-autoconf-preprocessor/Dockerfile.in @@ -0,0 +1,9 @@ +FROM @BASE_IMAGE@ + +ARG APP_PORT=@APP_PORT@ +EXPOSE @APP_PORT@ + +WORKDIR /workspace +COPY . /workspace + +CMD ["npm", "start"] \ No newline at end of file diff --git a/src/test/configs/dockerfile-autoconf-preprocessor/configure.ac b/src/test/configs/dockerfile-autoconf-preprocessor/configure.ac new file mode 100644 index 000000000..325410972 --- /dev/null +++ b/src/test/configs/dockerfile-autoconf-preprocessor/configure.ac @@ -0,0 +1,11 @@ +AC_INIT([generate-dockerfile], [1.0]) +AC_CONFIG_SRCDIR([Dockerfile.in]) + +BASE_IMAGE='node:22-bookworm' +APP_PORT='3000' + +AC_SUBST([BASE_IMAGE]) +AC_SUBST([APP_PORT]) + +AC_CONFIG_FILES([Dockerfile]) +AC_OUTPUT \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json new file mode 100644 index 000000000..95e81208d --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json @@ -0,0 +1,16 @@ +{ + "build": { + "dockerfile": "Dockerfile.in" + }, + "dockerfilePreprocessor": { + "commands": [ + "cmake -S . -B build" + ], + "output": "build/Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt b/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt new file mode 100644 index 000000000..0f2118862 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) +project(GenerateDockerfile NONE) + +set(BASE_IMAGE "node:22-bookworm") +set(APP_PORT "3000") + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/Dockerfile.in + ${CMAKE_CURRENT_BINARY_DIR}/Dockerfile + @ONLY +) \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in b/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in new file mode 100644 index 000000000..624fb764c --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in @@ -0,0 +1,9 @@ +FROM @BASE_IMAGE@ + +ARG APP_PORT=@APP_PORT@ +EXPOSE @APP_PORT@ + +WORKDIR /workspace +COPY . /workspace + +CMD ["npm", "start"] \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json new file mode 100644 index 000000000..149f52fa6 --- /dev/null +++ b/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json @@ -0,0 +1,16 @@ +{ + "build": { + "dockerfile": "Dockerfile.in" + }, + "dockerfilePreprocessor": { + "commands": [ + "cmake -S . -B build" + ], + "output": "" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake2-preprocessor/CMakeLists.txt b/src/test/configs/dockerfile-cmake2-preprocessor/CMakeLists.txt new file mode 100644 index 000000000..392203510 --- /dev/null +++ b/src/test/configs/dockerfile-cmake2-preprocessor/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) +project(GenerateDockerfile NONE) + +set(BASE_IMAGE "node:22-bookworm") +set(APP_PORT "3000") + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/Dockerfile.in + ${CMAKE_CURRENT_SOURCE_DIR}/Dockerfile + @ONLY +) \ No newline at end of file diff --git a/src/test/configs/dockerfile-cmake2-preprocessor/Dockerfile.in b/src/test/configs/dockerfile-cmake2-preprocessor/Dockerfile.in new file mode 100644 index 000000000..624fb764c --- /dev/null +++ b/src/test/configs/dockerfile-cmake2-preprocessor/Dockerfile.in @@ -0,0 +1,9 @@ +FROM @BASE_IMAGE@ + +ARG APP_PORT=@APP_PORT@ +EXPOSE @APP_PORT@ + +WORKDIR /workspace +COPY . /workspace + +CMD ["npm", "start"] \ No newline at end of file diff --git a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json new file mode 100644 index 000000000..d93572a2e --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json @@ -0,0 +1,16 @@ +{ + "build": { + "dockerfile": "Dockerfile.in" + }, + "dockerfilePreprocessor": { + "commands": [ + "cpp -P \"$DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT\" > \"$DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT\"" + ], + "output": "Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/src/test/configs/dockerfile-cpp-preprocessor/Dockerfile.in b/src/test/configs/dockerfile-cpp-preprocessor/Dockerfile.in new file mode 100644 index 000000000..d7e4fcd28 --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/Dockerfile.in @@ -0,0 +1,16 @@ +#define BASE_IMAGE ubuntu:20.04 +#define INSTALL_NODE +#define INSTALL_PYTHON + +FROM BASE_IMAGE + +#ifdef INSTALL_NODE +RUN apt-get update && apt-get install -y nodejs +#endif + +#ifdef INSTALL_PYTHON +RUN apt-get update && apt-get install -y python3 +#endif + +#include "common.Dockerfile" +#include "tools.Dockerfile" \ No newline at end of file diff --git a/src/test/configs/dockerfile-cpp-preprocessor/common.Dockerfile b/src/test/configs/dockerfile-cpp-preprocessor/common.Dockerfile new file mode 100644 index 000000000..97856b250 --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/common.Dockerfile @@ -0,0 +1,4 @@ +RUN apt-get update && apt-get install -y curl wget + +ENV APP_ENV=development +ENV APP_DEBUG=true \ No newline at end of file diff --git a/src/test/configs/dockerfile-cpp-preprocessor/test.sh b/src/test/configs/dockerfile-cpp-preprocessor/test.sh new file mode 100644 index 000000000..96f725cfb --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "hello! cpp test" + diff --git a/src/test/configs/dockerfile-cpp-preprocessor/tools.Dockerfile b/src/test/configs/dockerfile-cpp-preprocessor/tools.Dockerfile new file mode 100644 index 000000000..6cbd0129d --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/tools.Dockerfile @@ -0,0 +1,2 @@ +RUN apt-get update && apt-get install -y vim +COPY ./test.sh /usr/local/bin/test.sh \ No newline at end of file diff --git a/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json new file mode 100644 index 000000000..ce8cb677c --- /dev/null +++ b/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json @@ -0,0 +1,16 @@ +{ + "build": { + "dockerfile": "Dockerfile.in" + }, + "dockerfilePreprocessor": { + "commands": [ + "meson setup build --reconfigure || meson setup build" + ], + "output": "build/Dockerfile" + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/src/test/configs/dockerfile-meson-preprocessor/Dockerfile.in b/src/test/configs/dockerfile-meson-preprocessor/Dockerfile.in new file mode 100644 index 000000000..624fb764c --- /dev/null +++ b/src/test/configs/dockerfile-meson-preprocessor/Dockerfile.in @@ -0,0 +1,9 @@ +FROM @BASE_IMAGE@ + +ARG APP_PORT=@APP_PORT@ +EXPOSE @APP_PORT@ + +WORKDIR /workspace +COPY . /workspace + +CMD ["npm", "start"] \ No newline at end of file diff --git a/src/test/configs/dockerfile-meson-preprocessor/meson.build b/src/test/configs/dockerfile-meson-preprocessor/meson.build new file mode 100644 index 000000000..9c0f68cd1 --- /dev/null +++ b/src/test/configs/dockerfile-meson-preprocessor/meson.build @@ -0,0 +1,11 @@ +project('generate-dockerfile', 'c') + +conf = configuration_data() +conf.set('BASE_IMAGE', 'node:22-bookworm') +conf.set('APP_PORT', '3000') + +configure_file( + input: 'Dockerfile.in', + output: 'Dockerfile', + configuration: conf +) \ No newline at end of file diff --git a/src/test/dockerfilePreprocessor.test.ts b/src/test/dockerfilePreprocessor.test.ts new file mode 100644 index 000000000..0bf812f64 --- /dev/null +++ b/src/test/dockerfilePreprocessor.test.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; + +import { ContainerError } from '../spec-common/errors'; +import { getCLIHost, loadNativeModule } from '../spec-common/commonUtils'; +import { preprocessDockerExtensionFile, getDockerfilePreprocessedPath } from '../spec-node/dockerfilePreprocessor'; +import { nullLog } from '../spec-utils/log'; +import { devContainerDown, devContainerUp, shellExec } from './testUtils'; + +const pkg = require('../../package.json'); + +describe('dockerfilePreprocessor', function () { + it('returns undefined for non-.in Dockerfile', () => { + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/Dockerfile'), undefined); + }); + + it('returns preprocessed path for .in Dockerfile', () => { + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/Dockerfile.in'), '/tmp/Dockerfile'); + }); + + it('returns configured relative output path', () => { + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile.in', 'generated/Dockerfile'), '/tmp/folder/generated/Dockerfile'); + }); + + it('returns undefined for non-.in Dockerfile even when output is configured', () => { + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile', 'generated/Dockerfile'), undefined); + }); + + it('throws when dockerfilePreprocessor.commands is missing for .in Dockerfile', async () => { + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + await assert.rejects( + preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + {}, + '/tmp/Dockerfile.in' + ), + (err: unknown) => { + assert.ok(err instanceof ContainerError); + assert.match((err as ContainerError).description, /dockerfilePreprocessor\.commands/i); + return true; + } + ); + }); + + it('runs commands and produces Dockerfile output', async function () { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + const outputPath = path.join(tmpDir, 'Dockerfile'); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + const result = await preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { commands: ['cat "$input_file" > "$output_file"'] } }, + inputPath + ); + + assert.strictEqual(result, outputPath); + const outputContent = (await fs.readFile(outputPath)).toString(); + assert.strictEqual(outputContent, 'FROM alpine:3.20\n'); + }); + + it('runs ordered commands and writes configured output file', async function () { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + const outputPath = path.join(tmpDir, 'generated', 'Dockerfile'); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + const result = await preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { commands: ['cp "$input_file" "$output_file"'], output: 'generated/Dockerfile' } }, + inputPath + ); + + assert.strictEqual(result, outputPath); + const outputContent = (await fs.readFile(outputPath)).toString(); + assert.strictEqual(outputContent, 'FROM alpine:3.20\n'); + }); + + it('throws when a preprocessor command fails', async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + await assert.rejects(preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { commands: ['this-command-should-not-exist-xyz123'] } }, + inputPath + )); + }); + + it('throws when commands succeed but output Dockerfile is not generated', async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + await assert.rejects( + preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { commands: ['true'] } }, + inputPath + ), + (err: unknown) => { + assert.ok(err instanceof ContainerError); + assert.match((err as ContainerError).description, /did not produce/i); + return true; + } + ); + }); +}); + +(process.platform === 'linux' ? describe : describe.skip)('dockerfilePreprocessor integration', function () { + this.timeout('240s'); + + const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp')); + const cli = `npx --prefix ${tmp} devcontainer`; + let cppAvailable = false; + let cmakeAvailable = false; + let mesonAvailable = false; + let autoconfAvailable = false; + + before('Install', async () => { + await shellExec(`rm -rf ${tmp}/node_modules`); + await shellExec(`mkdir -p ${tmp}`); + await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`); + const commandCheck = await shellExec('command -v cpp', undefined, true, true); + cppAvailable = Boolean(commandCheck.stdout.trim()); + const cmakeCheck = await shellExec('command -v cmake', undefined, true, true); + cmakeAvailable = Boolean(cmakeCheck.stdout.trim()); + const mesonCheck = await shellExec('command -v meson', undefined, true, true); + mesonAvailable = Boolean(mesonCheck.stdout.trim()); + const autoconfCheck = await shellExec('command -v autoconf', undefined, true, true); + autoconfAvailable = Boolean(autoconfCheck.stdout.trim()); + }); + + it('should preprocess a Dockerfile.in during up cpp', async function () { + if (!cppAvailable) { + this.skip(); + } + const testFolder = `${__dirname}/configs/dockerfile-cpp-preprocessor`; + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v nodejs && command -v python3 && command -v curl && command -v wget && command -v vim && test -f /usr/local/bin/test.sh'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + } + }); + + it('should preprocess a Dockerfile.in during up cmake', async function (){ + if (!cmakeAvailable){ + this.skip(); + } + const testFolder = `${__dirname}/configs/dockerfile-cmake-preprocessor`; + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + // Check that the expected base image and port are set in the running container + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + } + }); + + it('should preprocess a Dockerfile.in during up cmake when no output folder is specified', async function () { + if (!cmakeAvailable){ + this.skip(); + } + const testFolder = `${__dirname}/configs/dockerfile-cmake2-preprocessor`; + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + // Check that the expected base image and port are set in the running container + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + } + }); + + it('should preprocess a Dockerfile.in during up autoconf', async function () { + if (!autoconfAvailable){ + this.skip(); + } + const testFolder = `${__dirname}/configs/dockerfile-autoconf-preprocessor`; + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + } + }); + + it('should preprocess a Dockerfile.in during up meson', async function () { + if (!mesonAvailable){ + this.skip(); + } + const testFolder = `${__dirname}/configs/dockerfile-meson-preprocessor`; + + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + } + }); +}); From e7b2478c0d51165b9913ceb6ec628a9fd34dc82d Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 20 May 2026 19:31:43 +0000 Subject: [PATCH 2/4] Refine Dockerfile preprocessor tool --- src/spec-configuration/configuration.ts | 12 +- src/spec-node/dockerfilePreprocessor.ts | 103 ++++++++----- .../.devcontainer-preprocessed/Dockerfile | 9 ++ .../.devcontainer.json | 10 +- .../build/CMakeCache.txt | 119 +++++++++++++++ .../build/CMakeFiles/3.25.1/CMakeSystem.cmake | 15 ++ .../CMakeDirectoryInformation.cmake | 16 ++ .../build/CMakeFiles/Makefile.cmake | 39 +++++ .../build/CMakeFiles/Makefile2 | 86 +++++++++++ .../build/CMakeFiles/TargetDirectories.txt | 2 + .../build/CMakeFiles/cmake.check_cache | 1 + .../build/CMakeFiles/progress.marks | 1 + .../build/Makefile | 140 ++++++++++++++++++ .../build/cmake_install.cmake | 49 ++++++ .../.devcontainer.json | 10 +- .../.devcontainer-preprocessed/Dockerfile | 8 + .../.devcontainer.json | 9 +- .../.devcontainer.json | 10 +- src/test/dockerfilePreprocessor.test.ts | 107 ++++++++++--- 19 files changed, 679 insertions(+), 67 deletions(-) create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/Makefile create mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake create mode 100644 src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile diff --git a/src/spec-configuration/configuration.ts b/src/spec-configuration/configuration.ts index 2f8e36255..e30b21b78 100644 --- a/src/spec-configuration/configuration.ts +++ b/src/spec-configuration/configuration.ts @@ -38,9 +38,17 @@ export interface DevContainerFeature { options: boolean | string | Record; } +// Dockerfile preprocessing always produces a CLI-owned final Dockerfile path. +// Users provide the preprocessor tool and optional arguments. +// For direct file transforms, users can select whether the tool behaves like a +// single-file transform or expects a build-tree style workspace argument. +// For workspace-style generators, users can instead set generatedDockerfile to +// tell the CLI which file to promote to the final Dockerfile after the tool runs. export interface DockerfilePreprocessor { - commands?: string[]; - output?: string; + tool?: string; + args?: string[]; + outputMode?: 'single-file' | 'build-tree'; + generatedDockerfile?: string; } export interface DevContainerFromImageConfig { diff --git a/src/spec-node/dockerfilePreprocessor.ts b/src/spec-node/dockerfilePreprocessor.ts index c6df7ad5f..73844e3a6 100644 --- a/src/spec-node/dockerfilePreprocessor.ts +++ b/src/spec-node/dockerfilePreprocessor.ts @@ -9,14 +9,15 @@ import { ContainerError, toErrorText } from '../spec-common/errors'; import { CLIHost, runCommandNoPty } from '../spec-common/commonUtils'; import { Log, LogLevel, makeLog } from '../spec-utils/log'; -export function getDockerfilePreprocessedPath(dockerfilePath: string, output?: string): string | undefined { +function dockerfilePreprocessorToolDocs(): string { + return "Set 'dockerfilePreprocessor.tool' and optional 'dockerfilePreprocessor.args' in devcontainer.json. Use 'outputMode' to choose whether the tool runs in 'single-file' mode or 'build-tree' mode. Use 'generatedDockerfile' for tools that write the final Dockerfile to a predictable workspace-relative path instead of the CLI-provided output argument."; +} + +export function getDockerfilePreprocessedPath(dockerfilePath: string): string | undefined { if (!dockerfilePath.toLowerCase().endsWith('.in')) { return undefined; } - if (output) { - return path.isAbsolute(output) ? output : path.join(path.dirname(dockerfilePath), output); - } - return dockerfilePath.slice(0, -3); + return path.join(path.dirname(dockerfilePath), '.devcontainer-preprocessed', 'Dockerfile'); } export async function preprocessDockerExtensionFile( @@ -24,62 +25,96 @@ export async function preprocessDockerExtensionFile( config: Pick, dockerfilePath: string ): Promise { - const outputDockerfilePath = getDockerfilePreprocessedPath(dockerfilePath, config.dockerfilePreprocessor?.output); - if (!outputDockerfilePath) { + const cliOutputPath = getDockerfilePreprocessedPath(dockerfilePath); + if (!cliOutputPath) { return dockerfilePath; } - const commands = (config.dockerfilePreprocessor?.commands || []).map(command => command.trim()).filter(command => command.length > 0); - if (!commands.length) { + const tool = config.dockerfilePreprocessor?.tool?.trim(); + const args = (config.dockerfilePreprocessor?.args || []).map(arg => arg.trim()).filter(arg => arg.length > 0); + const outputMode = config.dockerfilePreprocessor?.outputMode || 'build-tree'; + const generatedDockerfile = config.dockerfilePreprocessor?.generatedDockerfile?.trim(); + if (!tool) { throw new ContainerError({ - description: `Dockerfile preprocessor commands are required to build from '${dockerfilePath}'. Set 'dockerfilePreprocessor.commands' in devcontainer.json.`, + description: `A Dockerfile preprocessor tool is required to build from '${dockerfilePath}'. ${dockerfilePreprocessorToolDocs()}`, data: { fileWithError: dockerfilePath }, }); } const { cliHost, output } = params; const infoOutput = makeLog(output, LogLevel.Info); - const isWindows = cliHost.platform === 'win32'; - const shell = isWindows ? [cliHost.env.ComSpec || 'cmd.exe', '/c'] : ['/bin/sh', '-c']; + const cliOutputDir = path.dirname(cliOutputPath); + await cliHost.mkdirp(cliOutputDir); + const workdirPath = path.dirname(dockerfilePath); + const inputPath = dockerfilePath; + const outputPath = cliOutputPath; + const generatedOutputPath = generatedDockerfile ? path.resolve(workdirPath, generatedDockerfile) : outputPath; + // Strict contract: the CLI owns the final output path. Direct-transform + // tools can write to the CLI-provided output argument; workspace generators + // can instead declare a generated Dockerfile path for the CLI to promote. const env = { ...cliHost.env, - DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: dockerfilePath, - DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputDockerfilePath, - input_file: dockerfilePath, - output_file: outputDockerfilePath, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: inputPath, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputPath, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_WORKDIR: workdirPath, + DEVCONTAINER_DOCKERFILE_PREPROCESSOR_GENERATED_DOCKERFILE: generatedOutputPath, + input_file: inputPath, + output_file: outputPath, + generated_dockerfile: generatedOutputPath, + workdir: workdirPath, }; + const directOutputArgs = outputMode === 'single-file' + ? [inputPath, outputPath] + : [inputPath, outputPath, workdirPath]; + const invocationArgs = generatedDockerfile ? args : [...args, ...directOutputArgs]; try { - infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${outputDockerfilePath}'`); - for (const command of commands) { - await runCommandNoPty({ - exec: cliHost.exec, - cmd: shell[0], - args: [shell[1], command], - cwd: path.dirname(dockerfilePath), - env, - output: infoOutput, - print: 'continuous', - }); - } + infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${cliOutputPath}'`); + await runCommandNoPty({ + exec: cliHost.exec, + cmd: tool, + args: invocationArgs, + cwd: workdirPath, + env, + output: infoOutput, + print: 'continuous', + }); } catch (err) { + const originalError = err as { + message?: string; + stderr?: Buffer | string; + cmdOutput?: string; + code?: number; + signal?: string; + }; + const stderrText = typeof originalError?.stderr === 'string' ? originalError.stderr : originalError?.stderr?.toString(); throw new ContainerError({ - description: `Dockerfile preprocessing failed while running '${commands[commands.length - 1]}'.`, + description: `Dockerfile preprocessing failed while running '${tool}'. ${dockerfilePreprocessorToolDocs()}`, originalError: { - ...err, - message: `${err?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(err?.stderr || err?.cmdOutput || '')}`.trim(), + message: `${originalError?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(stderrText || originalError?.cmdOutput || '')}`.trim(), + code: originalError?.code, + signal: originalError?.signal, + stderr: originalError?.stderr, }, data: { fileWithError: dockerfilePath }, }); } - if (!await cliHost.isFile(outputDockerfilePath)) { + if (!await cliHost.isFile(generatedOutputPath)) { throw new ContainerError({ - description: `Dockerfile preprocessing did not produce '${outputDockerfilePath}'.`, + description: generatedDockerfile + ? `Dockerfile preprocessing did not produce '${generatedOutputPath}'. Ensure the configured tool writes the final Dockerfile to the configured generatedDockerfile path. ${dockerfilePreprocessorToolDocs()}` + : `Dockerfile preprocessing did not produce '${outputPath}'. Ensure the configured tool writes the final Dockerfile to the CLI-provided output argument. ${dockerfilePreprocessorToolDocs()}`, data: { fileWithError: dockerfilePath }, }); } - return outputDockerfilePath; + if (generatedOutputPath !== outputPath) { + await cliHost.rename(generatedOutputPath, outputPath); + } + + infoOutput.write(`Preprocessed Dockerfile written to '${cliOutputPath}'`); + + return cliOutputPath; } diff --git a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile new file mode 100644 index 000000000..b4bd17e51 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-bookworm + +ARG APP_PORT=3000 +EXPOSE 3000 + +WORKDIR /workspace +COPY . /workspace + +CMD ["npm", "start"] diff --git a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json index 95e81208d..f9d34a644 100644 --- a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json +++ b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json @@ -3,10 +3,14 @@ "dockerfile": "Dockerfile.in" }, "dockerfilePreprocessor": { - "commands": [ - "cmake -S . -B build" + "tool": "cmake", + "args": [ + "-S", + ".", + "-B", + "build" ], - "output": "build/Dockerfile" + "generatedDockerfile": "build/Dockerfile" }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt new file mode 100644 index 000000000..199626c6a --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt @@ -0,0 +1,119 @@ +# This is the CMakeCache file. +# For build in directory: /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build +# It was generated by CMake: /usr/bin/cmake +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Enable/Disable color output during build. +CMAKE_COLOR_MAKEFILE:BOOL=ON + +//Enable/Disable output of compile commands during generation. +CMAKE_EXPORT_COMPILE_COMMANDS:BOOL= + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/pkgRedirects + +//Install path prefix, prepended onto install directories. +CMAKE_INSTALL_PREFIX:PATH=/usr/local + +//Path to a program. +CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=GenerateDockerfile + +//If set, runtime paths are not added when installing shared libraries, +// but are added when building. +CMAKE_SKIP_INSTALL_RPATH:BOOL=NO + +//If set, runtime paths are not added when using shared libraries. +CMAKE_SKIP_RPATH:BOOL=NO + +//If this value is on, makefiles will be generated without the +// .SILENT directive, and all commands will be echoed to the console +// during the make. This is useful for debugging only. With Visual +// Studio IDE projects all commands are done without /nologo. +CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE + +//Value Computed by CMake +GenerateDockerfile_BINARY_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build + +//Value Computed by CMake +GenerateDockerfile_IS_TOP_LEVEL:STATIC=ON + +//Value Computed by CMake +GenerateDockerfile_SOURCE_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=25 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=1 +//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE +CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=/usr/bin/cmake +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest +//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS +CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1 +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Unix Makefiles +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor +//Install .so files without execute permission. +CMAKE_INSTALL_SO_NO_EXE:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MAKE_PROGRAM +CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=/usr/share/cmake-3.25 +//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH +CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_RPATH +CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 +//uname command +CMAKE_UNAME:INTERNAL=/usr/bin/uname +//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE +CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 +//linker supports push/pop state +_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=FALSE + diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake new file mode 100644 index 000000000..7186421e5 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Linux-6.8.0-1044-azure") +set(CMAKE_HOST_SYSTEM_NAME "Linux") +set(CMAKE_HOST_SYSTEM_VERSION "6.8.0-1044-azure") +set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") + + + +set(CMAKE_SYSTEM "Linux-6.8.0-1044-azure") +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_VERSION "6.8.0-1044-azure") +set(CMAKE_SYSTEM_PROCESSOR "x86_64") + +set(CMAKE_CROSSCOMPILING "FALSE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake new file mode 100644 index 000000000..af35207ae --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake @@ -0,0 +1,16 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.25 + +# Relative path conversion top directories. +set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor") +set(CMAKE_RELATIVE_PATH_TOP_BINARY "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build") + +# Force unix paths in dependencies. +set(CMAKE_FORCE_UNIX_PATHS 1) + + +# The C and CXX include file regular expressions for this directory. +set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$") +set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$") +set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN}) +set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN}) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake new file mode 100644 index 000000000..bf885a9b6 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake @@ -0,0 +1,39 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.25 + +# The generator used is: +set(CMAKE_DEPENDS_GENERATOR "Unix Makefiles") + +# The top level Makefile was generated from the following files: +set(CMAKE_MAKEFILE_DEPENDS + "CMakeCache.txt" + "/usr/share/cmake-3.25/Modules/CMakeDetermineSystem.cmake" + "/usr/share/cmake-3.25/Modules/CMakeGenericSystem.cmake" + "/usr/share/cmake-3.25/Modules/CMakeInitializeConfigs.cmake" + "/usr/share/cmake-3.25/Modules/CMakeSystem.cmake.in" + "/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInformation.cmake" + "/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInitialize.cmake" + "/usr/share/cmake-3.25/Modules/CMakeUnixFindMake.cmake" + "/usr/share/cmake-3.25/Modules/Platform/Linux.cmake" + "/usr/share/cmake-3.25/Modules/Platform/UnixPaths.cmake" + "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt" + "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in" + "CMakeFiles/3.25.1/CMakeSystem.cmake" + ) + +# The corresponding makefile is: +set(CMAKE_MAKEFILE_OUTPUTS + "Makefile" + "CMakeFiles/cmake.check_cache" + ) + +# Byproducts of CMake generate step: +set(CMAKE_MAKEFILE_PRODUCTS + "CMakeFiles/3.25.1/CMakeSystem.cmake" + "Dockerfile" + "CMakeFiles/CMakeDirectoryInformation.cmake" + ) + +# Dependency information for all targets: +set(CMAKE_DEPEND_INFO_FILES + ) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 new file mode 100644 index 000000000..5267d9951 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 @@ -0,0 +1,86 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.25 + +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target + +#============================================================================= +# Special targets provided by cmake. + +# Disable implicit rules so canonical targets will work. +.SUFFIXES: + +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /usr/bin/cmake + +# The command to remove a file. +RM = /usr/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build + +#============================================================================= +# Directory level rules for the build root directory + +# The main recursive "all" target. +all: +.PHONY : all + +# The main recursive "preinstall" target. +preinstall: +.PHONY : preinstall + +# The main recursive "clean" target. +clean: +.PHONY : clean + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt new file mode 100644 index 000000000..e2a49cee9 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt @@ -0,0 +1,2 @@ +/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/edit_cache.dir +/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/rebuild_cache.dir diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache new file mode 100644 index 000000000..3dccd7317 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks @@ -0,0 +1 @@ +0 diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile b/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile new file mode 100644 index 000000000..3dae2ff06 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile @@ -0,0 +1,140 @@ +# CMAKE generated file: DO NOT EDIT! +# Generated by "Unix Makefiles" Generator, CMake Version 3.25 + +# Default target executed when no arguments are given to make. +default_target: all +.PHONY : default_target + +# Allow only one "make -f Makefile2" at a time, but pass parallelism. +.NOTPARALLEL: + +#============================================================================= +# Special targets provided by cmake. + +# Disable implicit rules so canonical targets will work. +.SUFFIXES: + +# Disable VCS-based implicit rules. +% : %,v + +# Disable VCS-based implicit rules. +% : RCS/% + +# Disable VCS-based implicit rules. +% : RCS/%,v + +# Disable VCS-based implicit rules. +% : SCCS/s.% + +# Disable VCS-based implicit rules. +% : s.% + +.SUFFIXES: .hpux_make_needs_suffix_list + +# Command-line flag to silence nested $(MAKE). +$(VERBOSE)MAKESILENT = -s + +#Suppress display of executed commands. +$(VERBOSE).SILENT: + +# A target that is always out of date. +cmake_force: +.PHONY : cmake_force + +#============================================================================= +# Set environment variables for the build. + +# The shell in which to execute make rules. +SHELL = /bin/sh + +# The CMake executable. +CMAKE_COMMAND = /usr/bin/cmake + +# The command to remove a file. +RM = /usr/bin/cmake -E rm -f + +# Escaping for special characters. +EQUALS = = + +# The top-level source directory on which CMake was run. +CMAKE_SOURCE_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor + +# The top-level build directory on which CMake was run. +CMAKE_BINARY_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build + +#============================================================================= +# Targets provided globally by CMake. + +# Special rule for the target edit_cache +edit_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "No interactive CMake dialog available..." + /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available. +.PHONY : edit_cache + +# Special rule for the target edit_cache +edit_cache/fast: edit_cache +.PHONY : edit_cache/fast + +# Special rule for the target rebuild_cache +rebuild_cache: + @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Running CMake to regenerate build system..." + /usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) +.PHONY : rebuild_cache + +# Special rule for the target rebuild_cache +rebuild_cache/fast: rebuild_cache +.PHONY : rebuild_cache/fast + +# The main all target +all: cmake_check_build_system + $(CMAKE_COMMAND) -E cmake_progress_start /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build//CMakeFiles/progress.marks + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all + $(CMAKE_COMMAND) -E cmake_progress_start /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles 0 +.PHONY : all + +# The main clean target +clean: + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean +.PHONY : clean + +# The main clean target +clean/fast: clean +.PHONY : clean/fast + +# Prepare targets for installation. +preinstall: all + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall + +# Prepare targets for installation. +preinstall/fast: + $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall +.PHONY : preinstall/fast + +# clear depends +depend: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 +.PHONY : depend + +# Help Target +help: + @echo "The following are some of the valid targets for this Makefile:" + @echo "... all (the default if no target is provided)" + @echo "... clean" + @echo "... depend" + @echo "... edit_cache" + @echo "... rebuild_cache" +.PHONY : help + + + +#============================================================================= +# Special targets to cleanup operation of make. + +# Special rule to run CMake to check the build system integrity. +# No rule that depends on this can have commands that come from listfiles +# because they might be regenerated. +cmake_check_build_system: + $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 +.PHONY : cmake_check_build_system + diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake new file mode 100644 index 000000000..aacecb043 --- /dev/null +++ b/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake @@ -0,0 +1,49 @@ +# Install script for directory: /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor + +# Set the install prefix +if(NOT DEFINED CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX "/usr/local") +endif() +string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") + +# Set the install configuration name. +if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) + if(BUILD_TYPE) + string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" + CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}") + else() + set(CMAKE_INSTALL_CONFIG_NAME "") + endif() + message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"") +endif() + +# Set the component getting installed. +if(NOT CMAKE_INSTALL_COMPONENT) + if(COMPONENT) + message(STATUS "Install component: \"${COMPONENT}\"") + set(CMAKE_INSTALL_COMPONENT "${COMPONENT}") + else() + set(CMAKE_INSTALL_COMPONENT) + endif() +endif() + +# Install shared libraries without execute permission? +if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE) + set(CMAKE_INSTALL_SO_NO_EXE "1") +endif() + +# Is this installation the result of a crosscompile? +if(NOT DEFINED CMAKE_CROSSCOMPILING) + set(CMAKE_CROSSCOMPILING "FALSE") +endif() + +if(CMAKE_INSTALL_COMPONENT) + set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt") +else() + set(CMAKE_INSTALL_MANIFEST "install_manifest.txt") +endif() + +string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT + "${CMAKE_INSTALL_MANIFEST_FILES}") +file(WRITE "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/${CMAKE_INSTALL_MANIFEST}" + "${CMAKE_INSTALL_MANIFEST_CONTENT}") diff --git a/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json index 149f52fa6..0efa5ea3a 100644 --- a/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json +++ b/src/test/configs/dockerfile-cmake2-preprocessor/.devcontainer.json @@ -3,10 +3,14 @@ "dockerfile": "Dockerfile.in" }, "dockerfilePreprocessor": { - "commands": [ - "cmake -S . -B build" + "tool": "cmake", + "args": [ + "-S", + ".", + "-B", + "build" ], - "output": "" + "generatedDockerfile": "Dockerfile" }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { diff --git a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile new file mode 100644 index 000000000..5d6aca9ca --- /dev/null +++ b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:20.04 +RUN apt-get update && apt-get install -y nodejs +RUN apt-get update && apt-get install -y python3 +RUN apt-get update && apt-get install -y curl wget +ENV APP_ENV=development +ENV APP_DEBUG=true +RUN apt-get update && apt-get install -y vim +COPY ./test.sh /usr/local/bin/test.sh diff --git a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json index d93572a2e..29f6449e8 100644 --- a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json +++ b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer.json @@ -3,10 +3,11 @@ "dockerfile": "Dockerfile.in" }, "dockerfilePreprocessor": { - "commands": [ - "cpp -P \"$DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT\" > \"$DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT\"" - ], - "output": "Dockerfile" + "tool": "cpp", + "outputMode": "single-file", + "args": [ + "-P" + ] }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { diff --git a/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json index ce8cb677c..d40bedd61 100644 --- a/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json +++ b/src/test/configs/dockerfile-meson-preprocessor/.devcontainer.json @@ -3,14 +3,16 @@ "dockerfile": "Dockerfile.in" }, "dockerfilePreprocessor": { - "commands": [ - "meson setup build --reconfigure || meson setup build" + "tool": "meson", + "args": [ + "setup", + "build" ], - "output": "build/Dockerfile" + "generatedDockerfile": "build/Dockerfile" }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { "version": "latest" } } -} \ No newline at end of file +} diff --git a/src/test/dockerfilePreprocessor.test.ts b/src/test/dockerfilePreprocessor.test.ts index 0bf812f64..33d5e11d1 100644 --- a/src/test/dockerfilePreprocessor.test.ts +++ b/src/test/dockerfilePreprocessor.test.ts @@ -21,18 +21,18 @@ describe('dockerfilePreprocessor', function () { }); it('returns preprocessed path for .in Dockerfile', () => { - assert.strictEqual(getDockerfilePreprocessedPath('/tmp/Dockerfile.in'), '/tmp/Dockerfile'); + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/Dockerfile.in'), '/tmp/.devcontainer-preprocessed/Dockerfile'); }); - it('returns configured relative output path', () => { - assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile.in', 'generated/Dockerfile'), '/tmp/folder/generated/Dockerfile'); + it('returns fixed CLI-owned output path for .in Dockerfile', () => { + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile.in'), '/tmp/folder/.devcontainer-preprocessed/Dockerfile'); }); it('returns undefined for non-.in Dockerfile even when output is configured', () => { - assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile', 'generated/Dockerfile'), undefined); + assert.strictEqual(getDockerfilePreprocessedPath('/tmp/folder/Dockerfile'), undefined); }); - it('throws when dockerfilePreprocessor.commands is missing for .in Dockerfile', async () => { + it('throws when dockerfilePreprocessor.tool is missing for .in Dockerfile', async () => { const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); await assert.rejects( preprocessDockerExtensionFile( @@ -42,22 +42,22 @@ describe('dockerfilePreprocessor', function () { ), (err: unknown) => { assert.ok(err instanceof ContainerError); - assert.match((err as ContainerError).description, /dockerfilePreprocessor\.commands/i); + assert.match((err as ContainerError).description, /dockerfilePreprocessor\.tool/i); return true; } ); }); - it('runs commands and produces Dockerfile output', async function () { + it('runs tool and produces Dockerfile output at the CLI-owned path', async function () { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); - const outputPath = path.join(tmpDir, 'Dockerfile'); + const outputPath = path.join(tmpDir, '.devcontainer-preprocessed', 'Dockerfile'); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); const result = await preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { commands: ['cat "$input_file" > "$output_file"'] } }, + { dockerfilePreprocessor: { tool: 'cp', outputMode: 'single-file' } }, inputPath ); @@ -66,23 +66,50 @@ describe('dockerfilePreprocessor', function () { assert.strictEqual(outputContent, 'FROM alpine:3.20\n'); }); - it('runs ordered commands and writes configured output file', async function () { + it('passes the CLI-owned output path to the tool', async function () { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); - const outputPath = path.join(tmpDir, 'generated', 'Dockerfile'); - await fs.mkdir(path.dirname(outputPath), { recursive: true }); + const outputPath = path.join(tmpDir, '.devcontainer-preprocessed', 'Dockerfile'); + const scriptPath = path.join(tmpDir, 'write-output.sh'); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + await fs.writeFile(scriptPath, '#!/bin/sh\nset -eu\nprintf "FROM busybox\\n" > "$2"\n'); + await fs.chmod(scriptPath, 0o755); const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); const result = await preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { commands: ['cp "$input_file" "$output_file"'], output: 'generated/Dockerfile' } }, + { dockerfilePreprocessor: { tool: './write-output.sh', outputMode: 'build-tree' } }, inputPath ); assert.strictEqual(result, outputPath); const outputContent = (await fs.readFile(outputPath)).toString(); - assert.strictEqual(outputContent, 'FROM alpine:3.20\n'); + assert.strictEqual(outputContent, 'FROM busybox\n'); + }); + + it('promotes generatedDockerfile to the CLI-owned output path', async function () { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + const outputPath = path.join(tmpDir, '.devcontainer-preprocessed', 'Dockerfile'); + const generatedPath = path.join(tmpDir, 'build', 'Dockerfile'); + const scriptPath = path.join(tmpDir, 'write-generated.sh'); + await fs.mkdir(path.join(tmpDir, 'build'), { recursive: true }); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + await fs.writeFile(scriptPath, '#!/bin/sh\nset -eu\nmkdir -p "$3/build"\nprintf "FROM debian:bookworm\\n" > "$3/build/Dockerfile"\n'); + await fs.chmod(scriptPath, 0o755); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + const result = await preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { tool: './write-generated.sh', generatedDockerfile: 'build/Dockerfile' } }, + inputPath + ); + + assert.strictEqual(result, outputPath); + assert.strictEqual(await fs.stat(outputPath).then(() => true, () => false), true); + assert.strictEqual(await fs.stat(generatedPath).then(() => true, () => false), false); + const outputContent = (await fs.readFile(outputPath)).toString(); + assert.strictEqual(outputContent, 'FROM debian:bookworm\n'); }); it('throws when a preprocessor command fails', async () => { @@ -93,12 +120,12 @@ describe('dockerfilePreprocessor', function () { const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); await assert.rejects(preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { commands: ['this-command-should-not-exist-xyz123'] } }, + { dockerfilePreprocessor: { tool: 'this-command-should-not-exist-xyz123' } }, inputPath )); }); - it('throws when commands succeed but output Dockerfile is not generated', async () => { + it('throws when tool succeeds but output Dockerfile is not generated', async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); @@ -107,7 +134,7 @@ describe('dockerfilePreprocessor', function () { await assert.rejects( preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { commands: ['true'] } }, + { dockerfilePreprocessor: { tool: 'true' } }, inputPath ), (err: unknown) => { @@ -117,6 +144,26 @@ describe('dockerfilePreprocessor', function () { } ); }); + + it('throws when generatedDockerfile is configured but not produced', async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); + const inputPath = path.join(tmpDir, 'Dockerfile.in'); + await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + + const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); + await assert.rejects( + preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { tool: 'true', generatedDockerfile: 'build/Dockerfile' } }, + inputPath + ), + (err: unknown) => { + assert.ok(err instanceof ContainerError); + assert.match((err as ContainerError).description, /generatedDockerfile/i); + return true; + } + ); + }); }); (process.platform === 'linux' ? describe : describe.skip)('dockerfilePreprocessor integration', function () { @@ -124,11 +171,27 @@ describe('dockerfilePreprocessor', function () { const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp')); const cli = `npx --prefix ${tmp} devcontainer`; + const cleanupByFixture = new Map([ + ['dockerfile-cpp-preprocessor', ['Dockerfile', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ['dockerfile-cmake-preprocessor', ['Dockerfile', 'build', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ['dockerfile-cmake2-preprocessor', ['Dockerfile', 'build', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ['dockerfile-autoconf-preprocessor', ['Dockerfile', 'configure', 'config.log', 'config.status', 'autom4te.cache', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ['dockerfile-meson-preprocessor', ['Dockerfile', 'build', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ]); let cppAvailable = false; let cmakeAvailable = false; let mesonAvailable = false; let autoconfAvailable = false; + const cleanupGeneratedArtifacts = async (testFolder: string) => { + const fixture = path.basename(testFolder); + const generated = cleanupByFixture.get(fixture); + if (!generated?.length) { + return; + } + await Promise.all(generated.map(relative => fs.rm(path.join(testFolder, relative), { recursive: true, force: true }))); + }; + before('Install', async () => { await shellExec(`rm -rf ${tmp}/node_modules`); await shellExec(`mkdir -p ${tmp}`); @@ -148,12 +211,14 @@ describe('dockerfilePreprocessor', function () { this.skip(); } const testFolder = `${__dirname}/configs/dockerfile-cpp-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); let containerId: string | undefined; try { containerId = (await devContainerUp(cli, testFolder)).containerId; await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v nodejs && command -v python3 && command -v curl && command -v wget && command -v vim && test -f /usr/local/bin/test.sh'`); } finally { await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); } }); @@ -162,6 +227,7 @@ describe('dockerfilePreprocessor', function () { this.skip(); } const testFolder = `${__dirname}/configs/dockerfile-cmake-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); let containerId: string | undefined; try { containerId = (await devContainerUp(cli, testFolder)).containerId; @@ -169,6 +235,7 @@ describe('dockerfilePreprocessor', function () { await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); } finally { await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); } }); @@ -177,6 +244,7 @@ describe('dockerfilePreprocessor', function () { this.skip(); } const testFolder = `${__dirname}/configs/dockerfile-cmake2-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); let containerId: string | undefined; try { containerId = (await devContainerUp(cli, testFolder)).containerId; @@ -184,6 +252,7 @@ describe('dockerfilePreprocessor', function () { await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); } finally { await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); } }); @@ -192,12 +261,14 @@ describe('dockerfilePreprocessor', function () { this.skip(); } const testFolder = `${__dirname}/configs/dockerfile-autoconf-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); let containerId: string | undefined; try { containerId = (await devContainerUp(cli, testFolder)).containerId; await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); } finally { await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); } }); @@ -206,6 +277,7 @@ describe('dockerfilePreprocessor', function () { this.skip(); } const testFolder = `${__dirname}/configs/dockerfile-meson-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); let containerId: string | undefined; try { @@ -213,6 +285,7 @@ describe('dockerfilePreprocessor', function () { await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v node && command -v npm'`); } finally { await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); } }); }); From c4245b68019900ee43d15ebbd7ca2eacc27d4d52 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 20 May 2026 20:05:08 +0000 Subject: [PATCH 3/4] add test cases --- src/spec-node/dockerfilePreprocessor.ts | 17 +++ .../.devcontainer/devcontainer.json | 18 +++ .../.devcontainer/docker-compose.yml | 10 ++ .../Dockerfile.in | 16 ++ .../common.Dockerfile | 1 + .../test.sh | 2 + .../tools.Dockerfile | 2 + .../.devcontainer.json | 9 +- .../.devcontainer-preprocessed/Dockerfile | 9 -- .../build/CMakeCache.txt | 119 --------------- .../build/CMakeFiles/3.25.1/CMakeSystem.cmake | 15 -- .../CMakeDirectoryInformation.cmake | 16 -- .../build/CMakeFiles/Makefile.cmake | 39 ----- .../build/CMakeFiles/Makefile2 | 86 ----------- .../build/CMakeFiles/TargetDirectories.txt | 2 - .../build/CMakeFiles/cmake.check_cache | 1 - .../build/CMakeFiles/progress.marks | 1 - .../build/Makefile | 140 ------------------ .../build/cmake_install.cmake | 49 ------ .../.devcontainer-preprocessed/Dockerfile | 8 - src/test/dockerfilePreprocessor.test.ts | 63 +++++--- 21 files changed, 110 insertions(+), 513 deletions(-) create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/devcontainer.json create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/docker-compose.yml create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/Dockerfile.in create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/common.Dockerfile create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/test.sh create mode 100644 src/test/configs/dockercomposefile-cpp-preprocessor/tools.Dockerfile delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/Makefile delete mode 100644 src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake delete mode 100644 src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile diff --git a/src/spec-node/dockerfilePreprocessor.ts b/src/spec-node/dockerfilePreprocessor.ts index 73844e3a6..27ce889d0 100644 --- a/src/spec-node/dockerfilePreprocessor.ts +++ b/src/spec-node/dockerfilePreprocessor.ts @@ -49,6 +49,23 @@ export async function preprocessDockerExtensionFile( const inputPath = dockerfilePath; const outputPath = cliOutputPath; const generatedOutputPath = generatedDockerfile ? path.resolve(workdirPath, generatedDockerfile) : outputPath; + const staleOutputPaths = generatedOutputPath === outputPath ? [outputPath] : [outputPath, generatedOutputPath]; + for (const stalePath of staleOutputPaths) { + if (!await cliHost.isFile(stalePath)) { + continue; + } + const cleanupCommand = cliHost.platform === 'win32' + ? { cmd: 'cmd', args: ['/d', '/s', '/c', `del /f /q "${stalePath}"`] } + : { cmd: 'rm', args: ['-f', stalePath] }; + await runCommandNoPty({ + exec: cliHost.exec, + cmd: cleanupCommand.cmd, + args: cleanupCommand.args, + cwd: workdirPath, + env: cliHost.env, + output: infoOutput, + }); + } // Strict contract: the CLI owns the final output path. Direct-transform // tools can write to the CLI-provided output argument; workspace generators diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/devcontainer.json b/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/devcontainer.json new file mode 100644 index 000000000..870ca8ab8 --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/devcontainer.json @@ -0,0 +1,18 @@ +{ + "name": "Docker Compose Cpp Preprocessor", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "dockerfilePreprocessor": { + "tool": "cpp", + "outputMode": "single-file", + "args": [ + "-P" + ] + }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/docker-compose.yml b/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..7b2afb80b --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.8' + +services: + app: + build: + context: .. + dockerfile: Dockerfile.in + command: sleep infinity + volumes: + - ..:/workspace:cached diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/Dockerfile.in b/src/test/configs/dockercomposefile-cpp-preprocessor/Dockerfile.in new file mode 100644 index 000000000..52611ea7e --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/Dockerfile.in @@ -0,0 +1,16 @@ +#define BASE_IMAGE ubuntu:20.04 +#define INSTALL_NODE +#define INSTALL_PYTHON + +FROM BASE_IMAGE + +#ifdef INSTALL_NODE +RUN apt-get update && apt-get install -y nodejs +#endif + +#ifdef INSTALL_PYTHON +RUN apt-get update && apt-get install -y python3 +#endif + +#include "common.Dockerfile" +#include "tools.Dockerfile" diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/common.Dockerfile b/src/test/configs/dockercomposefile-cpp-preprocessor/common.Dockerfile new file mode 100644 index 000000000..67d4c72e0 --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/common.Dockerfile @@ -0,0 +1 @@ +RUN apt-get update && apt-get install -y curl wget diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/test.sh b/src/test/configs/dockercomposefile-cpp-preprocessor/test.sh new file mode 100644 index 000000000..e1a69b7af --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/test.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo hello diff --git a/src/test/configs/dockercomposefile-cpp-preprocessor/tools.Dockerfile b/src/test/configs/dockercomposefile-cpp-preprocessor/tools.Dockerfile new file mode 100644 index 000000000..8baf4e002 --- /dev/null +++ b/src/test/configs/dockercomposefile-cpp-preprocessor/tools.Dockerfile @@ -0,0 +1,2 @@ +RUN apt-get update && apt-get install -y vim +COPY ./test.sh /usr/local/bin/test.sh diff --git a/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json b/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json index 13764eed5..d2de81b69 100644 --- a/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json +++ b/src/test/configs/dockerfile-autoconf-preprocessor/.devcontainer.json @@ -3,11 +3,12 @@ "dockerfile": "Dockerfile.in" }, "dockerfilePreprocessor": { - "commands": [ - "autoconf", - "./configure" + "tool": "sh", + "args": [ + "-c", + "autoconf && ./configure" ], - "output": "Dockerfile" + "generatedDockerfile": "Dockerfile" }, "features": { "ghcr.io/devcontainers/features/github-cli:1": { diff --git a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile b/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile deleted file mode 100644 index b4bd17e51..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/.devcontainer-preprocessed/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM node:22-bookworm - -ARG APP_PORT=3000 -EXPOSE 3000 - -WORKDIR /workspace -COPY . /workspace - -CMD ["npm", "start"] diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt deleted file mode 100644 index 199626c6a..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeCache.txt +++ /dev/null @@ -1,119 +0,0 @@ -# This is the CMakeCache file. -# For build in directory: /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build -# It was generated by CMake: /usr/bin/cmake -# You can edit this file to change values found and used by cmake. -# If you do not want to change any of the values, simply exit the editor. -# If you do want to change a value, simply edit, save, and exit the editor. -# The syntax for the file is as follows: -# KEY:TYPE=VALUE -# KEY is the name of a variable in the cache. -# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. -# VALUE is the current value for the KEY. - -######################## -# EXTERNAL cache entries -######################## - -//Enable/Disable color output during build. -CMAKE_COLOR_MAKEFILE:BOOL=ON - -//Enable/Disable output of compile commands during generation. -CMAKE_EXPORT_COMPILE_COMMANDS:BOOL= - -//Value Computed by CMake. -CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/pkgRedirects - -//Install path prefix, prepended onto install directories. -CMAKE_INSTALL_PREFIX:PATH=/usr/local - -//Path to a program. -CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake - -//Value Computed by CMake -CMAKE_PROJECT_DESCRIPTION:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_HOMEPAGE_URL:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_NAME:STATIC=GenerateDockerfile - -//If set, runtime paths are not added when installing shared libraries, -// but are added when building. -CMAKE_SKIP_INSTALL_RPATH:BOOL=NO - -//If set, runtime paths are not added when using shared libraries. -CMAKE_SKIP_RPATH:BOOL=NO - -//If this value is on, makefiles will be generated without the -// .SILENT directive, and all commands will be echoed to the console -// during the make. This is useful for debugging only. With Visual -// Studio IDE projects all commands are done without /nologo. -CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE - -//Value Computed by CMake -GenerateDockerfile_BINARY_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build - -//Value Computed by CMake -GenerateDockerfile_IS_TOP_LEVEL:STATIC=ON - -//Value Computed by CMake -GenerateDockerfile_SOURCE_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor - - -######################## -# INTERNAL cache entries -######################## - -//This is the directory where this CMakeCache.txt was created -CMAKE_CACHEFILE_DIR:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build -//Major version of cmake used to create the current loaded cache -CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 -//Minor version of cmake used to create the current loaded cache -CMAKE_CACHE_MINOR_VERSION:INTERNAL=25 -//Patch version of cmake used to create the current loaded cache -CMAKE_CACHE_PATCH_VERSION:INTERNAL=1 -//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE -CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 -//Path to CMake executable. -CMAKE_COMMAND:INTERNAL=/usr/bin/cmake -//Path to cpack program executable. -CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack -//Path to ctest program executable. -CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest -//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS -CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1 -//Name of external makefile project generator. -CMAKE_EXTRA_GENERATOR:INTERNAL= -//Name of generator. -CMAKE_GENERATOR:INTERNAL=Unix Makefiles -//Generator instance identifier. -CMAKE_GENERATOR_INSTANCE:INTERNAL= -//Name of generator platform. -CMAKE_GENERATOR_PLATFORM:INTERNAL= -//Name of generator toolset. -CMAKE_GENERATOR_TOOLSET:INTERNAL= -//Source directory with the top level CMakeLists.txt file for this -// project -CMAKE_HOME_DIRECTORY:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor -//Install .so files without execute permission. -CMAKE_INSTALL_SO_NO_EXE:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MAKE_PROGRAM -CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 -//number of local generators -CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 -//Platform information initialized -CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 -//Path to CMake installation. -CMAKE_ROOT:INTERNAL=/usr/share/cmake-3.25 -//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH -CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SKIP_RPATH -CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 -//uname command -CMAKE_UNAME:INTERNAL=/usr/bin/uname -//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE -CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 -//linker supports push/pop state -_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=FALSE - diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake deleted file mode 100644 index 7186421e5..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/3.25.1/CMakeSystem.cmake +++ /dev/null @@ -1,15 +0,0 @@ -set(CMAKE_HOST_SYSTEM "Linux-6.8.0-1044-azure") -set(CMAKE_HOST_SYSTEM_NAME "Linux") -set(CMAKE_HOST_SYSTEM_VERSION "6.8.0-1044-azure") -set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") - - - -set(CMAKE_SYSTEM "Linux-6.8.0-1044-azure") -set(CMAKE_SYSTEM_NAME "Linux") -set(CMAKE_SYSTEM_VERSION "6.8.0-1044-azure") -set(CMAKE_SYSTEM_PROCESSOR "x86_64") - -set(CMAKE_CROSSCOMPILING "FALSE") - -set(CMAKE_SYSTEM_LOADED 1) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake deleted file mode 100644 index af35207ae..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/CMakeDirectoryInformation.cmake +++ /dev/null @@ -1,16 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.25 - -# Relative path conversion top directories. -set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor") -set(CMAKE_RELATIVE_PATH_TOP_BINARY "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build") - -# Force unix paths in dependencies. -set(CMAKE_FORCE_UNIX_PATHS 1) - - -# The C and CXX include file regular expressions for this directory. -set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$") -set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$") -set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN}) -set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN}) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake deleted file mode 100644 index bf885a9b6..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile.cmake +++ /dev/null @@ -1,39 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.25 - -# The generator used is: -set(CMAKE_DEPENDS_GENERATOR "Unix Makefiles") - -# The top level Makefile was generated from the following files: -set(CMAKE_MAKEFILE_DEPENDS - "CMakeCache.txt" - "/usr/share/cmake-3.25/Modules/CMakeDetermineSystem.cmake" - "/usr/share/cmake-3.25/Modules/CMakeGenericSystem.cmake" - "/usr/share/cmake-3.25/Modules/CMakeInitializeConfigs.cmake" - "/usr/share/cmake-3.25/Modules/CMakeSystem.cmake.in" - "/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInformation.cmake" - "/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInitialize.cmake" - "/usr/share/cmake-3.25/Modules/CMakeUnixFindMake.cmake" - "/usr/share/cmake-3.25/Modules/Platform/Linux.cmake" - "/usr/share/cmake-3.25/Modules/Platform/UnixPaths.cmake" - "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt" - "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in" - "CMakeFiles/3.25.1/CMakeSystem.cmake" - ) - -# The corresponding makefile is: -set(CMAKE_MAKEFILE_OUTPUTS - "Makefile" - "CMakeFiles/cmake.check_cache" - ) - -# Byproducts of CMake generate step: -set(CMAKE_MAKEFILE_PRODUCTS - "CMakeFiles/3.25.1/CMakeSystem.cmake" - "Dockerfile" - "CMakeFiles/CMakeDirectoryInformation.cmake" - ) - -# Dependency information for all targets: -set(CMAKE_DEPEND_INFO_FILES - ) diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 deleted file mode 100644 index 5267d9951..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/Makefile2 +++ /dev/null @@ -1,86 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.25 - -# Default target executed when no arguments are given to make. -default_target: all -.PHONY : default_target - -#============================================================================= -# Special targets provided by cmake. - -# Disable implicit rules so canonical targets will work. -.SUFFIXES: - -# Disable VCS-based implicit rules. -% : %,v - -# Disable VCS-based implicit rules. -% : RCS/% - -# Disable VCS-based implicit rules. -% : RCS/%,v - -# Disable VCS-based implicit rules. -% : SCCS/s.% - -# Disable VCS-based implicit rules. -% : s.% - -.SUFFIXES: .hpux_make_needs_suffix_list - -# Command-line flag to silence nested $(MAKE). -$(VERBOSE)MAKESILENT = -s - -#Suppress display of executed commands. -$(VERBOSE).SILENT: - -# A target that is always out of date. -cmake_force: -.PHONY : cmake_force - -#============================================================================= -# Set environment variables for the build. - -# The shell in which to execute make rules. -SHELL = /bin/sh - -# The CMake executable. -CMAKE_COMMAND = /usr/bin/cmake - -# The command to remove a file. -RM = /usr/bin/cmake -E rm -f - -# Escaping for special characters. -EQUALS = = - -# The top-level source directory on which CMake was run. -CMAKE_SOURCE_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor - -# The top-level build directory on which CMake was run. -CMAKE_BINARY_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build - -#============================================================================= -# Directory level rules for the build root directory - -# The main recursive "all" target. -all: -.PHONY : all - -# The main recursive "preinstall" target. -preinstall: -.PHONY : preinstall - -# The main recursive "clean" target. -clean: -.PHONY : clean - -#============================================================================= -# Special targets to cleanup operation of make. - -# Special rule to run CMake to check the build system integrity. -# No rule that depends on this can have commands that come from listfiles -# because they might be regenerated. -cmake_check_build_system: - $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 -.PHONY : cmake_check_build_system - diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt deleted file mode 100644 index e2a49cee9..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/TargetDirectories.txt +++ /dev/null @@ -1,2 +0,0 @@ -/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/edit_cache.dir -/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/rebuild_cache.dir diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache deleted file mode 100644 index 3dccd7317..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/cmake.check_cache +++ /dev/null @@ -1 +0,0 @@ -# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks b/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks deleted file mode 100644 index 573541ac9..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/progress.marks +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile b/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile deleted file mode 100644 index 3dae2ff06..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/Makefile +++ /dev/null @@ -1,140 +0,0 @@ -# CMAKE generated file: DO NOT EDIT! -# Generated by "Unix Makefiles" Generator, CMake Version 3.25 - -# Default target executed when no arguments are given to make. -default_target: all -.PHONY : default_target - -# Allow only one "make -f Makefile2" at a time, but pass parallelism. -.NOTPARALLEL: - -#============================================================================= -# Special targets provided by cmake. - -# Disable implicit rules so canonical targets will work. -.SUFFIXES: - -# Disable VCS-based implicit rules. -% : %,v - -# Disable VCS-based implicit rules. -% : RCS/% - -# Disable VCS-based implicit rules. -% : RCS/%,v - -# Disable VCS-based implicit rules. -% : SCCS/s.% - -# Disable VCS-based implicit rules. -% : s.% - -.SUFFIXES: .hpux_make_needs_suffix_list - -# Command-line flag to silence nested $(MAKE). -$(VERBOSE)MAKESILENT = -s - -#Suppress display of executed commands. -$(VERBOSE).SILENT: - -# A target that is always out of date. -cmake_force: -.PHONY : cmake_force - -#============================================================================= -# Set environment variables for the build. - -# The shell in which to execute make rules. -SHELL = /bin/sh - -# The CMake executable. -CMAKE_COMMAND = /usr/bin/cmake - -# The command to remove a file. -RM = /usr/bin/cmake -E rm -f - -# Escaping for special characters. -EQUALS = = - -# The top-level source directory on which CMake was run. -CMAKE_SOURCE_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor - -# The top-level build directory on which CMake was run. -CMAKE_BINARY_DIR = /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build - -#============================================================================= -# Targets provided globally by CMake. - -# Special rule for the target edit_cache -edit_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "No interactive CMake dialog available..." - /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available. -.PHONY : edit_cache - -# Special rule for the target edit_cache -edit_cache/fast: edit_cache -.PHONY : edit_cache/fast - -# Special rule for the target rebuild_cache -rebuild_cache: - @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Running CMake to regenerate build system..." - /usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) -.PHONY : rebuild_cache - -# Special rule for the target rebuild_cache -rebuild_cache/fast: rebuild_cache -.PHONY : rebuild_cache/fast - -# The main all target -all: cmake_check_build_system - $(CMAKE_COMMAND) -E cmake_progress_start /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build//CMakeFiles/progress.marks - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 all - $(CMAKE_COMMAND) -E cmake_progress_start /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles 0 -.PHONY : all - -# The main clean target -clean: - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 clean -.PHONY : clean - -# The main clean target -clean/fast: clean -.PHONY : clean/fast - -# Prepare targets for installation. -preinstall: all - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall -.PHONY : preinstall - -# Prepare targets for installation. -preinstall/fast: - $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 preinstall -.PHONY : preinstall/fast - -# clear depends -depend: - $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1 -.PHONY : depend - -# Help Target -help: - @echo "The following are some of the valid targets for this Makefile:" - @echo "... all (the default if no target is provided)" - @echo "... clean" - @echo "... depend" - @echo "... edit_cache" - @echo "... rebuild_cache" -.PHONY : help - - - -#============================================================================= -# Special targets to cleanup operation of make. - -# Special rule to run CMake to check the build system integrity. -# No rule that depends on this can have commands that come from listfiles -# because they might be regenerated. -cmake_check_build_system: - $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0 -.PHONY : cmake_check_build_system - diff --git a/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake b/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake deleted file mode 100644 index aacecb043..000000000 --- a/src/test/configs/dockerfile-cmake-preprocessor/build/cmake_install.cmake +++ /dev/null @@ -1,49 +0,0 @@ -# Install script for directory: /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor - -# Set the install prefix -if(NOT DEFINED CMAKE_INSTALL_PREFIX) - set(CMAKE_INSTALL_PREFIX "/usr/local") -endif() -string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") - -# Set the install configuration name. -if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) - if(BUILD_TYPE) - string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" - CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}") - else() - set(CMAKE_INSTALL_CONFIG_NAME "") - endif() - message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"") -endif() - -# Set the component getting installed. -if(NOT CMAKE_INSTALL_COMPONENT) - if(COMPONENT) - message(STATUS "Install component: \"${COMPONENT}\"") - set(CMAKE_INSTALL_COMPONENT "${COMPONENT}") - else() - set(CMAKE_INSTALL_COMPONENT) - endif() -endif() - -# Install shared libraries without execute permission? -if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE) - set(CMAKE_INSTALL_SO_NO_EXE "1") -endif() - -# Is this installation the result of a crosscompile? -if(NOT DEFINED CMAKE_CROSSCOMPILING) - set(CMAKE_CROSSCOMPILING "FALSE") -endif() - -if(CMAKE_INSTALL_COMPONENT) - set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt") -else() - set(CMAKE_INSTALL_MANIFEST "install_manifest.txt") -endif() - -string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT - "${CMAKE_INSTALL_MANIFEST_FILES}") -file(WRITE "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/${CMAKE_INSTALL_MANIFEST}" - "${CMAKE_INSTALL_MANIFEST_CONTENT}") diff --git a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile b/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile deleted file mode 100644 index 5d6aca9ca..000000000 --- a/src/test/configs/dockerfile-cpp-preprocessor/.devcontainer-preprocessed/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:20.04 -RUN apt-get update && apt-get install -y nodejs -RUN apt-get update && apt-get install -y python3 -RUN apt-get update && apt-get install -y curl wget -ENV APP_ENV=development -ENV APP_DEBUG=true -RUN apt-get update && apt-get install -y vim -COPY ./test.sh /usr/local/bin/test.sh diff --git a/src/test/dockerfilePreprocessor.test.ts b/src/test/dockerfilePreprocessor.test.ts index 33d5e11d1..c4542a635 100644 --- a/src/test/dockerfilePreprocessor.test.ts +++ b/src/test/dockerfilePreprocessor.test.ts @@ -87,54 +87,52 @@ describe('dockerfilePreprocessor', function () { assert.strictEqual(outputContent, 'FROM busybox\n'); }); - it('promotes generatedDockerfile to the CLI-owned output path', async function () { + it('throws when a preprocessor command fails', async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); - const outputPath = path.join(tmpDir, '.devcontainer-preprocessed', 'Dockerfile'); - const generatedPath = path.join(tmpDir, 'build', 'Dockerfile'); - const scriptPath = path.join(tmpDir, 'write-generated.sh'); - await fs.mkdir(path.join(tmpDir, 'build'), { recursive: true }); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); - await fs.writeFile(scriptPath, '#!/bin/sh\nset -eu\nmkdir -p "$3/build"\nprintf "FROM debian:bookworm\\n" > "$3/build/Dockerfile"\n'); - await fs.chmod(scriptPath, 0o755); const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); - const result = await preprocessDockerExtensionFile( + await assert.rejects(preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { tool: './write-generated.sh', generatedDockerfile: 'build/Dockerfile' } }, + { dockerfilePreprocessor: { tool: 'this-command-should-not-exist-xyz123' } }, inputPath - ); - - assert.strictEqual(result, outputPath); - assert.strictEqual(await fs.stat(outputPath).then(() => true, () => false), true); - assert.strictEqual(await fs.stat(generatedPath).then(() => true, () => false), false); - const outputContent = (await fs.readFile(outputPath)).toString(); - assert.strictEqual(outputContent, 'FROM debian:bookworm\n'); + )); }); - it('throws when a preprocessor command fails', async () => { + it('throws when tool succeeds but output Dockerfile is not generated', async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); - await assert.rejects(preprocessDockerExtensionFile( - { cliHost, output: nullLog }, - { dockerfilePreprocessor: { tool: 'this-command-should-not-exist-xyz123' } }, - inputPath - )); + await assert.rejects( + preprocessDockerExtensionFile( + { cliHost, output: nullLog }, + { dockerfilePreprocessor: { tool: 'true' } }, + inputPath + ), + (err: unknown) => { + assert.ok(err instanceof ContainerError); + assert.match((err as ContainerError).description, /did not produce/i); + return true; + } + ); }); - it('throws when tool succeeds but output Dockerfile is not generated', async () => { + it('does not treat stale CLI output as generated output', async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'devcontainer-preprocess-')); const inputPath = path.join(tmpDir, 'Dockerfile.in'); + const outputPath = path.join(tmpDir, '.devcontainer-preprocessed', 'Dockerfile'); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(inputPath, 'FROM alpine:3.20\n'); + await fs.writeFile(outputPath, 'FROM stale:old\n'); const cliHost = await getCLIHost(process.cwd(), loadNativeModule, true); await assert.rejects( preprocessDockerExtensionFile( { cliHost, output: nullLog }, - { dockerfilePreprocessor: { tool: 'true' } }, + { dockerfilePreprocessor: { tool: 'true', outputMode: 'single-file' } }, inputPath ), (err: unknown) => { @@ -173,6 +171,7 @@ describe('dockerfilePreprocessor', function () { const cli = `npx --prefix ${tmp} devcontainer`; const cleanupByFixture = new Map([ ['dockerfile-cpp-preprocessor', ['Dockerfile', '.devcontainer-lock.json', '.devcontainer-preprocessed']], + ['dockercomposefile-cpp-preprocessor', ['Dockerfile', '.devcontainer-lock.json', '.devcontainer-preprocessed']], ['dockerfile-cmake-preprocessor', ['Dockerfile', 'build', '.devcontainer-lock.json', '.devcontainer-preprocessed']], ['dockerfile-cmake2-preprocessor', ['Dockerfile', 'build', '.devcontainer-lock.json', '.devcontainer-preprocessed']], ['dockerfile-autoconf-preprocessor', ['Dockerfile', 'configure', 'config.log', 'config.status', 'autom4te.cache', '.devcontainer-lock.json', '.devcontainer-preprocessed']], @@ -222,6 +221,22 @@ describe('dockerfilePreprocessor', function () { } }); + it('should preprocess a Dockerfile.in during up docker compose cpp', async function () { + if (!cppAvailable) { + this.skip(); + } + const testFolder = `${__dirname}/configs/dockercomposefile-cpp-preprocessor`; + await cleanupGeneratedArtifacts(testFolder); + let containerId: string | undefined; + try { + containerId = (await devContainerUp(cli, testFolder)).containerId; + await shellExec(`${cli} exec --workspace-folder ${testFolder} sh -lc 'command -v nodejs && command -v python3 && command -v curl && command -v wget && command -v vim && test -f /usr/local/bin/test.sh'`); + } finally { + await devContainerDown({ containerId, doNotThrow: true }); + await cleanupGeneratedArtifacts(testFolder); + } + }); + it('should preprocess a Dockerfile.in during up cmake', async function (){ if (!cmakeAvailable){ this.skip(); From 907c19eb8b8ce849785c7d517ee94379ff8308f5 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 20 May 2026 20:19:59 +0000 Subject: [PATCH 4/4] update --- src/spec-common/cliHost.ts | 6 +++++- src/spec-node/dockerfilePreprocessor.ts | 18 +++++------------- src/test/workspaceConfiguration.test.ts | 2 ++ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/spec-common/cliHost.ts b/src/spec-common/cliHost.ts index 294f8be4a..45fb10819 100644 --- a/src/spec-common/cliHost.ts +++ b/src/spec-common/cliHost.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import * as net from 'net'; import * as os from 'os'; -import { readLocalFile, writeLocalFile, mkdirpLocal, isLocalFile, renameLocal, readLocalDir, isLocalFolder } from '../spec-utils/pfs'; +import { readLocalFile, writeLocalFile, mkdirpLocal, isLocalFile, renameLocal, readLocalDir, isLocalFolder, rmLocal, cpLocal } from '../spec-utils/pfs'; import { URI } from 'vscode-uri'; import { ExecFunction, getLocalUsername, plainExec, plainPtyExec, PtyExecFunction } from './commonUtils'; import { Abort, Duplex, Sink, Source, SourceCallback } from 'pull-stream'; @@ -32,7 +32,9 @@ export interface CLIHost { isFolder(filepath: string): Promise; readFile(filepath: string): Promise; writeFile(filepath: string, content: Buffer): Promise; + copyFile(oldPath: string, newPath: string): Promise; rename(oldPath: string, newPath: string): Promise; + remove(filepath: string): Promise; mkdirp(dirpath: string): Promise; readDir(dirpath: string): Promise; readDirWithTypes?(dirpath: string): Promise<[string, FileTypeBitmask][]>; @@ -76,7 +78,9 @@ function createLocalCLIHostFromExecFunctions(localCwd: string, exec: ExecFunctio isFolder: isLocalFolder, readFile: readLocalFile, writeFile: writeLocalFile, + copyFile: cpLocal, rename: renameLocal, + remove: async (filepath) => rmLocal(filepath, { force: true }), mkdirp: async (dirpath) => { await mkdirpLocal(dirpath); }, diff --git a/src/spec-node/dockerfilePreprocessor.ts b/src/spec-node/dockerfilePreprocessor.ts index 27ce889d0..a51e59f10 100644 --- a/src/spec-node/dockerfilePreprocessor.ts +++ b/src/spec-node/dockerfilePreprocessor.ts @@ -6,7 +6,8 @@ import * as path from 'path'; import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig } from '../spec-configuration/configuration'; import { ContainerError, toErrorText } from '../spec-common/errors'; -import { CLIHost, runCommandNoPty } from '../spec-common/commonUtils'; +import { CLIHost } from '../spec-common/cliHost'; +import { runCommandNoPty } from '../spec-common/commonUtils'; import { Log, LogLevel, makeLog } from '../spec-utils/log'; function dockerfilePreprocessorToolDocs(): string { @@ -54,17 +55,7 @@ export async function preprocessDockerExtensionFile( if (!await cliHost.isFile(stalePath)) { continue; } - const cleanupCommand = cliHost.platform === 'win32' - ? { cmd: 'cmd', args: ['/d', '/s', '/c', `del /f /q "${stalePath}"`] } - : { cmd: 'rm', args: ['-f', stalePath] }; - await runCommandNoPty({ - exec: cliHost.exec, - cmd: cleanupCommand.cmd, - args: cleanupCommand.args, - cwd: workdirPath, - env: cliHost.env, - output: infoOutput, - }); + await cliHost.remove(stalePath); } // Strict contract: the CLI owns the final output path. Direct-transform @@ -128,7 +119,8 @@ export async function preprocessDockerExtensionFile( } if (generatedOutputPath !== outputPath) { - await cliHost.rename(generatedOutputPath, outputPath); + await cliHost.copyFile(generatedOutputPath, outputPath); + await cliHost.remove(generatedOutputPath); } infoOutput.write(`Preprocessed Dockerfile written to '${cliOutputPath}'`); diff --git a/src/test/workspaceConfiguration.test.ts b/src/test/workspaceConfiguration.test.ts index 1e0b7e992..3065d89d9 100644 --- a/src/test/workspaceConfiguration.test.ts +++ b/src/test/workspaceConfiguration.test.ts @@ -35,7 +35,9 @@ function createMockCLIHost(options: { throw new Error(`File not found: ${filepath}`); }, writeFile: async () => { }, + copyFile: async () => { }, rename: async () => { }, + remove: async () => { }, mkdirp: async () => { }, readDir: async () => [], getUsername: async () => 'test',