Skip to content

Commit 0859e15

Browse files
committed
Support for building from Dockerfile.in-style templates using a new dockerfilePreprocessor in devcontainer.json
1 parent 65f98a5 commit 0859e15

22 files changed

Lines changed: 507 additions & 3 deletions

File tree

src/spec-configuration/configuration.ts

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

41+
export interface DockerfilePreprocessor {
42+
commands?: string[];
43+
output?: string;
44+
}
45+
4146
export interface DevContainerFromImageConfig {
4247
configFilePath?: URI;
4348
image?: string; // Only optional when setting up an existing container as a dev container.
@@ -111,6 +116,7 @@ export type DevContainerFromDockerfileConfig = {
111116
overrideFeatureInstallOrder?: string[];
112117
hostRequirements?: HostRequirements;
113118
customizations?: Record<string, any>;
119+
dockerfilePreprocessor?: DockerfilePreprocessor;
114120
} & (
115121
{
116122
dockerFile: string;
@@ -169,6 +175,7 @@ export interface DevContainerFromDockerComposeConfig {
169175
overrideFeatureInstallOrder?: string[];
170176
hostRequirements?: HostRequirements;
171177
customizations?: Record<string, any>;
178+
dockerfilePreprocessor?: DockerfilePreprocessor;
172179
}
173180

174181
interface DevContainerVSCodeConfig {

src/spec-node/dockerCompose.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Mount, parseMount } from '../spec-configuration/containerFeaturesConfig
1919
import path from 'path';
2020
import { getDevcontainerMetadata, getImageBuildInfoFromDockerfile, getImageBuildInfoFromImage, getImageMetadataFromContainer, ImageBuildInfo, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata';
2121
import { ensureDockerfileHasFinalStageName } from './dockerfileUtils';
22+
import { preprocessDockerExtensionFile } from './dockerfilePreprocessor';
2223
import { randomUUID } from 'crypto';
2324

2425
const projectLabel = 'com.docker.compose.project';
@@ -166,7 +167,10 @@ export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConf
166167
const serviceInfo = getBuildInfoForService(composeService, cliHost.path, localComposeFiles);
167168
if (serviceInfo.build) {
168169
const { context, dockerfilePath, target } = serviceInfo.build;
169-
const resolvedDockerfilePath = cliHost.path.isAbsolute(dockerfilePath) ? dockerfilePath : path.resolve(context, dockerfilePath);
170+
let resolvedDockerfilePath = cliHost.path.isAbsolute(dockerfilePath) ? dockerfilePath : path.resolve(context, dockerfilePath);
171+
if (resolvedDockerfilePath.toLowerCase().endsWith('.in')) {
172+
resolvedDockerfilePath = await preprocessDockerExtensionFile(common, config, resolvedDockerfilePath);
173+
}
170174
const originalDockerfile = (await cliHost.readFile(resolvedDockerfilePath)).toString();
171175
dockerfile = originalDockerfile;
172176
if (target) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig } from '../spec-configuration/configuration';
8+
import { ContainerError, toErrorText } from '../spec-common/errors';
9+
import { CLIHost, runCommandNoPty } from '../spec-common/commonUtils';
10+
import { Log, LogLevel, makeLog } from '../spec-utils/log';
11+
12+
export function getDockerfilePreprocessedPath(dockerfilePath: string, output?: string): string | undefined {
13+
if (!dockerfilePath.toLowerCase().endsWith('.in')) {
14+
return undefined;
15+
}
16+
if (output) {
17+
return path.isAbsolute(output) ? output : path.join(path.dirname(dockerfilePath), output);
18+
}
19+
return dockerfilePath.slice(0, -3);
20+
}
21+
22+
export async function preprocessDockerExtensionFile(
23+
params: { cliHost: CLIHost; output: Log },
24+
config: Pick<DevContainerFromDockerfileConfig | DevContainerFromDockerComposeConfig, 'dockerfilePreprocessor'>,
25+
dockerfilePath: string
26+
): Promise<string> {
27+
const outputDockerfilePath = getDockerfilePreprocessedPath(dockerfilePath, config.dockerfilePreprocessor?.output);
28+
if (!outputDockerfilePath) {
29+
return dockerfilePath;
30+
}
31+
32+
const commands = (config.dockerfilePreprocessor?.commands || []).map(command => command.trim()).filter(command => command.length > 0);
33+
if (!commands.length) {
34+
throw new ContainerError({
35+
description: `Dockerfile preprocessor commands are required to build from '${dockerfilePath}'. Set 'dockerfilePreprocessor.commands' in devcontainer.json.`,
36+
data: { fileWithError: dockerfilePath },
37+
});
38+
}
39+
40+
const { cliHost, output } = params;
41+
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'];
44+
45+
const env = {
46+
...cliHost.env,
47+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_INPUT: dockerfilePath,
48+
DEVCONTAINER_DOCKERFILE_PREPROCESSOR_OUTPUT: outputDockerfilePath,
49+
input_file: dockerfilePath,
50+
output_file: outputDockerfilePath,
51+
};
52+
53+
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+
}
66+
} catch (err) {
67+
throw new ContainerError({
68+
description: `Dockerfile preprocessing failed while running '${commands[commands.length - 1]}'.`,
69+
originalError: {
70+
...err,
71+
message: `${err?.message || 'Dockerfile preprocessing command failed.'} ${toErrorText(err?.stderr || err?.cmdOutput || '')}`.trim(),
72+
},
73+
data: { fileWithError: dockerfilePath },
74+
});
75+
}
76+
77+
if (!await cliHost.isFile(outputDockerfilePath)) {
78+
throw new ContainerError({
79+
description: `Dockerfile preprocessing did not produce '${outputDockerfilePath}'.`,
80+
data: { fileWithError: dockerfilePath },
81+
});
82+
}
83+
84+
return outputDockerfilePath;
85+
}

src/spec-node/singleContainer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { LogLevel, Log, makeLog } from '../spec-utils/log';
1313
import { extendImage, getExtendImageBuildInfo, updateRemoteUserUID } from './containerFeatures';
1414
import { getDevcontainerMetadata, getImageBuildInfoFromDockerfile, getImageMetadataFromContainer, ImageMetadataEntry, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata';
1515
import { ensureDockerfileHasFinalStageName, generateMountCommand } from './dockerfileUtils';
16+
import { preprocessDockerExtensionFile } from './dockerfilePreprocessor';
1617

1718
export const hostFolderLabel = 'devcontainer.local_folder'; // used to label containers created from a workspace/folder
1819
export const configFileLabel = 'devcontainer.config_file';
@@ -125,8 +126,11 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config
125126
const { cliHost, output } = buildParams.common;
126127
const { config } = configWithRaw;
127128
const dockerfileUri = getDockerfilePath(cliHost, config);
128-
const dockerfilePath = await uriToWSLFsPath(dockerfileUri, cliHost);
129-
if (!cliHost.isFile(dockerfilePath)) {
129+
let dockerfilePath = await uriToWSLFsPath(dockerfileUri, cliHost);
130+
if (dockerfilePath.toLowerCase().endsWith('.in')) {
131+
dockerfilePath = await preprocessDockerExtensionFile(buildParams.common, config, dockerfilePath);
132+
}
133+
if (!await cliHost.isFile(dockerfilePath)) {
130134
throw new ContainerError({ description: `Dockerfile (${dockerfilePath}) not found.` });
131135
}
132136

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"build": {
3+
"dockerfile": "Dockerfile.in"
4+
},
5+
"dockerfilePreprocessor": {
6+
"commands": [
7+
"autoconf",
8+
"./configure"
9+
],
10+
"output": "Dockerfile"
11+
},
12+
"features": {
13+
"ghcr.io/devcontainers/features/github-cli:1": {
14+
"version": "latest"
15+
}
16+
}
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM @BASE_IMAGE@
2+
3+
ARG APP_PORT=@APP_PORT@
4+
EXPOSE @APP_PORT@
5+
6+
WORKDIR /workspace
7+
COPY . /workspace
8+
9+
CMD ["npm", "start"]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
AC_INIT([generate-dockerfile], [1.0])
2+
AC_CONFIG_SRCDIR([Dockerfile.in])
3+
4+
BASE_IMAGE='node:22-bookworm'
5+
APP_PORT='3000'
6+
7+
AC_SUBST([BASE_IMAGE])
8+
AC_SUBST([APP_PORT])
9+
10+
AC_CONFIG_FILES([Dockerfile])
11+
AC_OUTPUT
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"build": {
3+
"dockerfile": "Dockerfile.in"
4+
},
5+
"dockerfilePreprocessor": {
6+
"commands": [
7+
"cmake -S . -B build"
8+
],
9+
"output": "build/Dockerfile"
10+
},
11+
"features": {
12+
"ghcr.io/devcontainers/features/github-cli:1": {
13+
"version": "latest"
14+
}
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
project(GenerateDockerfile NONE)
3+
4+
set(BASE_IMAGE "node:22-bookworm")
5+
set(APP_PORT "3000")
6+
7+
configure_file(
8+
${CMAKE_CURRENT_SOURCE_DIR}/Dockerfile.in
9+
${CMAKE_CURRENT_BINARY_DIR}/Dockerfile
10+
@ONLY
11+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM @BASE_IMAGE@
2+
3+
ARG APP_PORT=@APP_PORT@
4+
EXPOSE @APP_PORT@
5+
6+
WORKDIR /workspace
7+
COPY . /workspace
8+
9+
CMD ["npm", "start"]

0 commit comments

Comments
 (0)