aws-lambda-layer-cli 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,821 @@
1
+ #!/bin/bash
2
+
3
+ # AWS Lambda Layer CLI Tool
4
+ # Install: sudo ./scripts/install.sh
5
+ # Usage: aws-lambda-layer zip --nodejs express,axios -n my-layer
6
+
7
+ set -e
8
+ set -u
9
+
10
+ # Colors for output
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ YELLOW='\033[0;33m'
14
+ BLUE='\033[0;34m'
15
+ MAGENTA='\033[0;35m'
16
+ CYAN='\033[0;36m'
17
+ WHITE='\033[0;37m'
18
+ NC='\033[0m' # No Color
19
+
20
+ # Styles
21
+ BOLD='\033[1m'
22
+ ITALIC='\033[3m'
23
+ UNDERLINE='\033[4m'
24
+ REVERSE='\033[7m'
25
+ STRIKETHROUGH='\033[9m'
26
+
27
+ # Paths
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ INSTALL_DIR="/usr/local/lib/aws-lambda-layer"
30
+
31
+ # Determine script locations (prefer relative to script, fallback to global install)
32
+ if [ -f "$SCRIPT_DIR/create_nodejs_layer.sh" ]; then
33
+ NODE_SCRIPT="$SCRIPT_DIR/create_nodejs_layer.sh"
34
+ else
35
+ NODE_SCRIPT="$INSTALL_DIR/create_nodejs_layer.sh"
36
+ fi
37
+
38
+ if [ -f "$SCRIPT_DIR/create_python_layer.sh" ]; then
39
+ PYTHON_SCRIPT="$SCRIPT_DIR/create_python_layer.sh"
40
+ else
41
+ PYTHON_SCRIPT="$INSTALL_DIR/create_python_layer.sh"
42
+ fi
43
+
44
+ BIN_DIR="/usr/local/bin"
45
+ COMPLETION_DIR="/etc/bash_completion.d"
46
+
47
+ # Show help
48
+ show_help() {
49
+ local version_file="$SCRIPT_DIR/VERSION.txt"
50
+ local version="1.4.1"
51
+ if [ -f "$version_file" ]; then
52
+ version=$(cat "$version_file")
53
+ fi
54
+ printf "${BLUE}${BOLD}AWS Lambda Layer CLI Tool - ${version}${NC}\n\n"
55
+
56
+ printf "${BLUE}Usage:${NC}\n"
57
+ printf " aws-lambda-layer ${GREEN}zip${NC} ${YELLOW}--nodejs${NC} <packages> [options]\n"
58
+ printf " aws-lambda-layer ${GREEN}zip${NC} ${YELLOW}--python${NC} <packages> [options]\n"
59
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--nodejs${NC} <packages> [options]\n"
60
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--python${NC} <packages> [options]\n"
61
+ printf " aws-lambda-layer ${GREEN}help${NC}\n"
62
+ printf " aws-lambda-layer [options]\n\n"
63
+
64
+ printf "${BLUE}Commands:${NC}\n"
65
+ printf " ${GREEN}zip${NC} Create and package a Lambda layer as zip file\n"
66
+ printf " ${GREEN}publish${NC} Create and publish a Lambda layer to AWS (uses IAM credentials)\n"
67
+ printf " ${GREEN}help${NC} Show this help message\n\n"
68
+
69
+ printf "${BLUE}Runtime Options:${NC}\n"
70
+ printf " ${YELLOW}--nodejs, --node, -n${NC} Create a Node.js Lambda layer\n"
71
+ printf " ${YELLOW}--python, --py, -p${NC} Create a Python Lambda layer\n"
72
+ printf " ${YELLOW}--runtime=RUNTIME${NC} Specify runtime (nodejs or python)\n"
73
+ printf "${BLUE}Arguments:${NC}\n"
74
+ printf " <packages> Comma-separated list of packages with optional versions (required)\n"
75
+
76
+ printf "${BLUE}Common Options:${NC}\n"
77
+ printf " ${YELLOW}--name${NC} Name for the output zip file / layer name\n"
78
+ printf " ${YELLOW}--description${NC} Description for the layer (publish command only)\n"
79
+ printf "${BLUE}AWS Options (publish command only):${NC}\n"
80
+ printf " ${YELLOW}--profile${NC} AWS CLI profile to use (default: default profile)\n"
81
+ printf " ${YELLOW}--region${NC} AWS region (e.g., us-east-1, ap-east-1)\n"
82
+ printf "${BLUE}Other Options:${NC}\n"
83
+ printf " ${YELLOW}--node-version${NC} Node.js version (default: 24)\n"
84
+ printf " ${YELLOW}--python-version${NC} Python version (default: 3.14)\n"
85
+ printf " ${YELLOW}--no-uv${NC} Use pip/venv instead of uv\n\n"
86
+
87
+ printf "${MAGENTA}${UNDERLINE}Package Version Examples:${NC}\n"
88
+ printf " Node.js: express@^4.0.0, lodash@~4.17.0, axios@>=1.6.0\n"
89
+ printf " Python: numpy==1.26.0, pandas>=2.1.0, requests~=2.31.0\n\n"
90
+
91
+ printf "${MAGENTA}${UNDERLINE}Examples:${NC}\n"
92
+ printf " aws-lambda-layer ${GREEN}zip${NC} ${YELLOW}--nodejs${NC} express,lodash\n"
93
+ printf " aws-lambda-layer ${GREEN}zip${NC} ${YELLOW}--python${NC} \"numpy==1.26.0,pandas>=2.1.0\"\n"
94
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--nodejs${NC} express@4.18.2 ${YELLOW}--description${NC} \"Express layer\"\n"
95
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--python${NC} numpy==1.26.0 ${YELLOW}--description${NC} \"NumPy layer\"\n"
96
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--nodejs${NC} date-fns,uuid ${YELLOW}--name${NC} utility-layer ${YELLOW}--description${NC} \"Utility packages\"\n"
97
+ printf " aws-lambda-layer ${GREEN}publish${NC} ${YELLOW}--nodejs${NC} express@4.18.2 ${YELLOW}--profile${NC} production ${YELLOW}--region${NC} ap-east-1 ${YELLOW}--description${NC} \"Express layer\"\n\n"
98
+
99
+ printf "${YELLOW}${BOLD}Requirements for publish command:${NC}\n"
100
+ printf " - AWS CLI installed and configured\n"
101
+ printf " - IAM credentials with lambda:PublishLayerVersion permission\n"
102
+ printf " - Proper AWS region configuration\n"
103
+ }
104
+
105
+ # Show version
106
+ show_version() {
107
+ local version_file="$SCRIPT_DIR/VERSION.txt"
108
+ if [ -f "$version_file" ]; then
109
+ local version=$(cat "$version_file")
110
+ echo "v$version"
111
+ else
112
+ # Fallback if VERSION file is missing (e.g. during development or if moved)
113
+ echo "v1.4.1"
114
+ fi
115
+ }
116
+
117
+ # Check if AWS CLI is installed and configured
118
+ check_aws_cli() {
119
+ local profile="$1"
120
+ local region="$2"
121
+ local aws_opts=()
122
+
123
+ if ! command -v aws &> /dev/null; then
124
+ printf "${RED}Error: AWS CLI is not installed${NC}\n"
125
+ printf "Please install AWS CLI first:\n"
126
+ printf " https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n"
127
+ return 1
128
+ fi
129
+
130
+ # Build AWS CLI options
131
+ if [ -n "$profile" ]; then
132
+ aws_opts+=("--profile" "$profile")
133
+ fi
134
+ if [ -n "$region" ]; then
135
+ aws_opts+=("--region" "$region")
136
+ fi
137
+
138
+ # Check if AWS credentials are configured
139
+ if ! aws ${aws_opts[@]+"${aws_opts[@]}"} sts get-caller-identity &> /dev/null; then
140
+ printf "${RED}Error: AWS credentials not configured or invalid${NC}\n"
141
+ if [ -n "$profile" ]; then
142
+ printf "Profile: $profile\n"
143
+ fi
144
+ printf "Please configure AWS credentials:\n"
145
+ printf " aws configure"
146
+ if [ -n "$profile" ]; then
147
+ printf " --profile $profile"
148
+ fi
149
+ printf "\n"
150
+ printf " or set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION environment variables\n"
151
+ return 1
152
+ fi
153
+
154
+ return 0
155
+ }
156
+
157
+ # Get AWS account info
158
+ get_aws_account_info() {
159
+ local profile="$1"
160
+ local region="$2"
161
+ local aws_opts=()
162
+
163
+ # Build AWS CLI options
164
+ if [ -n "$profile" ]; then
165
+ aws_opts+=("--profile" "$profile")
166
+ fi
167
+ if [ -n "$region" ]; then
168
+ aws_opts+=("--region" "$region")
169
+ fi
170
+
171
+ printf "${CYAN}Checking AWS account...${NC}\n"
172
+
173
+ # Get account ID
174
+ local account_id
175
+ account_id=$(aws ${aws_opts[@]+"${aws_opts[@]}"} sts get-caller-identity --query Account --output text 2>/dev/null)
176
+ if [ $? -eq 0 ]; then
177
+ printf " Account ID: ${GREEN}$account_id${NC}\n"
178
+ else
179
+ printf " ${YELLOW}Could not retrieve account ID${NC}\n"
180
+ account_id="UNKNOWN"
181
+ fi
182
+
183
+ # Display profile if specified
184
+ if [ -n "$profile" ]; then
185
+ printf " Profile: ${GREEN}$profile${NC}\n"
186
+ fi
187
+
188
+ # Get account aliases
189
+ local aliases
190
+ aliases=$(aws ${aws_opts[@]+"${aws_opts[@]}"} iam list-account-aliases --query 'AccountAliases' --output text 2>/dev/null)
191
+ if [ $? -eq 0 ] && [ -n "$aliases" ]; then
192
+ printf " Account Aliases: ${GREEN}$aliases${NC}\n"
193
+ else
194
+ printf " ${YELLOW}No account aliases found${NC}\n"
195
+ fi
196
+
197
+ # Get region
198
+ local display_region="$region"
199
+ if [ -z "$display_region" ]; then
200
+ if [ -n "$profile" ]; then
201
+ display_region=$(aws ${aws_opts[@]+"${aws_opts[@]}"} configure get region 2>/dev/null)
202
+ else
203
+ display_region=$(aws configure get region 2>/dev/null || echo "$AWS_DEFAULT_REGION")
204
+ fi
205
+ fi
206
+ if [ -n "$display_region" ]; then
207
+ printf " Region: ${GREEN}$display_region${NC}\n"
208
+ else
209
+ printf " ${YELLOW}Region not configured${NC}\n"
210
+ fi
211
+
212
+ printf "\n"
213
+ printf "${YELLOW}⚠️ This layer will be published to the AWS account above.${NC}\n"
214
+
215
+ # Prompt user for confirmation
216
+ read -p "Do you want to proceed? [Y/n]: " response
217
+ response=${response:-Y} # Default to Y if empty
218
+
219
+ case "$response" in
220
+ [Yy]|[Yy][Ee][Ss])
221
+ printf "${GREEN}Proceeding with layer publication...${NC}\n\n"
222
+ ;;
223
+ *)
224
+ printf "${RED}Publication cancelled.${NC}\n"
225
+ exit 0
226
+ ;;
227
+ esac
228
+ }
229
+
230
+ # Determine compatible runtimes for AWS
231
+ get_compatible_runtimes() {
232
+ local runtime="$1"
233
+ local version="$2"
234
+
235
+ case "$runtime" in
236
+ nodejs)
237
+ echo "nodejs${version}.x"
238
+ ;;
239
+ python)
240
+ # AWS Lambda uses format like python3.14, not python3.14.x
241
+ echo "python${version}"
242
+ ;;
243
+ *)
244
+ echo ""
245
+ ;;
246
+ esac
247
+ }
248
+
249
+ # Check dependencies
250
+ check_dependencies() {
251
+ local runtime="$1"
252
+
253
+ # Check for zip
254
+ if ! command -v zip &> /dev/null; then
255
+ printf "${RED}Error: 'zip' command is not installed${NC}\n"
256
+ printf "Please install zip first.\n"
257
+ return 1
258
+ fi
259
+
260
+ # Check runtime
261
+ if [ "$runtime" = "nodejs" ]; then
262
+ if ! command -v node &> /dev/null; then
263
+ printf "${RED}Error: 'node' command is not installed${NC}\n"
264
+ printf "Please install Node.js first.\n"
265
+ return 1
266
+ fi
267
+ elif [ "$runtime" = "python" ]; then
268
+ if ! command -v python3 &> /dev/null && ! command -v python &> /dev/null; then
269
+ printf "${RED}Error: 'python' command is not installed${NC}\n"
270
+ printf "Please install Python first.\n"
271
+ return 1
272
+ fi
273
+ fi
274
+ return 0
275
+ }
276
+
277
+ # Zip command handler - creates local zip files
278
+ handle_zip() {
279
+ local runtime=""
280
+ local packages=""
281
+
282
+ # Parse runtime flag
283
+ case "${1:-}" in
284
+ --nodejs|--node|-n)
285
+ runtime="nodejs"
286
+ shift
287
+ ;;
288
+ --python|--py|-p)
289
+ runtime="python"
290
+ shift
291
+ ;;
292
+ --runtime=*)
293
+ runtime="${1#*=}"
294
+ case "$runtime" in
295
+ nodejs)
296
+ shift
297
+ ;;
298
+ python)
299
+ shift
300
+ ;;
301
+ *)
302
+ printf "${RED}Error: Invalid runtime specified: $runtime${NC}\n"
303
+ printf "Valid runtimes: nodejs, python\n"
304
+ exit 1
305
+ ;;
306
+ esac
307
+ ;;
308
+ --runtime)
309
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
310
+ runtime="$2"
311
+ shift 2
312
+ case "$runtime" in
313
+ nodejs|python)
314
+ ;;
315
+ *)
316
+ printf "${RED}Error: Invalid runtime specified: $runtime${NC}\n"
317
+ printf "Valid runtimes: nodejs, python\n"
318
+ exit 1
319
+ ;;
320
+ esac
321
+ else
322
+ printf "${RED}Error: --runtime requires an argument${NC}\n"
323
+ printf "Example: --runtime=nodejs or --runtime python\n"
324
+ exit 1
325
+ fi
326
+ ;;
327
+ *)
328
+ printf "${RED}Error: Missing or invalid runtime specification${NC}\n"
329
+ printf "Use --nodejs, --node, -n for Node.js or --python, --py, -p for Python\n"
330
+ printf "Or use --runtime=nodejs or --runtime=python\n"
331
+ exit 1
332
+ ;;
333
+ esac
334
+
335
+ # Get packages as first positional argument
336
+ if [[ -n "${1:-}" && "${1:-}" != -* ]]; then
337
+ packages="$1"
338
+ shift
339
+ else
340
+ printf "${RED}Error: Missing packages argument${NC}\n"
341
+ printf "Usage: aws-lambda-layer zip --nodejs <packages> [options]\n"
342
+ printf "Example: aws-lambda-layer zip --nodejs express,axios\n"
343
+ exit 1
344
+ fi
345
+
346
+ # Check dependencies
347
+ if ! check_dependencies "$runtime"; then
348
+ exit 1
349
+ fi
350
+
351
+ # Create output directory if it doesn't exist
352
+ local output_dir="output"
353
+ mkdir -p "$output_dir"
354
+
355
+ # Save current directory and change to output directory
356
+ local current_dir=$(pwd)
357
+ cd "$output_dir"
358
+
359
+ # Pass arguments to the appropriate script with -i
360
+ if [ "$runtime" = "nodejs" ]; then
361
+ if [ ! -f "$NODE_SCRIPT" ]; then
362
+ cd "$current_dir"
363
+ printf "${RED}Error: Node.js script not found at $NODE_SCRIPT${NC}\n"
364
+ printf "Please run scripts/install.sh first\n"
365
+ exit 1
366
+ fi
367
+ printf "${BLUE}Creating Node.js Lambda layer (local zip)...${NC}\n"
368
+ bash "$NODE_SCRIPT" -i "$packages" "$@"
369
+ local exit_code=$?
370
+ cd "$current_dir"
371
+ exit $exit_code
372
+ elif [ "$runtime" = "python" ]; then
373
+ if [ ! -f "$PYTHON_SCRIPT" ]; then
374
+ cd "$current_dir"
375
+ printf "${RED}Error: Python script not found at $PYTHON_SCRIPT${NC}\n"
376
+ printf "Please run scripts/install.sh first\n"
377
+ exit 1
378
+ fi
379
+ printf "${BLUE}Creating Python Lambda layer (local zip)...${NC}\n"
380
+ bash "$PYTHON_SCRIPT" -i "$packages" "$@"
381
+ local exit_code=$?
382
+ cd "$current_dir"
383
+ exit $exit_code
384
+ fi
385
+ }
386
+
387
+ # Publish command handler - publishes layer directly to AWS
388
+ handle_publish() {
389
+ local runtime=""
390
+ local description=""
391
+ local layer_name=""
392
+ local packages=""
393
+ local profile=""
394
+ local region=""
395
+ local args=()
396
+
397
+ # Parse publish-specific arguments first
398
+ local new_args=()
399
+ while [[ $# -gt 0 ]]; do
400
+ case "$1" in
401
+ --description)
402
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
403
+ description="$2"
404
+ shift 2
405
+ else
406
+ printf "${RED}Error: --description requires an argument${NC}\n"
407
+ exit 1
408
+ fi
409
+ ;;
410
+ --description=*)
411
+ description="${1#*=}"
412
+ shift
413
+ ;;
414
+ --name)
415
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
416
+ layer_name="$2"
417
+ shift 2
418
+ else
419
+ printf "${RED}Error: --name requires an argument${NC}\n"
420
+ exit 1
421
+ fi
422
+ ;;
423
+ --name=*)
424
+ layer_name="${1#*=}"
425
+ shift
426
+ ;;
427
+ --profile)
428
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
429
+ profile="$2"
430
+ shift 2
431
+ else
432
+ printf "${RED}Error: --profile requires an argument${NC}\n"
433
+ exit 1
434
+ fi
435
+ ;;
436
+ --profile=*)
437
+ profile="${1#*=}"
438
+ shift
439
+ ;;
440
+ --region)
441
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
442
+ region="$2"
443
+ shift 2
444
+ else
445
+ printf "${RED}Error: --region requires an argument${NC}\n"
446
+ exit 1
447
+ fi
448
+ ;;
449
+ --region=*)
450
+ region="${1#*=}"
451
+ shift
452
+ ;;
453
+ *)
454
+ new_args+=("$1")
455
+ shift
456
+ ;;
457
+ esac
458
+ done
459
+
460
+ # Parse runtime flag from remaining arguments
461
+ set -- "${new_args[@]}"
462
+ case "${1:-}" in
463
+ --nodejs|--node|-n)
464
+ runtime="nodejs"
465
+ shift
466
+ ;;
467
+ --python|--py|-p)
468
+ runtime="python"
469
+ shift
470
+ ;;
471
+ --runtime=*)
472
+ runtime="${1#*=}"
473
+ case "$runtime" in
474
+ nodejs)
475
+ shift
476
+ ;;
477
+ python)
478
+ shift
479
+ ;;
480
+ *)
481
+ printf "${RED}Error: Invalid runtime specified: $runtime${NC}\n"
482
+ printf "Valid runtimes: nodejs, python\n"
483
+ exit 1
484
+ ;;
485
+ esac
486
+ ;;
487
+ --runtime)
488
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
489
+ runtime="$2"
490
+ shift 2
491
+ case "$runtime" in
492
+ nodejs|python)
493
+ ;;
494
+ *)
495
+ printf "${RED}Error: Invalid runtime specified: $runtime${NC}\n"
496
+ printf "Valid runtimes: nodejs, python\n"
497
+ exit 1
498
+ ;;
499
+ esac
500
+ else
501
+ printf "${RED}Error: --runtime requires an argument${NC}\n"
502
+ printf "Example: --runtime=nodejs or --runtime python\n"
503
+ exit 1
504
+ fi
505
+ ;;
506
+ *)
507
+ printf "${RED}Error: Missing or invalid runtime specification${NC}\n"
508
+ printf "Use --nodejs, --node, -n for Node.js or --python, --py, -p for Python\n"
509
+ printf "Or use --runtime=nodejs or --runtime=python\n"
510
+ exit 1
511
+ ;;
512
+ esac
513
+
514
+ # Get packages as first positional argument
515
+ if [[ -n "${1:-}" && "${1:-}" != -* ]]; then
516
+ packages="$1"
517
+ shift
518
+ else
519
+ printf "${RED}Error: Missing packages argument${NC}\n"
520
+ printf "Usage: aws-lambda-layer publish --nodejs <packages> [options]\n"
521
+ printf "Example: aws-lambda-layer publish --nodejs express,axios --description \"My layer\"\n"
522
+ exit 1
523
+ fi
524
+
525
+ # Check dependencies
526
+ if ! check_dependencies "$runtime"; then
527
+ exit 1
528
+ fi
529
+
530
+ # Check AWS CLI and credentials
531
+ if ! check_aws_cli "$profile" "$region"; then
532
+ exit 1
533
+ fi
534
+
535
+ # Show AWS account info and wait for confirmation
536
+ get_aws_account_info "$profile" "$region"
537
+
538
+ # Create output directory if it doesn't exist
539
+ local output_dir="output"
540
+ mkdir -p "$output_dir"
541
+
542
+ # Save current directory and change to output directory
543
+ local current_dir=$(pwd)
544
+ cd "$output_dir"
545
+
546
+ printf "${BLUE}Building Lambda layer in output directory...${NC}\n"
547
+
548
+ # Build the layer using the appropriate script
549
+ local build_args=("-i" "$packages")
550
+ if [ -n "$layer_name" ]; then
551
+ build_args+=("--name" "${layer_name}.zip")
552
+ fi
553
+
554
+ # Add remaining arguments
555
+ for arg in "$@"; do
556
+ build_args+=("$arg")
557
+ done
558
+
559
+ local zip_file=""
560
+ if [ "$runtime" = "nodejs" ]; then
561
+ if [ ! -f "$NODE_SCRIPT" ]; then
562
+ cd "$current_dir"
563
+ printf "${RED}Error: Node.js script not found${NC}\n"
564
+ exit 1
565
+ fi
566
+ bash "$NODE_SCRIPT" "${build_args[@]}" 2>&1 | tee build.log
567
+
568
+ # Check if build script succeeded
569
+ if [ ${PIPESTATUS[0]} -ne 0 ]; then
570
+ cd "$current_dir"
571
+ printf "${RED}Error: Node.js layer build failed${NC}\n"
572
+ printf "Check build log: $output_dir/build.log\n"
573
+ exit 1
574
+ fi
575
+
576
+ # Extract zip file name from build output (just the filename, not full path)
577
+ zip_file=$(grep -o "File: .*\.zip" "build.log" | cut -d' ' -f2 | tail -1)
578
+ if [ -n "$zip_file" ]; then
579
+ zip_file=$(basename "$zip_file")
580
+ else
581
+ # Try to find zip file in current directory
582
+ zip_file=$(find . -maxdepth 1 -name "*.zip" -type f | head -1 | sed 's|^\./||')
583
+ fi
584
+
585
+ # Extract Node.js version for compatible runtimes
586
+ local node_version=$(grep -o "Node.js version: [0-9.]*" "build.log" | cut -d' ' -f3 | tail -1)
587
+ if [ -z "$node_version" ]; then
588
+ node_version="24"
589
+ fi
590
+ local compatible_runtimes="nodejs${node_version}.x"
591
+
592
+ elif [ "$runtime" = "python" ]; then
593
+ if [ ! -f "$PYTHON_SCRIPT" ]; then
594
+ cd "$current_dir"
595
+ printf "${RED}Error: Python script not found${NC}\n"
596
+ exit 1
597
+ fi
598
+ bash "$PYTHON_SCRIPT" "${build_args[@]}" 2>&1 | tee build.log
599
+
600
+ # Check if build script succeeded
601
+ if [ ${PIPESTATUS[0]} -ne 0 ]; then
602
+ cd "$current_dir"
603
+ printf "${RED}Error: Python layer build failed${NC}\n"
604
+ printf "Check build log: $output_dir/build.log\n"
605
+ exit 1
606
+ fi
607
+
608
+ # Extract zip file name from build output (just the filename, not full path)
609
+ zip_file=$(grep -o "File: .*\.zip" "build.log" | cut -d' ' -f2 | tail -1)
610
+ if [ -n "$zip_file" ]; then
611
+ zip_file=$(basename "$zip_file")
612
+ else
613
+ # Try to find zip file in current directory
614
+ zip_file=$(find . -maxdepth 1 -name "*.zip" -type f | head -1 | sed 's|^\./||')
615
+ fi
616
+
617
+ # Extract Python version for compatible runtimes
618
+ local python_version=$(grep -o "Python: [0-9.]*" "build.log" | cut -d' ' -f2 | tail -1)
619
+ if [ -z "$python_version" ]; then
620
+ python_version="3.14"
621
+ fi
622
+ local compatible_runtimes="python${python_version}"
623
+ fi
624
+
625
+ # Check if zip file was created
626
+ if [ -z "$zip_file" ] || [ ! -f "$zip_file" ]; then
627
+ cd "$current_dir"
628
+ printf "${RED}Error: Failed to create layer zip file${NC}\n"
629
+ printf "Check build log: $output_dir/build.log\n"
630
+ exit 1
631
+ fi
632
+
633
+ # Extract package info for layer naming and description
634
+ local packages_info=""
635
+ if [ "$runtime" = "nodejs" ]; then
636
+ packages_info=$(grep -o "Installed packages: .*" "build.log" | cut -d' ' -f3- | tail -1)
637
+ elif [ "$runtime" = "python" ]; then
638
+ packages_info=$(grep -o "Installed packages: .*" "build.log" | cut -d' ' -f3- | tail -1)
639
+ fi
640
+
641
+ # Determine layer name - use just the first package name if not specified
642
+ if [ -z "$layer_name" ]; then
643
+ # Extract first package name from packages list
644
+ local first_package=$(echo "$packages" | cut -d',' -f1)
645
+ # Remove version specifiers to get just the package name
646
+ if [ "$runtime" = "nodejs" ]; then
647
+ # For Node.js: remove @version, handle scoped packages like @aws-sdk/client-s3
648
+ layer_name=$(echo "$first_package" | sed 's/@[0-9^~<>=].*$//' | sed 's/^@//' | tr '/' '-')
649
+ else
650
+ # For Python: remove ==version, >=version, etc.
651
+ layer_name=$(echo "$first_package" | sed 's/[=<>~!].*$//')
652
+ fi
653
+ fi
654
+
655
+ # Determine description - use package name with version
656
+ local final_description=""
657
+ if [ -n "$description" ]; then
658
+ final_description="$description - $packages_info"
659
+ else
660
+ final_description="$packages_info"
661
+ fi
662
+
663
+ # Limit description length (AWS limit is 256 characters)
664
+ if [ ${#final_description} -gt 256 ]; then
665
+ final_description="${final_description:0:253}..."
666
+ fi
667
+
668
+ printf "\n${CYAN}Publishing Lambda layer to AWS...${NC}\n"
669
+ printf " Layer name: ${GREEN}$layer_name${NC}\n"
670
+ printf " Description: ${GREEN}$final_description${NC}\n"
671
+ printf " Compatible runtimes: ${GREEN}$compatible_runtimes${NC}\n"
672
+ printf " Zip file: ${GREEN}$zip_file${NC}\n\n"
673
+
674
+ # Confirm publish
675
+ read -p "Do you want to publish this layer? [Y/n]: " confirm_publish
676
+ confirm_publish=${confirm_publish:-Y}
677
+
678
+ case "$confirm_publish" in
679
+ [Yy]|[Yy][Ee][Ss])
680
+ ;;
681
+ *)
682
+ printf "${YELLOW}Publishing cancelled.${NC}\n"
683
+ exit 0
684
+ ;;
685
+ esac
686
+
687
+ # Build AWS CLI options for publish
688
+ local publish_aws_opts=()
689
+ if [ -n "$profile" ]; then
690
+ publish_aws_opts+=("--profile" "$profile")
691
+ fi
692
+ if [ -n "$region" ]; then
693
+ publish_aws_opts+=("--region" "$region")
694
+ fi
695
+
696
+ # Publish the layer
697
+ printf "${YELLOW}Executing: aws lambda publish-layer-version${NC}\n"
698
+ if [ -n "$profile" ]; then
699
+ printf " --profile \"$profile\"\n"
700
+ fi
701
+ if [ -n "$region" ]; then
702
+ printf " --region \"$region\"\n"
703
+ fi
704
+ printf " --layer-name \"$layer_name\"\n"
705
+ printf " --description \"$final_description\"\n"
706
+ printf " --zip-file \"fileb://$(convert_path "$current_dir/$output_dir/$zip_file")\"\n"
707
+ printf " --compatible-runtimes \"$compatible_runtimes\"\n\n"
708
+
709
+ # Run AWS CLI command and capture output and exit code
710
+ aws ${publish_aws_opts[@]+"${publish_aws_opts[@]}"} lambda publish-layer-version \
711
+ --layer-name "$layer_name" \
712
+ --description "$final_description" \
713
+ --zip-file "fileb://$(convert_path "$current_dir/$output_dir/$zip_file")" \
714
+ --compatible-runtimes "$compatible_runtimes" \
715
+ --query '[LayerVersionArn, Version, Description]' \
716
+ --output table 2>&1 | tee publish.log
717
+
718
+ local aws_exit_code=${PIPESTATUS[0]}
719
+
720
+ if [ $aws_exit_code -eq 0 ]; then
721
+ printf "\n${GREEN}✅ Lambda layer published successfully!${NC}\n"
722
+
723
+ # Extract layer ARN from log
724
+ local layer_arn=$(grep -o "arn:aws:lambda:[^:]*:[0-9]*:layer:$layer_name:[0-9]*" "publish.log" | tail -1)
725
+ if [ -n "$layer_arn" ]; then
726
+ printf " Layer ARN: ${CYAN}$layer_arn${NC}\n"
727
+
728
+ # Show usage example
729
+ printf "\n${YELLOW}Usage example in Lambda function:${NC}\n"
730
+ printf " layers:\n"
731
+ printf " - $layer_arn\n"
732
+ printf "\n${YELLOW}To attach to existing Lambda function:${NC}\n"
733
+ printf " aws lambda update-function-configuration \\\\\n"
734
+ printf " --function-name YOUR_FUNCTION_NAME \\\\\n"
735
+ printf " --layers $layer_arn\n"
736
+ fi
737
+ else
738
+ cd "$current_dir"
739
+ printf "\n${RED}❌ Failed to publish Lambda layer${NC}\n"
740
+ printf "Check publish log: $output_dir/publish.log\n"
741
+ printf "\n${YELLOW}Common issues:${NC}\n"
742
+ printf "1. IAM permissions missing (lambda:PublishLayerVersion)\n"
743
+ printf "2. Layer name already exists (try different --name)\n"
744
+ printf "3. Zip file too large (max 50MB for direct upload)\n"
745
+ printf "4. Network connectivity issues\n"
746
+ exit 1
747
+ fi
748
+
749
+ # Return to original directory
750
+ cd "$current_dir"
751
+
752
+ printf "\n${GREEN}✅ All done! Layer is now available in your AWS account.${NC}\n"
753
+ printf "📁 Zip file saved: ${CYAN}$output_dir/$zip_file${NC}\n"
754
+ }
755
+
756
+ # Function to convert file paths for compatibility across environments
757
+ convert_path() {
758
+ local input_path="$1"
759
+ case "$(uname -s)" in
760
+ Linux)
761
+ if grep -qEi "microsoft|wsl" /proc/version &>/dev/null; then
762
+ # WSL detected, no conversion needed
763
+ echo "$input_path"
764
+ else
765
+ # Native Linux
766
+ echo "$input_path"
767
+ fi
768
+ ;;
769
+ Darwin)
770
+ # macOS
771
+ echo "$input_path"
772
+ ;;
773
+ CYGWIN*|MINGW*|MSYS*)
774
+ # Git Bash or Cygwin on Windows
775
+ # Prefer C:/ style to avoid fileb:// URI parsing issues with backslashes
776
+ if command -v cygpath >/dev/null 2>&1; then
777
+ cygpath -m "$input_path"
778
+ else
779
+ echo "$input_path"
780
+ fi
781
+ ;;
782
+ *)
783
+ # Default case (no conversion)
784
+ echo "$input_path"
785
+ ;;
786
+ esac
787
+ }
788
+
789
+ # Main command parsing
790
+ main() {
791
+ if [ $# -eq 0 ]; then
792
+ show_help
793
+ exit 0
794
+ fi
795
+
796
+ case "$1" in
797
+ zip)
798
+ shift
799
+ handle_zip "$@"
800
+ ;;
801
+ publish)
802
+ shift
803
+ handle_publish "$@"
804
+ ;;
805
+ help|--help|-h)
806
+ show_help
807
+ ;;
808
+ --version|-v)
809
+ show_version
810
+ ;;
811
+ *)
812
+ printf "${RED}Error: Unknown command '$1'${NC}\n"
813
+ printf "Available commands: zip, publish\n"
814
+ printf "Use --help for more information\n"
815
+ exit 1
816
+ ;;
817
+ esac
818
+ }
819
+
820
+ # Run main function with all arguments
821
+ main "$@"