@@ -9,77 +9,112 @@ import { ContainerError, toErrorText } from '../spec-common/errors';
99import { CLIHost , runCommandNoPty } from '../spec-common/commonUtils' ;
1010import { 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
2223export 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}
0 commit comments