Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): added build field to cdk.json #17176

Merged
merged 19 commits into from Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/aws-cdk/README.md
Expand Up @@ -464,6 +464,7 @@ Some of the interesting keys that can be used in the JSON configuration files:
```json5
{
"app": "node bin/main.js", // Command to start the CDK app (--app='node bin/main.js')
"build": "mvn package", // Specify pre-synth build (no command line option)
"context": { // Context entries (--context=key=value)
"key": "value"
},
Expand All @@ -473,6 +474,12 @@ Some of the interesting keys that can be used in the JSON configuration files:
}
```

If specified, the command in the `build` key will be executed immediately before synthesis.
This can be used to build Lambda Functions, CDK Application code, or other assets.
`build` cannot be specified on the command line or in the User configuration,
and must be specified in the Project configuration. The command specified
in `build` will be executed by the "watch" process before deployment.

### Environment

The following environment variables affect aws-cdk:
Expand Down
11 changes: 8 additions & 3 deletions packages/aws-cdk/lib/api/cxapp/exec.ts
Expand Up @@ -46,6 +46,11 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
debug('context:', context);
env[cxapi.CONTEXT_ENV] = JSON.stringify(context);

const build = config.settings.get(['build']);
if (build) {
await exec(build);
}

const app = config.settings.get(['app']);
if (!app) {
throw new Error(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`);
Expand Down Expand Up @@ -74,7 +79,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom

debug('env:', env);

await exec();
await exec(commandLine.join(' '));
comcalvi marked this conversation as resolved.
Show resolved Hide resolved

return createAssembly(outdir);

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

async function exec() {
async function exec(commandAndArgs: string) {
return new Promise<void>((ok, fail) => {
// We use a slightly lower-level interface to:
//
Expand All @@ -103,7 +108,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
// anyway, and if the subprocess is printing to it for debugging purposes the
// user gets to see it sooner. Plus, capturing doesn't interact nicely with some
// processes like Maven.
const proc = childProcess.spawn(commandLine[0], commandLine.slice(1), {
const proc = childProcess.spawn(commandAndArgs, {
stdio: ['ignore', 'inherit', 'inherit'],
detached: false,
shell: true,
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/settings.ts
Expand Up @@ -113,6 +113,10 @@ export class Configuration {

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

if (userConfig.get(['build'])) {
throw new Error('The `build` key cannot be specified in the user config (~/.cdk.json), specify it in the project config (cdk.json) instead');
}

const contextSources = [
this.commandLineContext,
this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly(),
Expand Down
40 changes: 35 additions & 5 deletions packages/aws-cdk/test/api/exec.test.ts
Expand Up @@ -137,7 +137,7 @@ test('the application set in --app is executed', async () => {
// GIVEN
config.settings.set(['app'], 'cloud-executable');
mockSpawn({
commandLine: ['cloud-executable'],
commandLine: 'cloud-executable',
sideEffect: () => writeOutputAssembly(),
});

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

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

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

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

// WHEN
await execProgram(sdkProvider, config);
});

test('cli throws when the `build` script fails', async () => {
// GIVEN
config.settings.set(['build'], 'fake-command');
mockSpawn({
commandLine: 'fake-command',
exitCode: 127,
});

// WHEN
await expect(execProgram(sdkProvider, config)).rejects.toEqual(new Error('Subprocess exited with error 127'));
}, TEN_SECOND_TIMEOUT);

test('cli does not throw when the `build` script succeeds', async () => {
// GIVEN
config.settings.set(['build'], 'real command');
config.settings.set(['app'], 'executable-app.js');
mockSpawn({
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
commandLine: 'real command', // `build` key is not split on whitespace
exitCode: 0,
},
{
commandLine: 'executable-app.js',
sideEffect: () => writeOutputAssembly(),
});

// WHEN
await execProgram(sdkProvider, config);
}, TEN_SECOND_TIMEOUT);


function writeOutputAssembly() {
const asm = testAssembly({
stacks: [],
Expand Down
20 changes: 20 additions & 0 deletions packages/aws-cdk/test/usersettings.test.ts
Expand Up @@ -69,4 +69,24 @@ test('load context from all 3 files if available', async () => {
expect(config.context.get('project')).toBe('foobar');
expect(config.context.get('foo')).toBe('bar');
expect(config.context.get('test')).toBe('bar');
});

test('throws an error if the `build` key is specified in the user config', async () => {
// GIVEN
const GIVEN_CONFIG: Map<string, any> = new Map([
[USER_CONFIG, {
build: 'foobar',
}],
]);

// WHEN
mockedFs.pathExists.mockImplementation(path => {
return GIVEN_CONFIG.has(path);
});
mockedFs.readJSON.mockImplementation(path => {
return GIVEN_CONFIG.get(path);
});

// THEN
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'));
});
21 changes: 5 additions & 16 deletions packages/aws-cdk/test/util/mock-child_process.ts
Expand Up @@ -6,16 +6,11 @@ if (!(child_process as any).spawn.mockImplementationOnce) {
}

export interface Invocation {
commandLine: string[];
commandLine: string;
cwd?: string;
exitCode?: number;
stdout?: string;

/**
* Only match a prefix of the command (don't care about the details of the arguments)
*/
prefix?: boolean;

/**
* Run this function as a side effect, if present
*/
Expand All @@ -26,14 +21,8 @@ export function mockSpawn(...invocations: Invocation[]) {
let mock = (child_process.spawn as any);
for (const _invocation of invocations) {
const invocation = _invocation; // Mirror into variable for closure
mock = mock.mockImplementationOnce((binary: string, args: string[], options: child_process.SpawnOptions) => {
if (invocation.prefix) {
// Match command line prefix
expect([binary, ...args].slice(0, invocation.commandLine.length)).toEqual(invocation.commandLine);
} else {
// Match full command line
expect([binary, ...args]).toEqual(invocation.commandLine);
}
mock = mock.mockImplementationOnce((binary: string, options: child_process.SpawnOptions) => {
expect(binary).toEqual(invocation.commandLine);

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

mock.mockImplementation((binary: string, args: string[], _options: any) => {
throw new Error(`Did not expect call of ${JSON.stringify([binary, ...args])}`);
mock.mockImplementation((binary: string, _options: any) => {
throw new Error(`Did not expect call of ${binary}`);
});
}

Expand Down