Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/@aws-cdk/cli-lib-alpha/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import type { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions } from './commands';
import { StackActivityProgress, HotswapMode } from './commands';
import { exec as runCli } from '../../../aws-cdk/lib';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createAssembly, prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cxapp/exec';
import { debug } from '../../../aws-cdk/lib/legacy-exports';

const debugFn = async (msg: string) => void debug(msg);

/**
* AWS CDK CLI operations
Expand Down Expand Up @@ -123,8 +124,8 @@ export class AwsCdkCli implements IAwsCdkCli {
public static fromCloudAssemblyDirectoryProducer(producer: ICloudAssemblyDirectoryProducer) {
return new AwsCdkCli(async (args) => changeDir(
() => runCli(args, async (sdk, config) => {
const env = await prepareDefaultEnvironment(sdk);
const context = await prepareContext(config.settings, config.context.all, env);
const env = await prepareDefaultEnvironment(sdk, debugFn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the default value anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepareDefaultEnvironment used to have a default for debugFn, but the default was using the old legacy logging system. Removed the default, made the param mandatory. cli-lib-alpha will use legacy logging until further notice, but we plan to deprecate that package anyway soon.

const context = await prepareContext(config.settings, config.context.all, env, debugFn);

return withEnv(async() => createAssembly(await producer.produce(context)), env);
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ export const IO = {
code: 'CDK_ASSEMBLY_I0000',
description: 'Default debug messages emitted from Cloud Assembly operations',
}),
DEFAULT_ASSEMBLY_INFO: make.info({
code: 'CDK_ASSEMBLY_I0000',
description: 'Default info messages emitted from Cloud Assembly operations',
}),
DEFAULT_ASSEMBLY_WARN: make.warn({
code: 'CDK_ASSEMBLY_W0000',
description: 'Default warning messages emitted from Cloud Assembly operations',
}),

CDK_ASSEMBLY_I0010: make.debug({
code: 'CDK_ASSEMBLY_I0010',
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/toolkit-lib/docs/message-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ group: Documents
| `CDK_TOOLKIT_I0101` | A notice that is marked as informational | `info` | n/a |
| `CDK_ASSEMBLY_I0000` | Default trace messages emitted from Cloud Assembly operations | `trace` | n/a |
| `CDK_ASSEMBLY_I0000` | Default debug messages emitted from Cloud Assembly operations | `debug` | n/a |
| `CDK_ASSEMBLY_I0000` | Default info messages emitted from Cloud Assembly operations | `info` | n/a |
| `CDK_ASSEMBLY_W0000` | Default warning messages emitted from Cloud Assembly operations | `warn` | n/a |
| `CDK_ASSEMBLY_I0010` | Generic environment preparation debug messages | `debug` | n/a |
| `CDK_ASSEMBLY_W0010` | Emitted if the found framework version does not support context overflow | `warn` | n/a |
| `CDK_ASSEMBLY_I0042` | Writing updated context | `debug` | {@link UpdatedContext} |
Expand Down
22 changes: 15 additions & 7 deletions packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type * as cxapi from '@aws-cdk/cx-api';
import { environmentsFromDescriptors } from './private';
import type { Tag } from '../../api/aws-cdk';
import type { ICloudAssemblySource } from '../../api/cloud-assembly';
import type { ICloudAssemblySource, IIoHost } from '../../api/cloud-assembly';
import { ALL_STACKS } from '../../api/cloud-assembly/private';
import { asIoHelper } from '../../api/shared-private';
import { assemblyFromSource } from '../../toolkit/private';

/**
Expand All @@ -21,21 +22,28 @@ export class BootstrapEnvironments {
* Create from a cloud assembly source
*/
static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments {
return new BootstrapEnvironments(async () => {
const assembly = await assemblyFromSource(cx);
const stackCollection = assembly.selectStacksV2(ALL_STACKS);
return new BootstrapEnvironments(async (ioHost: IIoHost) => {
const ioHelper = asIoHelper(ioHost, 'bootstrap');
const assembly = await assemblyFromSource(ioHelper, cx);
const stackCollection = await assembly.selectStacksV2(ALL_STACKS);
return stackCollection.stackArtifacts.map(stack => stack.environment);
});
}

private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise<cxapi.Environment[]>)) {
private constructor(private readonly envProvider: cxapi.Environment[] | ((ioHost: IIoHost) => Promise<cxapi.Environment[]>)) {

}

async getEnvironments(): Promise<cxapi.Environment[]> {
/**
* Compute the bootstrap enviornments
*
* @internal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously missed to mark internal.

*/
async getEnvironments(ioHost: IIoHost): Promise<cxapi.Environment[]> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's internal, we can change this.

if (Array.isArray(this.envProvider)) {
return this.envProvider;
}
return this.envProvider();
return this.envProvider(ioHost);
}
}

Expand Down
Copy link
Contributor Author

@mrgrain mrgrain Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this turned into a bigger refactor. I realized a lot of these helpers share the same context, so a new abstraction (class) seemed appropriate. No functional changes.

Original file line number Diff line number Diff line change
Expand Up @@ -5,122 +5,149 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import { lte } from 'semver';
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber } from '../../../api/aws-cdk';
import type { SdkProvider } from '../../../api/aws-cdk';
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber, guessExecutable } from '../../../api/aws-cdk';
import { splitBySize } from '../../../private/util';
import type { ToolkitServices } from '../../../toolkit/private';
import { IO } from '../../io/private';
import type { IoHelper } from '../../shared-private';
import { ToolkitError } from '../../shared-public';
import type { AppSynthOptions, LoadAssemblyOptions } from '../source-builder';

export { guessExecutable } from '../../../api/aws-cdk';

type Env = { [key: string]: string };
type Context = { [key: string]: any };

/**
* Turn the given optional output directory into a fixed output directory
*/
export function determineOutputDirectory(outdir?: string) {
return outdir ?? fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
}

/**
* If we don't have region/account defined in context, we fall back to the default SDK behavior
* where region is retrieved from ~/.aws/config and account is based on default credentials provider
* chain and then STS is queried.
*
* This is done opportunistically: for example, if we can't access STS for some reason or the region
* is not configured, the context value will be 'null' and there could failures down the line. In
* some cases, synthesis does not require region/account information at all, so that might be perfectly
* fine in certain scenarios.
*
* @param context The context key/value bash.
*/
export async function prepareDefaultEnvironment(services: ToolkitServices, props: { outdir?: string } = {}): Promise<Env> {
const logFn = (msg: string, ...args: any) => services.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(format(msg, ...args)));
const env = await oldPrepare(services.sdkProvider, logFn);

if (props.outdir) {
env[cxapi.OUTDIR_ENV] = props.outdir;
await logFn('outdir:', props.outdir);
export class ExecutionEnvironment {
private readonly ioHelper: IoHelper;
private readonly sdkProvider: SdkProvider;
private readonly debugFn: (msg: string) => Promise<void>;
private _outdir: string | undefined;

public constructor(services: ToolkitServices, props: { outdir?: string } = {}) {
this.ioHelper = services.ioHelper;
this.sdkProvider = services.sdkProvider;
this.debugFn = (msg: string) => this.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg));
this._outdir = props.outdir;
}

// CLI version information
env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version();
env[cxapi.CLI_VERSION_ENV] = versionNumber();

await logFn('env:', env);
return env;
}

/**
* Run code from a different working directory
*/
export async function changeDir<T>(block: () => Promise<T>, workingDir?: string) {
const originalWorkingDir = process.cwd();
try {
if (workingDir) {
process.chdir(workingDir);
/**
* Turn the given optional output directory into a fixed output directory
*/
public get outdir(): string {
if (!this._outdir) {
const outdir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
this._outdir = outdir;
}
return this._outdir;
}

return await block();
} finally {
if (workingDir) {
process.chdir(originalWorkingDir);
}
/**
* Guess the executable from the command-line argument
*
* Only do this if the file is NOT marked as executable. If it is,
* we'll defer to the shebang inside the file itself.
*
* If we're on Windows, we ALWAYS take the handler, since it's hard to
* verify if registry associations have or have not been set up for this
* file type, so we'll assume the worst and take control.
*/
public guessExecutable(app: string) {
return guessExecutable(app, this.debugFn);
}
}

/**
* Run code with additional environment variables
*/
export async function withEnv<T>(env: Env = {}, block: () => Promise<T>) {
const originalEnv = process.env;
try {
process.env = {
...originalEnv,
...env,
};

return await block();
} finally {
process.env = originalEnv;
/**
* If we don't have region/account defined in context, we fall back to the default SDK behavior
* where region is retrieved from ~/.aws/config and account is based on default credentials provider
* chain and then STS is queried.
*
* This is done opportunistically: for example, if we can't access STS for some reason or the region
* is not configured, the context value will be 'null' and there could failures down the line. In
* some cases, synthesis does not require region/account information at all, so that might be perfectly
* fine in certain scenarios.
*/
public async defaultEnvVars(): Promise<Env> {
const debugFn = (msg: string) => this.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(msg));
Copy link
Contributor Author

@mrgrain mrgrain Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uses a different code than the other messages in this class. probably doesn't need to. We can make a breaking change at some point.

const env = await oldPrepare(this.sdkProvider, debugFn);

env[cxapi.OUTDIR_ENV] = this.outdir;
await debugFn(format('outdir:', this.outdir));

// CLI version information
env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version();
env[cxapi.CLI_VERSION_ENV] = versionNumber();

await debugFn(format('env:', env));
return env;
}
}

/**
* Run code with context setup inside the environment
*/
export async function withContext<T>(
inputContext: Context,
env: Env,
synthOpts: AppSynthOptions = {},
block: (env: Env, context: Context) => Promise<T>,
) {
const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env);
let contextOverflowLocation = null;
/**
* Run code from a different working directory
*/
public async changeDir<T>(block: () => Promise<T>, workingDir?: string) {
const originalWorkingDir = process.cwd();
try {
if (workingDir) {
process.chdir(workingDir);
}

return await block();
} finally {
if (workingDir) {
process.chdir(originalWorkingDir);
}
}
}

try {
const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072;
const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit));

// Store the safe part in the environment variable
env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext);

// If there was any overflow, write it to a temporary file
if (Object.keys(overflow ?? {}).length > 0) {
const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context'));
contextOverflowLocation = path.join(contextDir, 'context-overflow.json');
fs.writeJSONSync(contextOverflowLocation, overflow);
env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation;
/**
* Run code with additional environment variables
*/
public async withEnv<T>(env: Env = {}, block: () => Promise<T>) {
const originalEnv = process.env;
try {
process.env = {
...originalEnv,
...env,
};

return await block();
} finally {
process.env = originalEnv;
}
}

// call the block code with new environment
return await block(env, context);
} finally {
if (contextOverflowLocation) {
fs.removeSync(path.dirname(contextOverflowLocation));
/**
* Run code with context setup inside the environment
*/
public async withContext<T>(
inputContext: Context,
env: Env,
synthOpts: AppSynthOptions = {},
block: (env: Env, context: Context) => Promise<T>,
) {
const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env, this.debugFn);
let contextOverflowLocation = null;

try {
const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072;
const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit));

// Store the safe part in the environment variable
env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext);

// If there was any overflow, write it to a temporary file
if (Object.keys(overflow ?? {}).length > 0) {
const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context'));
contextOverflowLocation = path.join(contextDir, 'context-overflow.json');
fs.writeJSONSync(contextOverflowLocation, overflow);
env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation;
}

// call the block code with new environment
return await block(env, context);
} finally {
if (contextOverflowLocation) {
fs.removeSync(path.dirname(contextOverflowLocation));
}
}
}
}
Expand All @@ -130,8 +157,9 @@ export async function withContext<T>(
*
* @param assembly the assembly to check
*/
export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
const tree = loadTree(assembly, (msg: string) => void ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg)));
async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
const traceFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg));
const tree = await loadTree(assembly, traceFn);
const frameworkDoesNotSupportContextOverflow = some(tree, node => {
const fqn = node.constructInfo?.fqn;
const version = node.constructInfo?.version;
Expand All @@ -149,22 +177,22 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly,
/**
* Safely create an assembly from a cloud assembly directory
*/
export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
try {
const assembly = new cxapi.CloudAssembly(assemblyDir, {
skipVersionCheck: !(loadOptions.checkVersion ?? true),
skipEnumCheck: !(loadOptions.checkEnums ?? true),
// We sort as we deploy
topoSort: false,
});
await checkContextOverflowSupport(assembly, ioHost);
await checkContextOverflowSupport(assembly, ioHelper);
return assembly;
} catch (err: any) {
if (err.message.includes(cxschema.VERSION_MISMATCH)) {
// this means the CLI version is too old.
// we instruct the user to upgrade.
const message = 'This AWS CDK Toolkit is not compatible with the AWS CDK library used by your application. Please upgrade to the latest version.';
await ioHost.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err }));
await ioHelper.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err }));
throw new ToolkitError(`${message}\n(${err.message}`);
}
throw err;
Expand Down
Loading