Skip to content

Commit f663bb1

Browse files
committed
simplify contract
1 parent fa5b8da commit f663bb1

10 files changed

Lines changed: 87 additions & 132 deletions

File tree

src/spec-configuration/configuration.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,10 @@ 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.
4741
export interface DockerfilePreprocessor {
4842
tool?: string;
4943
args?: string[];
50-
outputMode?: 'single-file' | 'build-tree';
51-
generatedDockerfile?: string;
44+
generatedDockerfilePath?: string;
5245
}
5346

5447
export interface DevContainerFromImageConfig {

src/spec-node/dockerfilePreprocessor.ts

Lines changed: 17 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,86 +11,55 @@ import { runCommandNoPty } from '../spec-common/commonUtils';
1111
import { Log, LogLevel, makeLog } from '../spec-utils/log';
1212

1313
function dockerfilePreprocessorToolDocs(): string {
14-
return "Set 'dockerfilePreprocessor.tool' and optional 'dockerfilePreprocessor.args' in devcontainer.json. 'outputMode' controls CLI invocation shape: 'single-file' passes input/output and 'build-tree' passes input/output/workdir. When using 'build-tree', set 'generatedDockerfile' to the tool's produced Dockerfile path so the CLI can verify and synchronize outputs.";
15-
}
16-
17-
export function getDockerfilePreprocessedPath(dockerfilePath: string): string | undefined {
18-
if (!dockerfilePath.toLowerCase().endsWith('.in')) {
19-
return undefined;
20-
}
21-
return path.join(path.dirname(dockerfilePath), '.devcontainer-preprocessed', 'Dockerfile');
14+
return "Set 'dockerfilePreprocessor.tool', optional 'dockerfilePreprocessor.args', and 'dockerfilePreprocessor.generatedDockerfilePath' in devcontainer.json. The CLI invokes the tool with configured args and validates that the generated Dockerfile exists at the configured path.";
2215
}
2316

2417
export async function preprocessDockerExtensionFile(
2518
params: { cliHost: CLIHost; output: Log },
2619
config: Pick<DevContainerFromDockerfileConfig | DevContainerFromDockerComposeConfig, 'dockerfilePreprocessor'>,
2720
dockerfilePath: string
2821
): Promise<string> {
29-
const cliOutputPath = getDockerfilePreprocessedPath(dockerfilePath);
30-
if (!cliOutputPath) {
31-
return dockerfilePath;
32-
}
3322

3423
const tool = config.dockerfilePreprocessor?.tool?.trim();
3524
const args = (config.dockerfilePreprocessor?.args || []).map(arg => arg.trim()).filter(arg => arg.length > 0);
36-
const outputMode = config.dockerfilePreprocessor?.outputMode || 'build-tree';
37-
const generatedDockerfile = config.dockerfilePreprocessor?.generatedDockerfile?.trim();
25+
const generatedDockerfilePath = config.dockerfilePreprocessor?.generatedDockerfilePath?.trim();
3826
if (!tool) {
3927
throw new ContainerError({
4028
description: `A Dockerfile preprocessor tool is required to build from '${dockerfilePath}'. ${dockerfilePreprocessorToolDocs()}`,
4129
data: { fileWithError: dockerfilePath },
4230
});
4331
}
44-
if (outputMode === 'single-file' && generatedDockerfile) {
45-
throw new ContainerError({
46-
description: `dockerfilePreprocessor.outputMode 'single-file' cannot be used with 'dockerfilePreprocessor.generatedDockerfile'. Omit generatedDockerfile in single-file mode. ${dockerfilePreprocessorToolDocs()}`,
47-
data: { fileWithError: dockerfilePath },
48-
});
49-
}
50-
if (outputMode === 'build-tree' && !generatedDockerfile) {
32+
if (!generatedDockerfilePath) {
5133
throw new ContainerError({
52-
description: `dockerfilePreprocessor.outputMode 'build-tree' requires 'dockerfilePreprocessor.generatedDockerfile' to be set. ${dockerfilePreprocessorToolDocs()}`,
34+
description: `dockerfilePreprocessor.generatedDockerfilePath is required. ${dockerfilePreprocessorToolDocs()}`,
5335
data: { fileWithError: dockerfilePath },
5436
});
5537
}
5638

5739
const { cliHost, output } = params;
5840
const infoOutput = makeLog(output, LogLevel.Info);
59-
const cliOutputDir = path.dirname(cliOutputPath);
60-
await cliHost.mkdirp(cliOutputDir);
6141
const workdirPath = path.dirname(dockerfilePath);
62-
const inputPath = dockerfilePath;
63-
const outputPath = cliOutputPath;
64-
const generatedOutputPath = generatedDockerfile ? path.resolve(workdirPath, generatedDockerfile) : outputPath;
65-
const staleOutputPaths = generatedOutputPath === outputPath ? [outputPath] : [outputPath, generatedOutputPath];
42+
const generatedOutputPath = path.resolve(workdirPath, generatedDockerfilePath);
43+
const generatedOutputDir = path.dirname(generatedOutputPath);
44+
await cliHost.mkdirp(generatedOutputDir);
45+
const staleOutputPaths = [generatedOutputPath];
6646
for (const stalePath of staleOutputPaths) {
6747
if (!await cliHost.isFile(stalePath)) {
6848
continue;
6949
}
7050
await cliHost.remove(stalePath);
7151
}
7252

73-
// Strict contract: the CLI owns the final output path. Direct-transform
74-
// tools can write to the CLI-provided output argument; workspace generators
75-
// can instead declare a generated Dockerfile path for the CLI to promote.
53+
// Minimal contract: tool args are user-controlled and run in the Dockerfile
54+
// directory. The CLI only provides the resolved generated Dockerfile path.
7655
const env = {
7756
...cliHost.env,
78-
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: inputPath,
79-
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputPath,
80-
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_WORKDIR: workdirPath,
8157
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_GENERATED_DOCKERFILE: generatedOutputPath,
82-
input_file: inputPath,
83-
output_file: outputPath,
84-
generated_dockerfile: generatedOutputPath,
85-
workdir: workdirPath,
8658
};
87-
const directOutputArgs = outputMode === 'single-file'
88-
? [inputPath, outputPath]
89-
: [inputPath, outputPath, workdirPath];
90-
const invocationArgs = [...args, ...directOutputArgs];
59+
const invocationArgs = [...args];
9160

9261
try {
93-
infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${cliOutputPath}'`);
62+
infoOutput.write(`Preprocessing '${dockerfilePath}' -> '${generatedOutputPath}'`);
9463
await runCommandNoPty({
9564
exec: cliHost.exec,
9665
cmd: tool,
@@ -121,25 +90,15 @@ export async function preprocessDockerExtensionFile(
12190
});
12291
}
12392

124-
if (generatedDockerfile && generatedOutputPath !== outputPath && !await cliHost.isFile(generatedOutputPath) && await cliHost.isFile(outputPath)) {
125-
infoOutput.write(`No generated Dockerfile found at '${generatedOutputPath}', copying from CLI output '${outputPath}' to keep generated output consistent.`);
126-
await cliHost.copyFile(outputPath, generatedOutputPath);
127-
}
128-
129-
if (!await cliHost.isFile(generatedOutputPath)) {
93+
const generatedExists = await cliHost.isFile(generatedOutputPath);
94+
if (!generatedExists) {
13095
throw new ContainerError({
131-
description: generatedDockerfile
132-
? `Dockerfile preprocessing did not produce '${generatedOutputPath}'. Ensure the configured tool writes the final Dockerfile to the configured generatedDockerfile path. ${dockerfilePreprocessorToolDocs()}`
133-
: `Dockerfile preprocessing did not produce '${outputPath}'. Ensure the configured tool writes the final Dockerfile to the CLI-provided output argument. ${dockerfilePreprocessorToolDocs()}`,
96+
description: `Dockerfile preprocessing did not produce '${generatedOutputPath}'. Ensure the configured tool writes the final Dockerfile to the configured generatedDockerfile path. ${dockerfilePreprocessorToolDocs()}`,
13497
data: { fileWithError: dockerfilePath },
13598
});
13699
}
137100

138-
if (generatedOutputPath !== outputPath) {
139-
await cliHost.copyFile(generatedOutputPath, outputPath);
140-
}
141-
142-
infoOutput.write(`Preprocessed Dockerfile written to '${cliOutputPath}'`);
101+
infoOutput.write(`Preprocessed Dockerfile written to '${generatedOutputPath}'`);
143102

144-
return cliOutputPath;
103+
return generatedOutputPath;
145104
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#define BASE_IMAGE ubuntu:20.04
2+
#define INSTALL_NODE
3+
#define INSTALL_PYTHON
4+
5+
FROM BASE_IMAGE
6+
7+
#ifdef INSTALL_NODE
8+
RUN apt-get update && apt-get install -y nodejs
9+
#endif
10+
11+
#ifdef INSTALL_PYTHON
12+
RUN apt-get update && apt-get install -y python3
13+
#endif
14+
15+
#include "common.Dockerfile"
16+
#include "tools.Dockerfile"

src/test/configs/dockercomposefile-cpp-preprocessor/.devcontainer/devcontainer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
"workspaceFolder": "/workspace",
66
"dockerfilePreprocessor": {
77
"tool": "cpp",
8-
"outputMode": "single-file",
8+
"generatedDockerfilePath": "Dockerfile",
99
"args": [
10-
"-P"
10+
"-P",
11+
"Dockerfile.in",
12+
"Dockerfile"
1113
]
1214
},
1315
"features": {
14-
"ghcr.io/devcontainers/features/github-cli:1": {
15-
"version": "latest"
16-
}
16+
"ghcr.io/devcontainers/features/git": "latest"
1717
}
1818
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"-c",
99
"autoconf && ./configure"
1010
],
11-
"generatedDockerfile": "Dockerfile"
11+
"generatedDockerfilePath": "Dockerfile"
1212
},
1313
"features": {
1414
"ghcr.io/devcontainers/features/github-cli:1": {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"-B",
1111
"build"
1212
],
13-
"generatedDockerfile": "build/Dockerfile"
13+
"generatedDockerfilePath": "build/Dockerfile"
1414
},
1515
"features": {
1616
"ghcr.io/devcontainers/features/github-cli:1": {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"-B",
1111
"build"
1212
],
13-
"generatedDockerfile": "Dockerfile"
13+
"generatedDockerfilePath": "Dockerfile"
1414
},
1515
"features": {
1616
"ghcr.io/devcontainers/features/github-cli:1": {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
},
55
"dockerfilePreprocessor": {
66
"tool": "cpp",
7-
"outputMode": "single-file",
87
"args": [
9-
"-P"
10-
]
8+
"-P",
9+
"./Dockerfile.in",
10+
"Dockerfile"
11+
],
12+
"generatedDockerfilePath": "Dockerfile"
1113
},
1214
"features": {
1315
"ghcr.io/devcontainers/features/github-cli:1": {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"setup",
99
"build"
1010
],
11-
"generatedDockerfile": "build/Dockerfile"
11+
"generatedDockerfilePath": "build/Dockerfile"
1212
},
1313
"features": {
1414
"ghcr.io/devcontainers/features/github-cli:1": {

0 commit comments

Comments
 (0)