1+ import * as path from 'node:path' ;
12import {
23 AssetHashType ,
34 AssetStaging ,
45 type BundlingFileAccess ,
56 DockerImage ,
6- type DockerRunOptions ,
77 type DockerVolume ,
88} from 'aws-cdk-lib' ;
99import {
@@ -12,63 +12,83 @@ import {
1212 Code ,
1313 type Runtime ,
1414} from 'aws-cdk-lib/aws-lambda' ;
15+ import type { BundlingOptions , ICommandHooks } from './types' ;
1516
1617export const HASHABLE_DEPENDENCIES_EXCLUDE = [ '*.pyc' ] ;
1718
19+ export const DEFAULT_ASSET_EXCLUDES = [
20+ '.venv/' ,
21+ 'node_modules/' ,
22+ 'cdk.out/' ,
23+ '.git/' ,
24+ ] ;
25+
26+ const userCustomizePy = `
27+ import sys
28+ import os
29+ from pathlib import Path
30+
31+ base_path = Path(__file__).parent
32+
33+ for pth_file in base_path.glob('*.pth'):
34+ print('Adding', pth_file, base_path)
35+ with open(pth_file, 'r') as f:
36+ for line in f:
37+ module_path_str = str(base_path / line.strip())
38+ print('...', module_path_str)
39+ if module_path_str not in sys.path:
40+ sys.path.insert(0, module_path_str)
41+
42+ print('sys.path', sys.path)
43+
44+ ` ;
45+
1846interface BundlingCommandOptions {
19- readonly entry : string ;
47+ readonly rootDir : string ;
48+ readonly workspacePackage ?: string ;
2049 readonly inputDir : string ;
2150 readonly outputDir : string ;
51+ readonly assetExcludes : string [ ] ;
52+ readonly commandHooks ?: ICommandHooks ;
2253}
2354
24- export interface BundlingProps extends DockerRunOptions {
25- readonly entry : string ;
26- readonly runtime : Runtime ;
27-
55+ export interface BundlingProps extends BundlingOptions {
2856 /**
29- * Lambda CPU architecture
30- *
31- * @default Architecture.X86_64
57+ * uv project root (workspace root)
3258 */
33- readonly architecture ?: Architecture ;
59+ readonly rootDir : string ;
3460
3561 /**
36- * Skip bundling process
37- *
38- * @default false
62+ * uv package to use for the Lambda Function
3963 */
40- readonly skip ?: boolean ;
64+ readonly workspacePackage ?: string ;
4165
4266 /**
43- * Docker image to use for bundling.
44- *
45- * @default - Default bundling image from the sam/build-python set
67+ * Lambda runtime (must be one of the Python runtimes)
4668 */
47- readonly image ?: DockerImage ;
69+ readonly runtime : Runtime ;
4870
4971 /**
50- * Optional build arguments to pass to the bundling container when the default
51- * image is used.
72+ * Lambda CPU architecture
5273 *
53- * @default - {}
74+ * @default Architecture.ARM_64
5475 */
55- readonly buildArgs ?: { [ key : string ] : string } ;
76+ readonly architecture ?: Architecture ;
5677
5778 /**
58- * Specifies how to copy files to/from the docker container. BIND_MOUNT is generally faster
59- * but VOLUME_MOUNT works with remote Docker contexts.
79+ * Skip bundling process
6080 *
61- * @default - BundlingFileAccess.BIND_MOUNT
81+ * @default false
6282 */
63- readonly bundlingFileAccess ?: BundlingFileAccess ;
83+ readonly skip ?: boolean ;
6484}
6585
6686/**
6787 * Bundling options for Python Lambda assets
6888 */
6989export class Bundling {
7090 public static bundle ( options : BundlingProps ) : AssetCode {
71- return Code . fromAsset ( options . entry , {
91+ return Code . fromAsset ( options . rootDir , {
7292 assetHashType : AssetHashType . SOURCE ,
7393 exclude : HASHABLE_DEPENDENCIES_EXCLUDE ,
7494 bundling : options . skip ? undefined : new Bundling ( options ) ,
@@ -88,17 +108,28 @@ export class Bundling {
88108 public readonly bundlingFileAccess ?: BundlingFileAccess | undefined ;
89109
90110 constructor ( props : BundlingProps ) {
91- const { entry, image, runtime, architecture = Architecture . X86_64 } = props ;
111+ const {
112+ rootDir,
113+ workspacePackage,
114+ image,
115+ runtime,
116+ commandHooks,
117+ assetExcludes = DEFAULT_ASSET_EXCLUDES ,
118+ architecture = Architecture . ARM_64 ,
119+ } = props ;
92120
93121 const bundlingCommands = this . createBundlingCommands ( {
94- entry,
122+ rootDir,
123+ workspacePackage,
124+ assetExcludes,
125+ commandHooks,
95126 inputDir : AssetStaging . BUNDLING_INPUT_DIR ,
96127 outputDir : AssetStaging . BUNDLING_OUTPUT_DIR ,
97128 } ) ;
98129
99130 this . image =
100131 image ??
101- DockerImage . fromBuild ( __dirname , {
132+ DockerImage . fromBuild ( path . resolve ( __dirname , '..' , 'resources' ) , {
102133 buildArgs : {
103134 ...props . buildArgs ,
104135 IMAGE : runtime . bundlingImage . image ,
@@ -123,6 +154,32 @@ export class Bundling {
123154 }
124155
125156 private createBundlingCommands ( options : BundlingCommandOptions ) : string [ ] {
126- return [ `rsync -rLv ${ options . inputDir } / ${ options . outputDir } ` , 'uv sync' ] ;
157+ const excludeArgs = options . assetExcludes . map ( ( exclude ) => `--exclude="${ exclude } "` ) ;
158+ const workspacePackage = options . workspacePackage ;
159+ const uvCommonArgs = `--directory ${ options . outputDir } ` ;
160+ const uvPackageArgs = workspacePackage ? `--package ${ workspacePackage } ` : '' ;
161+ const reqsFile = `/tmp/requirements${ workspacePackage || '' } .txt` ;
162+ const commands = [ ] ;
163+ commands . push (
164+ ...options . commandHooks ?. beforeBundling ( options . inputDir , options . outputDir ) ?? [ ] ,
165+ ) ;
166+ commands . push ( ...[
167+ `while [ -e ${ options . inputDir } /wait.txt ]; do sleep 2; echo Waiting; done` ,
168+ `rsync -rLv ${ excludeArgs . join ( ' ' ) } ${ options . inputDir } / ${ options . outputDir } ` ,
169+ `cd ${ options . outputDir } ` , // uv pip install needs to be run from here for editable deps to relative paths to be resolved
170+ `VIRTUAL_ENV=/tmp/venv uv sync ${ uvCommonArgs } ${ uvPackageArgs } --compile-bytecode --no-dev --frozen --no-editable --link-mode=copy` ,
171+ `VIRTUAL_ENV=/tmp/venv uv export ${ uvCommonArgs } ${ uvPackageArgs } --no-dev --frozen --no-editable > ${ reqsFile } ` ,
172+ `uv pip install -r ${ reqsFile } --target ${ options . outputDir } --reinstall --compile-bytecode --link-mode=copy --editable $(grep -e "^\./" ${ reqsFile } )` ,
173+ `sed -i 's|${ options . outputDir } /|.|g' ${ options . outputDir } /*.pth` ,
174+ `rm -rf ${ options . outputDir } /.venv` ,
175+ `echo ${ Buffer . from ( userCustomizePy ) . toString ( 'base64' ) } | base64 -d > ${ options . outputDir } /usercustomize.py` ,
176+ `while [ -e ${ options . inputDir } /wait2.txt ]; do sleep 2; echo Waiting; done` ,
177+ ] ) ;
178+ commands . push (
179+ ...options . commandHooks ?. afterBundling ( options . inputDir , options . outputDir ) ?? [ ] ,
180+ ) ;
181+ console . log ( 'Bundling commands' , { options, commands } ) ;
182+
183+ return commands ;
127184 }
128185}
0 commit comments