Skip to content

Commit e7b2478

Browse files
committed
Refine Dockerfile preprocessor tool
1 parent 0859e15 commit e7b2478

19 files changed

Lines changed: 679 additions & 67 deletions

File tree

src/spec-configuration/configuration.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,17 @@ export interface DevContainerFeature {
3838
options: boolean | string | Record<string, boolean | string | undefined>;
3939
}
4040

41+
// Dockerfile preprocessing always produces a CLI-owned final Dockerfile path.
42+
// Users provide the preprocessor tool and optional arguments.
43+
// For direct file transforms, users can select whether the tool behaves like a
44+
// single-file transform or expects a build-tree style workspace argument.
45+
// For workspace-style generators, users can instead set generatedDockerfile to
46+
// tell the CLI which file to promote to the final Dockerfile after the tool runs.
4147
export interface DockerfilePreprocessor {
42-
commands?: string[];
43-
output?: string;
48+
tool?: string;
49+
args?: string[];
50+
outputMode?: 'single-file' | 'build-tree';
51+
generatedDockerfile?: string;
4452
}
4553

4654
export interface DevContainerFromImageConfig {

src/spec-node/dockerfilePreprocessor.ts

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,77 +9,112 @@ import { ContainerError, toErrorText } from '../spec-common/errors';
99
import { CLIHost, runCommandNoPty } from '../spec-common/commonUtils';
1010
import { Log, LogLevel, makeLog } from '../spec-utils/log';
1111

12-
export function getDockerfilePreprocessedPath(dockerfilePath: string, output?: string): string | undefined {
12+
function dockerfilePreprocessorToolDocs(): string {
13+
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.";
14+
}
15+
16+
export function getDockerfilePreprocessedPath(dockerfilePath: string): string | undefined {
1317
if (!dockerfilePath.toLowerCase().endsWith('.in')) {
1418
return undefined;
1519
}
16-
if (output) {
17-
return path.isAbsolute(output) ? output : path.join(path.dirname(dockerfilePath), output);
18-
}
19-
return dockerfilePath.slice(0, -3);
20+
return path.join(path.dirname(dockerfilePath), '.devcontainer-preprocessed', 'Dockerfile');
2021
}
2122

2223
export async function preprocessDockerExtensionFile(
2324
params: { cliHost: CLIHost; output: Log },
2425
config: Pick<DevContainerFromDockerfileConfig | DevContainerFromDockerComposeConfig, 'dockerfilePreprocessor'>,
2526
dockerfilePath: string
2627
): Promise<string> {
27-
const outputDockerfilePath = getDockerfilePreprocessedPath(dockerfilePath, config.dockerfilePreprocessor?.output);
28-
if (!outputDockerfilePath) {
28+
const cliOutputPath = getDockerfilePreprocessedPath(dockerfilePath);
29+
if (!cliOutputPath) {
2930
return dockerfilePath;
3031
}
3132

32-
const commands = (config.dockerfilePreprocessor?.commands || []).map(command => command.trim()).filter(command => command.length > 0);
33-
if (!commands.length) {
33+
const tool = config.dockerfilePreprocessor?.tool?.trim();
34+
const args = (config.dockerfilePreprocessor?.args || []).map(arg => arg.trim()).filter(arg => arg.length > 0);
35+
const outputMode = config.dockerfilePreprocessor?.outputMode || 'build-tree';
36+
const generatedDockerfile = config.dockerfilePreprocessor?.generatedDockerfile?.trim();
37+
if (!tool) {
3438
throw new ContainerError({
35-
description: `Dockerfile preprocessor commands are required to build from '${dockerfilePath}'. Set 'dockerfilePreprocessor.commands' in devcontainer.json.`,
39+
description: `A Dockerfile preprocessor tool is required to build from '${dockerfilePath}'. ${dockerfilePreprocessorToolDocs()}`,
3640
data: { fileWithError: dockerfilePath },
3741
});
3842
}
3943

4044
const { cliHost, output } = params;
4145
const infoOutput = makeLog(output, LogLevel.Info);
42-
const isWindows = cliHost.platform === 'win32';
43-
const shell = isWindows ? [cliHost.env.ComSpec || 'cmd.exe', '/c'] : ['/bin/sh', '-c'];
46+
const cliOutputDir = path.dirname(cliOutputPath);
47+
await cliHost.mkdirp(cliOutputDir);
48+
const workdirPath = path.dirname(dockerfilePath);
49+
const inputPath = dockerfilePath;
50+
const outputPath = cliOutputPath;
51+
const generatedOutputPath = generatedDockerfile ? path.resolve(workdirPath, generatedDockerfile) : outputPath;
4452

53+
// Strict contract: the CLI owns the final output path. Direct-transform
54+
// tools can write to the CLI-provided output argument; workspace generators
55+
// can instead declare a generated Dockerfile path for the CLI to promote.
4556
const env = {
4657
...cliHost.env,
47-
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: dockerfilePath,
48-
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputDockerfilePath,
49-
input_file: dockerfilePath,
50-
output_file: outputDockerfilePath,
58+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: inputPath,
59+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputPath,
60+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_WORKDIR: workdirPath,
61+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_GENERATED_DOCKERFILE: generatedOutputPath,
62+
input_file: inputPath,
63+
output_file: outputPath,
64+
generated_dockerfile: generatedOutputPath,
65+
workdir: workdirPath,
5166
};
67+
const directOutputArgs = outputMode === 'single-file'
68+
? [inputPath, outputPath]
69+
: [inputPath, outputPath, workdirPath];
70+
const invocationArgs = generatedDockerfile ? args : [...args, ...directOutputArgs];
5271

5372
try {
54-
infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${outputDockerfilePath}'`);
55-
for (const command of commands) {
56-
await runCommandNoPty({
57-
exec: cliHost.exec,
58-
cmd: shell[0],
59-
args: [shell[1], command],
60-
cwd: path.dirname(dockerfilePath),
61-
env,
62-
output: infoOutput,
63-
print: 'continuous',
64-
});
65-
}
73+
infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${cliOutputPath}'`);
74+
await runCommandNoPty({
75+
exec: cliHost.exec,
76+
cmd: tool,
77+
args: invocationArgs,
78+
cwd: workdirPath,
79+
env,
80+
output: infoOutput,
81+
print: 'continuous',
82+
});
6683
} catch (err) {
84+
const originalError = err as {
85+
message?: string;
86+
stderr?: Buffer | string;
87+
cmdOutput?: string;
88+
code?: number;
89+
signal?: string;
90+
};
91+
const stderrText = typeof originalError?.stderr === 'string' ? originalError.stderr : originalError?.stderr?.toString();
6792
throw new ContainerError({
68-
description: `Dockerfile preprocessing failed while running '${commands[commands.length - 1]}'.`,
93+
description: `Dockerfile preprocessing failed while running '${tool}'. ${dockerfilePreprocessorToolDocs()}`,
6994
originalError: {
70-
...err,
71-
message: `${err?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(err?.stderr || err?.cmdOutput || '')}`.trim(),
95+
message: `${originalError?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(stderrText || originalError?.cmdOutput || '')}`.trim(),
96+
code: originalError?.code,
97+
signal: originalError?.signal,
98+
stderr: originalError?.stderr,
7299
},
73100
data: { fileWithError: dockerfilePath },
74101
});
75102
}
76103

77-
if (!await cliHost.isFile(outputDockerfilePath)) {
104+
if (!await cliHost.isFile(generatedOutputPath)) {
78105
throw new ContainerError({
79-
description: `Dockerfile preprocessing did not produce '${outputDockerfilePath}'.`,
106+
description: generatedDockerfile
107+
? `Dockerfile preprocessing did not produce '${generatedOutputPath}'. Ensure the configured tool writes the final Dockerfile to the configured generatedDockerfile path. ${dockerfilePreprocessorToolDocs()}`
108+
: `Dockerfile preprocessing did not produce '${outputPath}'. Ensure the configured tool writes the final Dockerfile to the CLI-provided output argument. ${dockerfilePreprocessorToolDocs()}`,
80109
data: { fileWithError: dockerfilePath },
81110
});
82111
}
83112

84-
return outputDockerfilePath;
113+
if (generatedOutputPath !== outputPath) {
114+
await cliHost.rename(generatedOutputPath, outputPath);
115+
}
116+
117+
infoOutput.write(`Preprocessed Dockerfile written to '${cliOutputPath}'`);
118+
119+
return cliOutputPath;
85120
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM node:22-bookworm
2+
3+
ARG APP_PORT=3000
4+
EXPOSE 3000
5+
6+
WORKDIR /workspace
7+
COPY . /workspace
8+
9+
CMD ["npm", "start"]

src/test/configs/dockerfile-cmake-preprocessor/.devcontainer.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
"dockerfile": "Dockerfile.in"
44
},
55
"dockerfilePreprocessor": {
6-
"commands": [
7-
"cmake -S . -B build"
6+
"tool": "cmake",
7+
"args": [
8+
"-S",
9+
".",
10+
"-B",
11+
"build"
812
],
9-
"output": "build/Dockerfile"
13+
"generatedDockerfile": "build/Dockerfile"
1014
},
1115
"features": {
1216
"ghcr.io/devcontainers/features/github-cli:1": {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# This is the CMakeCache file.
2+
# For build in directory: /workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build
3+
# It was generated by CMake: /usr/bin/cmake
4+
# You can edit this file to change values found and used by cmake.
5+
# If you do not want to change any of the values, simply exit the editor.
6+
# If you do want to change a value, simply edit, save, and exit the editor.
7+
# The syntax for the file is as follows:
8+
# KEY:TYPE=VALUE
9+
# KEY is the name of a variable in the cache.
10+
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
11+
# VALUE is the current value for the KEY.
12+
13+
########################
14+
# EXTERNAL cache entries
15+
########################
16+
17+
//Enable/Disable color output during build.
18+
CMAKE_COLOR_MAKEFILE:BOOL=ON
19+
20+
//Enable/Disable output of compile commands during generation.
21+
CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=
22+
23+
//Value Computed by CMake.
24+
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build/CMakeFiles/pkgRedirects
25+
26+
//Install path prefix, prepended onto install directories.
27+
CMAKE_INSTALL_PREFIX:PATH=/usr/local
28+
29+
//Path to a program.
30+
CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake
31+
32+
//Value Computed by CMake
33+
CMAKE_PROJECT_DESCRIPTION:STATIC=
34+
35+
//Value Computed by CMake
36+
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
37+
38+
//Value Computed by CMake
39+
CMAKE_PROJECT_NAME:STATIC=GenerateDockerfile
40+
41+
//If set, runtime paths are not added when installing shared libraries,
42+
// but are added when building.
43+
CMAKE_SKIP_INSTALL_RPATH:BOOL=NO
44+
45+
//If set, runtime paths are not added when using shared libraries.
46+
CMAKE_SKIP_RPATH:BOOL=NO
47+
48+
//If this value is on, makefiles will be generated without the
49+
// .SILENT directive, and all commands will be echoed to the console
50+
// during the make. This is useful for debugging only. With Visual
51+
// Studio IDE projects all commands are done without /nologo.
52+
CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE
53+
54+
//Value Computed by CMake
55+
GenerateDockerfile_BINARY_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build
56+
57+
//Value Computed by CMake
58+
GenerateDockerfile_IS_TOP_LEVEL:STATIC=ON
59+
60+
//Value Computed by CMake
61+
GenerateDockerfile_SOURCE_DIR:STATIC=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor
62+
63+
64+
########################
65+
# INTERNAL cache entries
66+
########################
67+
68+
//This is the directory where this CMakeCache.txt was created
69+
CMAKE_CACHEFILE_DIR:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build
70+
//Major version of cmake used to create the current loaded cache
71+
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3
72+
//Minor version of cmake used to create the current loaded cache
73+
CMAKE_CACHE_MINOR_VERSION:INTERNAL=25
74+
//Patch version of cmake used to create the current loaded cache
75+
CMAKE_CACHE_PATCH_VERSION:INTERNAL=1
76+
//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE
77+
CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1
78+
//Path to CMake executable.
79+
CMAKE_COMMAND:INTERNAL=/usr/bin/cmake
80+
//Path to cpack program executable.
81+
CMAKE_CPACK_COMMAND:INTERNAL=/usr/bin/cpack
82+
//Path to ctest program executable.
83+
CMAKE_CTEST_COMMAND:INTERNAL=/usr/bin/ctest
84+
//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS
85+
CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1
86+
//Name of external makefile project generator.
87+
CMAKE_EXTRA_GENERATOR:INTERNAL=
88+
//Name of generator.
89+
CMAKE_GENERATOR:INTERNAL=Unix Makefiles
90+
//Generator instance identifier.
91+
CMAKE_GENERATOR_INSTANCE:INTERNAL=
92+
//Name of generator platform.
93+
CMAKE_GENERATOR_PLATFORM:INTERNAL=
94+
//Name of generator toolset.
95+
CMAKE_GENERATOR_TOOLSET:INTERNAL=
96+
//Source directory with the top level CMakeLists.txt file for this
97+
// project
98+
CMAKE_HOME_DIRECTORY:INTERNAL=/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor
99+
//Install .so files without execute permission.
100+
CMAKE_INSTALL_SO_NO_EXE:INTERNAL=1
101+
//ADVANCED property for variable: CMAKE_MAKE_PROGRAM
102+
CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
103+
//number of local generators
104+
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
105+
//Platform information initialized
106+
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
107+
//Path to CMake installation.
108+
CMAKE_ROOT:INTERNAL=/usr/share/cmake-3.25
109+
//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH
110+
CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1
111+
//ADVANCED property for variable: CMAKE_SKIP_RPATH
112+
CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1
113+
//uname command
114+
CMAKE_UNAME:INTERNAL=/usr/bin/uname
115+
//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE
116+
CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1
117+
//linker supports push/pop state
118+
_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=FALSE
119+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
set(CMAKE_HOST_SYSTEM "Linux-6.8.0-1044-azure")
2+
set(CMAKE_HOST_SYSTEM_NAME "Linux")
3+
set(CMAKE_HOST_SYSTEM_VERSION "6.8.0-1044-azure")
4+
set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64")
5+
6+
7+
8+
set(CMAKE_SYSTEM "Linux-6.8.0-1044-azure")
9+
set(CMAKE_SYSTEM_NAME "Linux")
10+
set(CMAKE_SYSTEM_VERSION "6.8.0-1044-azure")
11+
set(CMAKE_SYSTEM_PROCESSOR "x86_64")
12+
13+
set(CMAKE_CROSSCOMPILING "FALSE")
14+
15+
set(CMAKE_SYSTEM_LOADED 1)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CMAKE generated file: DO NOT EDIT!
2+
# Generated by "Unix Makefiles" Generator, CMake Version 3.25
3+
4+
# Relative path conversion top directories.
5+
set(CMAKE_RELATIVE_PATH_TOP_SOURCE "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor")
6+
set(CMAKE_RELATIVE_PATH_TOP_BINARY "/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/build")
7+
8+
# Force unix paths in dependencies.
9+
set(CMAKE_FORCE_UNIX_PATHS 1)
10+
11+
12+
# The C and CXX include file regular expressions for this directory.
13+
set(CMAKE_C_INCLUDE_REGEX_SCAN "^.*$")
14+
set(CMAKE_C_INCLUDE_REGEX_COMPLAIN "^$")
15+
set(CMAKE_CXX_INCLUDE_REGEX_SCAN ${CMAKE_C_INCLUDE_REGEX_SCAN})
16+
set(CMAKE_CXX_INCLUDE_REGEX_COMPLAIN ${CMAKE_C_INCLUDE_REGEX_COMPLAIN})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# CMAKE generated file: DO NOT EDIT!
2+
# Generated by "Unix Makefiles" Generator, CMake Version 3.25
3+
4+
# The generator used is:
5+
set(CMAKE_DEPENDS_GENERATOR "Unix Makefiles")
6+
7+
# The top level Makefile was generated from the following files:
8+
set(CMAKE_MAKEFILE_DEPENDS
9+
"CMakeCache.txt"
10+
"/usr/share/cmake-3.25/Modules/CMakeDetermineSystem.cmake"
11+
"/usr/share/cmake-3.25/Modules/CMakeGenericSystem.cmake"
12+
"/usr/share/cmake-3.25/Modules/CMakeInitializeConfigs.cmake"
13+
"/usr/share/cmake-3.25/Modules/CMakeSystem.cmake.in"
14+
"/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInformation.cmake"
15+
"/usr/share/cmake-3.25/Modules/CMakeSystemSpecificInitialize.cmake"
16+
"/usr/share/cmake-3.25/Modules/CMakeUnixFindMake.cmake"
17+
"/usr/share/cmake-3.25/Modules/Platform/Linux.cmake"
18+
"/usr/share/cmake-3.25/Modules/Platform/UnixPaths.cmake"
19+
"/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/CMakeLists.txt"
20+
"/workspaces/cli/src/test/configs/dockerfile-cmake-preprocessor/Dockerfile.in"
21+
"CMakeFiles/3.25.1/CMakeSystem.cmake"
22+
)
23+
24+
# The corresponding makefile is:
25+
set(CMAKE_MAKEFILE_OUTPUTS
26+
"Makefile"
27+
"CMakeFiles/cmake.check_cache"
28+
)
29+
30+
# Byproducts of CMake generate step:
31+
set(CMAKE_MAKEFILE_PRODUCTS
32+
"CMakeFiles/3.25.1/CMakeSystem.cmake"
33+
"Dockerfile"
34+
"CMakeFiles/CMakeDirectoryInformation.cmake"
35+
)
36+
37+
# Dependency information for all targets:
38+
set(CMAKE_DEPEND_INFO_FILES
39+
)

0 commit comments

Comments
 (0)