Skip to content

Commit 57ad1e0

Browse files
authored
feat(cli): added build field to cdk.json (#17176)
Adds a `build` field to `cdk.json`. The command specified in the `build` will be executed before synthesis. This can be used to build any code that needs to be built before synthesis (for example, CDK App code or Lambda Function code). This is part of the changes needed for the `cdk watch` command (aws/aws-cdk-rfcs#383). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent bc00427 commit 57ad1e0

File tree

6 files changed

+79
-24
lines changed

6 files changed

+79
-24
lines changed

packages/aws-cdk/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ Some of the interesting keys that can be used in the JSON configuration files:
464464
```json5
465465
{
466466
"app": "node bin/main.js", // Command to start the CDK app (--app='node bin/main.js')
467+
"build": "mvn package", // Specify pre-synth build (no command line option)
467468
"context": { // Context entries (--context=key=value)
468469
"key": "value"
469470
},
@@ -473,6 +474,12 @@ Some of the interesting keys that can be used in the JSON configuration files:
473474
}
474475
```
475476

477+
If specified, the command in the `build` key will be executed immediately before synthesis.
478+
This can be used to build Lambda Functions, CDK Application code, or other assets.
479+
`build` cannot be specified on the command line or in the User configuration,
480+
and must be specified in the Project configuration. The command specified
481+
in `build` will be executed by the "watch" process before deployment.
482+
476483
### Environment
477484

478485
The following environment variables affect aws-cdk:

packages/aws-cdk/lib/api/cxapp/exec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
4646
debug('context:', context);
4747
env[cxapi.CONTEXT_ENV] = JSON.stringify(context);
4848

49+
const build = config.settings.get(['build']);
50+
if (build) {
51+
await exec(build);
52+
}
53+
4954
const app = config.settings.get(['app']);
5055
if (!app) {
5156
throw new Error(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`);
@@ -74,7 +79,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
7479

7580
debug('env:', env);
7681

77-
await exec();
82+
await exec(commandLine.join(' '));
7883

7984
return createAssembly(outdir);
8085

@@ -91,7 +96,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
9196
}
9297
}
9398

94-
async function exec() {
99+
async function exec(commandAndArgs: string) {
95100
return new Promise<void>((ok, fail) => {
96101
// We use a slightly lower-level interface to:
97102
//
@@ -103,7 +108,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
103108
// anyway, and if the subprocess is printing to it for debugging purposes the
104109
// user gets to see it sooner. Plus, capturing doesn't interact nicely with some
105110
// processes like Maven.
106-
const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), {
111+
const proc = childProcess.spawn(commandAndArgs, {
107112
stdio: ['ignore', 'inherit', 'inherit'],
108113
detached: false,
109114
shell: true,

packages/aws-cdk/lib/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ export class Configuration {
113113

114114
const readUserContext = this.props.readUserContext ?? true;
115115

116+
if (userConfig.get(['build'])) {
117+
throw new Error('The `build` key cannot be specified in the user config (~/.cdk.json), specify it in the project config (cdk.json) instead');
118+
}
119+
116120
const contextSources = [
117121
this.commandLineContext,
118122
this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly(),

packages/aws-cdk/test/api/exec.test.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ test('the application set in --app is executed', async () => {
137137
// GIVEN
138138
config.settings.set(['app'], 'cloud-executable');
139139
mockSpawn({
140-
commandLine: ['cloud-executable'],
140+
commandLine: 'cloud-executable',
141141
sideEffect: () => writeOutputAssembly(),
142142
});
143143

@@ -149,7 +149,7 @@ test('the application set in --app is executed as-is if it contains a filename t
149149
// GIVEN
150150
config.settings.set(['app'], 'does-not-exist');
151151
mockSpawn({
152-
commandLine: ['does-not-exist'],
152+
commandLine: 'does-not-exist',
153153
sideEffect: () => writeOutputAssembly(),
154154
});
155155

@@ -161,7 +161,7 @@ test('the application set in --app is executed with arguments', async () => {
161161
// GIVEN
162162
config.settings.set(['app'], 'cloud-executable an-arg');
163163
mockSpawn({
164-
commandLine: ['cloud-executable', 'an-arg'],
164+
commandLine: 'cloud-executable an-arg',
165165
sideEffect: () => writeOutputAssembly(),
166166
});
167167

@@ -174,7 +174,7 @@ test('application set in --app as `*.js` always uses handler on windows', async
174174
sinon.stub(process, 'platform').value('win32');
175175
config.settings.set(['app'], 'windows.js');
176176
mockSpawn({
177-
commandLine: [process.execPath, 'windows.js'],
177+
commandLine: process.execPath + ' windows.js',
178178
sideEffect: () => writeOutputAssembly(),
179179
});
180180

@@ -186,14 +186,44 @@ test('application set in --app is `*.js` and executable', async () => {
186186
// GIVEN
187187
config.settings.set(['app'], 'executable-app.js');
188188
mockSpawn({
189-
commandLine: ['executable-app.js'],
189+
commandLine: 'executable-app.js',
190190
sideEffect: () => writeOutputAssembly(),
191191
});
192192

193193
// WHEN
194194
await execProgram(sdkProvider, config);
195195
});
196196

197+
test('cli throws when the `build` script fails', async () => {
198+
// GIVEN
199+
config.settings.set(['build'], 'fake-command');
200+
mockSpawn({
201+
commandLine: 'fake-command',
202+
exitCode: 127,
203+
});
204+
205+
// WHEN
206+
await expect(execProgram(sdkProvider, config)).rejects.toEqual(new Error('Subprocess exited with error 127'));
207+
}, TEN_SECOND_TIMEOUT);
208+
209+
test('cli does not throw when the `build` script succeeds', async () => {
210+
// GIVEN
211+
config.settings.set(['build'], 'real command');
212+
config.settings.set(['app'], 'executable-app.js');
213+
mockSpawn({
214+
commandLine: 'real command', // `build` key is not split on whitespace
215+
exitCode: 0,
216+
},
217+
{
218+
commandLine: 'executable-app.js',
219+
sideEffect: () => writeOutputAssembly(),
220+
});
221+
222+
// WHEN
223+
await execProgram(sdkProvider, config);
224+
}, TEN_SECOND_TIMEOUT);
225+
226+
197227
function writeOutputAssembly() {
198228
const asm = testAssembly({
199229
stacks: [],

packages/aws-cdk/test/usersettings.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,24 @@ test('load context from all 3 files if available', async () => {
6969
expect(config.context.get('project')).toBe('foobar');
7070
expect(config.context.get('foo')).toBe('bar');
7171
expect(config.context.get('test')).toBe('bar');
72+
});
73+
74+
test('throws an error if the `build` key is specified in the user config', async () => {
75+
// GIVEN
76+
const GIVEN_CONFIG: Map<string, any> = new Map([
77+
[USER_CONFIG, {
78+
build: 'foobar',
79+
}],
80+
]);
81+
82+
// WHEN
83+
mockedFs.pathExists.mockImplementation(path => {
84+
return GIVEN_CONFIG.has(path);
85+
});
86+
mockedFs.readJSON.mockImplementation(path => {
87+
return GIVEN_CONFIG.get(path);
88+
});
89+
90+
// THEN
91+
await expect(new Configuration().load()).rejects.toEqual(new Error('The `build` key cannot be specified in the user config (~/.cdk.json), specify it in the project config (cdk.json) instead'));
7292
});

packages/aws-cdk/test/util/mock-child_process.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@ if (!(child_process as any).spawn.mockImplementationOnce) {
66
}
77

88
export interface Invocation {
9-
commandLine: string[];
9+
commandLine: string;
1010
cwd?: string;
1111
exitCode?: number;
1212
stdout?: string;
1313

14-
/**
15-
* Only match a prefix of the command (don't care about the details of the arguments)
16-
*/
17-
prefix?: boolean;
18-
1914
/**
2015
* Run this function as a side effect, if present
2116
*/
@@ -26,14 +21,8 @@ export function mockSpawn(...invocations: Invocation[]) {
2621
let mock = (child_process.spawn as any);
2722
for (const _invocation of invocations) {
2823
const invocation = _invocation; // Mirror into variable for closure
29-
mock = mock.mockImplementationOnce((binary: string, args: string[], options: child_process.SpawnOptions) => {
30-
if (invocation.prefix) {
31-
// Match command line prefix
32-
expect([binary, ...args].slice(0, invocation.commandLine.length)).toEqual(invocation.commandLine);
33-
} else {
34-
// Match full command line
35-
expect([binary, ...args]).toEqual(invocation.commandLine);
36-
}
24+
mock = mock.mockImplementationOnce((binary: string, options: child_process.SpawnOptions) => {
25+
expect(binary).toEqual(invocation.commandLine);
3726

3827
if (invocation.cwd != null) {
3928
expect(options.cwd).toBe(invocation.cwd);
@@ -60,8 +49,8 @@ export function mockSpawn(...invocations: Invocation[]) {
6049
});
6150
}
6251

63-
mock.mockImplementation((binary: string, args: string[], _options: any) => {
64-
throw new Error(`Did not expect call of ${JSON.stringify([binary, ...args])}`);
52+
mock.mockImplementation((binary: string, _options: any) => {
53+
throw new Error(`Did not expect call of ${binary}`);
6554
});
6655
}
6756

0 commit comments

Comments
 (0)