packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts (134 lines of code) (raw):

import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import type { AssemblyDirectoryProps, AssemblySourceProps, ICloudAssemblySource } from '../'; import type { ContextAwareCloudAssemblyProps } from './context-aware-source'; import { ContextAwareCloudAssemblySource } from './context-aware-source'; import { execInChildProcess } from './exec'; import { ExecutionEnvironment, assemblyFromDirectory } from './prepare-source'; import type { ToolkitServices } from '../../../toolkit/private'; import { IO } from '../../io/private'; import { ToolkitError, AssemblyError } from '../../shared-public'; import type { AssemblyBuilder, FromCdkAppOptions } from '../source-builder'; import { ReadableCloudAssembly } from './readable-assembly'; import { Context } from '../../context'; import { RWLock } from '../../rwlock'; import { Settings } from '../../settings'; export abstract class CloudAssemblySourceBuilder { /** * Helper to provide the CloudAssemblySourceBuilder with required toolkit services * @internal * @deprecated this should move to the toolkit really. */ protected abstract sourceBuilderServices(): Promise<ToolkitServices>; /** * Create a Cloud Assembly from a Cloud Assembly builder function. * * A temporary output directory will be created if no output directory is * explicitly given. This directory will be cleaned up if synthesis fails, or * when the Cloud Assembly produced by this source is disposed. * * A write lock will be acquired on the output directory for the duration of * the CDK app synthesis (which means that no two apps can synthesize at the * same time), and after synthesis a read lock will be acquired on the * directory. This means that while the CloudAssembly is being used, no CDK * app synthesis can take place into that directory. * * @param builder the builder function * @param props additional configuration properties * @returns the CloudAssembly source */ public async fromAssemblyBuilder( builder: AssemblyBuilder, props: AssemblySourceProps = {}, ): Promise<ICloudAssemblySource> { const services = await this.sourceBuilderServices(); const context = new Context({ bag: new Settings(props.context ?? {}) }); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, context, lookups: props.lookups, }; return new ContextAwareCloudAssemblySource( { produce: async () => { await using execution = await ExecutionEnvironment.create(services, { outdir: props.outdir }); const env = await execution.defaultEnvVars(); const assembly = await execution.changeDir(async () => execution.withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => execution.withEnv(envWithContext, async () => { try { return await builder({ outdir: execution.outdir, context: ctx, }); } catch (error: unknown) { // re-throw toolkit errors unchanged if (ToolkitError.isToolkitError(error)) { throw error; } // otherwise, wrap into an assembly error throw AssemblyError.withCause('Assembly builder failed', error); } }), ), props.workingDirectory); // Convert what we got to the definitely correct type we're expecting, a cxapi.CloudAssembly const asm = cxapi.CloudAssembly.isCloudAssembly(assembly) ? assembly : await assemblyFromDirectory(assembly.directory, services.ioHelper, props.loadAssemblyOptions); const success = await execution.markSuccessful(); return new ReadableCloudAssembly(asm, success.readLock, { deleteOnDispose: execution.outDirIsTemporary }); }, }, contextAssemblyProps, ); } /** * Creates a Cloud Assembly from an existing assembly directory. * * A read lock will be acquired for the directory. This means that while * the CloudAssembly is being used, no CDK app synthesis can take place into * that directory. * * @param directory the directory of a already produced Cloud Assembly. * @returns the CloudAssembly source */ public async fromAssemblyDirectory(directory: string, props: AssemblyDirectoryProps = {}): Promise<ICloudAssemblySource> { const services: ToolkitServices = await this.sourceBuilderServices(); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, context: new Context(), // @todo there is probably a difference between contextaware and contextlookup sources lookups: false, }; return new ContextAwareCloudAssemblySource( { produce: async () => { // @todo build await services.ioHelper.notify(IO.CDK_ASSEMBLY_I0150.msg('--app points to a cloud assembly, so we bypass synth')); const readLock = await new RWLock(directory).acquireRead(); try { const asm = await assemblyFromDirectory(directory, services.ioHelper, props.loadAssemblyOptions); return new ReadableCloudAssembly(asm, readLock, { deleteOnDispose: false }); } catch (e) { await readLock.release(); throw e; } }, }, contextAssemblyProps, ); } /** * Use a directory containing an AWS CDK app as source. * * A temporary output directory will be created if no output directory is * explicitly given. This directory will be cleaned up if synthesis fails, or * when the Cloud Assembly produced by this source is disposed. * * A write lock will be acquired on the output directory for the duration of * the CDK app synthesis (which means that no two apps can synthesize at the * same time), and after synthesis a read lock will be acquired on the * directory. This means that while the CloudAssembly is being used, no CDK * app synthesis can take place into that directory. * * @param props additional configuration properties * @returns the CloudAssembly source */ public async fromCdkApp(app: string, props: FromCdkAppOptions = {}): Promise<ICloudAssemblySource> { const services: ToolkitServices = await this.sourceBuilderServices(); // @todo this definitely needs to read files from the CWD const context = new Context({ bag: new Settings(props.context ?? {}) }); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, context, lookups: props.lookups, }; return new ContextAwareCloudAssemblySource( { produce: async () => { // @todo build // const build = this.props.configuration.settings.get(['build']); // if (build) { // await execInChildProcess(build, { cwd: props.workingDirectory }); // } const outdir = props.outdir ?? 'cdk.out'; try { fs.mkdirpSync(outdir); } catch (e: any) { throw new ToolkitError(`Could not create output directory at '${outdir}' (${e.message}).`); } await using execution = await ExecutionEnvironment.create(services, { outdir }); const commandLine = await execution.guessExecutable(app); const env = noUndefined({ ...await execution.defaultEnvVars(), ...props.env, }); return await execution.withContext(context.all, env, props.synthOptions, async (envWithContext, _ctx) => { await execInChildProcess(commandLine.join(' '), { eventPublisher: async (type, line) => { switch (type) { case 'data_stdout': await services.ioHelper.notify(IO.CDK_ASSEMBLY_I1001.msg(line)); break; case 'data_stderr': await services.ioHelper.notify(IO.CDK_ASSEMBLY_E1002.msg(line)); break; } }, extraEnv: envWithContext, cwd: props.workingDirectory, }); const asm = await assemblyFromDirectory(outdir, services.ioHelper, props.loadAssemblyOptions); const success = await execution.markSuccessful(); return new ReadableCloudAssembly(asm, success.readLock, { deleteOnDispose: execution.outDirIsTemporary }); }); }, }, contextAssemblyProps, ); } } /** * Remove undefined values from a dictionary */ function noUndefined<A>(xs: Record<string, A>): Record<string, NonNullable<A>> { return Object.fromEntries(Object.entries(xs).filter(([_, v]) => v !== undefined)) as any; }