aws-lambda-layer-cli 2.0.4 → 2.2.0

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.
package/README.md CHANGED
@@ -4,10 +4,10 @@ A command-line tool for creating and publishing AWS Lambda layers for Node.js an
4
4
 
5
5
  ## Features
6
6
 
7
- - Create and publish Lambda layers for Node.js (npm) and Python (uv/pip)
8
- - Automatic version management and smart naming
9
- - Direct publishing to AWS with IAM credentials and region support
10
- - Support for multiple packages in a single layer
7
+ - **Effortless Publishing**: Create and publish Node.js and Python layers in a single command
8
+ - **Smart Compatibility**: Auto-selects the right Linux binaries for Amazon Linux 2 or 2023
9
+ - **Cross-Architecture**: Native support for `x86_64` and `arm64` builds
10
+ - **Auto-Versioning**: Automatically handles layer naming and version increments
11
11
 
12
12
  ## Installation
13
13
 
@@ -46,13 +46,14 @@ aws-lambda-layer-cli <command> [options]
46
46
  |--------|-------------|
47
47
  | `--nodejs, -n <pkgs>` | Create Node.js layer (comma-separated packages) |
48
48
  | `--python, -p <pkgs>` | Create Python layer (comma-separated packages) |
49
+ | `--wheel, -w <file>` | Use with `--python` to create layer from `.whl` file |
49
50
  | `--name` | Custom layer name |
50
51
  | `--description` | Layer description (publish only) |
51
52
  | `--profile` | AWS CLI profile (publish only) |
52
53
  | `--region` | AWS region (publish only) |
54
+ | `--architecture, -a` | Target architecture (`x86_64` or `arm64`) |
53
55
  | `--node-version` | Node.js version (default: 24) |
54
56
  | `--python-version` | Python version (default: 3.14) |
55
- | `--no-uv` | Use pip/venv instead of uv |
56
57
  | `-v, --version` | Show version |
57
58
 
58
59
  ## Examples
@@ -68,12 +69,26 @@ aws-lambda-layer-cli publish --nodejs lodash --profile prod --region us-east-1 -
68
69
 
69
70
  ### Python
70
71
  ```bash
71
- # Create local zip with specific python version
72
- aws-lambda-layer-cli zip --python numpy==1.26.0,pandas --python-version 3.12
72
+ # Create local zip with specific python version and architecture
73
+ aws-lambda-layer-cli zip --python numpy==1.26.0,pandas --python-version 3.12 --architecture arm64
73
74
 
74
- # Publish to AWS
75
- aws-lambda-layer-cli publish --python requests --name web-layer
75
+ # Publish to AWS for ARM64 architecture
76
+ aws-lambda-layer-cli publish --python requests --name web-layer --architecture arm64
76
77
  ```
78
+ > **Note**: This tool automatically selects the optimal platform based on the Python version:
79
+ > - **Python 3.12+ (Amazon Linux 2023)**: Targets `manylinux_2_28` (GLIBC 2.28+)
80
+ > - **Python 3.11- (Amazon Linux 2)**: Targets `manylinux2014` (GLIBC 2.17+)
81
+
82
+ ### Wheel File
83
+ The tool auto-detects Python version and architecture from the wheel filename.
84
+ ```bash
85
+ # Create local zip from wheel (preferred syntax)
86
+ aws-lambda-layer-cli zip --python --wheel numpy-2.4.1-cp313-cp313-manylinux.whl
87
+
88
+ # Publish directly from wheel
89
+ aws-lambda-layer-cli publish --python --wheel pandas-2.1.0-cp311-...-x86_64.whl
90
+ ```
91
+ > **Note**: For wheels, arguments like `--python-version` or `--architecture` are checked against the wheel metadata. If they conflict, the tool will error to prevent incompatibility.
77
92
 
78
93
  ## Shell Completion
79
94
 
@@ -13,7 +13,7 @@ _aws_lambda_layer_cli() {
13
13
  local common_opts="--name -h --help"
14
14
  local publish_opts="--description --layer-name --profile --region"
15
15
  local node_opts="--node-version"
16
- local python_opts="--python-version --no-uv"
16
+ local python_opts="--python-version --platform"
17
17
 
18
18
  case ${cword} in
19
19
  1)
@@ -40,7 +40,7 @@ _aws-lambda-layer-cli() {
40
40
 
41
41
  python_opts=(
42
42
  '--python-version:version:'
43
- '--no-uv'
43
+ '--platform:platform:'
44
44
  )
45
45
 
46
46
  _arguments -C \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-lambda-layer-cli",
3
- "version": "2.0.4",
3
+ "version": "2.2.0",
4
4
  "description": "CLI tool for creating and publishing AWS Lambda layers for Node.js and Python.",
5
5
  "keywords": [
6
6
  "aws",
@@ -41,13 +41,19 @@ else
41
41
  PYTHON_SCRIPT="$INSTALL_DIR/create_python_layer.sh"
42
42
  fi
43
43
 
44
+ if [ -f "$SCRIPT_DIR/create_wheel_layer.sh" ]; then
45
+ WHEEL_SCRIPT="$SCRIPT_DIR/create_wheel_layer.sh"
46
+ else
47
+ WHEEL_SCRIPT="$INSTALL_DIR/create_wheel_layer.sh"
48
+ fi
49
+
44
50
  BIN_DIR="/usr/local/bin"
45
51
  COMPLETION_DIR="/etc/bash_completion.d"
46
52
 
47
53
  # Show help
48
54
  show_help() {
49
55
  local version_file="$SCRIPT_DIR/VERSION.txt"
50
- local version="2.0.4"
56
+ local version="2.2.0"
51
57
  if [ -f "$version_file" ]; then
52
58
  version=$(cat "$version_file")
53
59
  fi
@@ -56,8 +62,10 @@ show_help() {
56
62
  printf "${BLUE}Usage:${NC}\n"
57
63
  printf " aws-lambda-layer-cli ${GREEN}zip${NC} ${YELLOW}--nodejs${NC} <packages> [options]\n"
58
64
  printf " aws-lambda-layer-cli ${GREEN}zip${NC} ${YELLOW}--python${NC} <packages> [options]\n"
65
+ printf " aws-lambda-layer-cli ${GREEN}zip${NC} ${YELLOW}--python --wheel${NC} <wheel_file> [options]\n"
59
66
  printf " aws-lambda-layer-cli ${GREEN}publish${NC} ${YELLOW}--nodejs${NC} <packages> [options]\n"
60
67
  printf " aws-lambda-layer-cli ${GREEN}publish${NC} ${YELLOW}--python${NC} <packages> [options]\n"
68
+ printf " aws-lambda-layer-cli ${GREEN}publish${NC} ${YELLOW}--python --wheel${NC} <wheel_file> [options]\n"
61
69
  printf " aws-lambda-layer-cli ${GREEN}help${NC}\n"
62
70
  printf " aws-lambda-layer-cli [options]\n\n"
63
71
 
@@ -72,8 +80,11 @@ show_help() {
72
80
  printf " ${YELLOW}--nodejs, --node, -n${NC} Create a Node.js Lambda layer\n"
73
81
  printf " ${YELLOW}--python, --py, -p${NC} Create a Python Lambda layer\n"
74
82
  printf " ${YELLOW}--runtime=RUNTIME${NC} Specify runtime (nodejs or python)\n"
83
+ printf "${BLUE}Source Options (Python only):${NC}\n"
84
+ printf " ${YELLOW}--wheel, -w${NC} Create layer from existing Wheel file\n"
75
85
  printf "${BLUE}Arguments:${NC}\n"
76
- printf " <packages> Comma-separated list of packages with optional versions (required)\n"
86
+ printf " <packages> Comma-separated list of packages\n"
87
+ printf " <wheel_file> Path to a local .whl file (use with --python --wheel)\n"
77
88
 
78
89
  printf "${BLUE}Common Options:${NC}\n"
79
90
  printf " ${YELLOW}--name${NC} Name for the output zip file / layer name\n"
@@ -85,7 +96,7 @@ show_help() {
85
96
  printf " ${YELLOW}--version, -v${NC} Show tool version information\n"
86
97
  printf " ${YELLOW}--node-version${NC} Node.js version (default: 24)\n"
87
98
  printf " ${YELLOW}--python-version${NC} Python version (default: 3.14)\n"
88
- printf " ${YELLOW}--no-uv${NC} Use pip/venv instead of uv\n\n"
99
+ printf " ${YELLOW}--architecture, -a${NC} Target architecture (x86_64, arm64)\n\n"
89
100
 
90
101
  printf "${MAGENTA}${UNDERLINE}Package Version Examples:${NC}\n"
91
102
  printf " Node.js: express@^4.0.0, lodash@~4.17.0, axios@>=1.6.0\n"
@@ -113,7 +124,7 @@ show_version() {
113
124
  echo "v$version"
114
125
  else
115
126
  # Fallback if VERSION file is missing (e.g. during development or if moved)
116
- echo "v2.0.4"
127
+ echo "v2.2.0"
117
128
  fi
118
129
  }
119
130
 
@@ -248,25 +259,6 @@ get_aws_account_info() {
248
259
  esac
249
260
  }
250
261
 
251
- # Determine compatible runtimes for AWS
252
- get_compatible_runtimes() {
253
- local runtime="$1"
254
- local version="2.0.4"
255
-
256
- case "$runtime" in
257
- nodejs)
258
- echo "nodejs${version}.x"
259
- ;;
260
- python)
261
- # AWS Lambda uses format like python3.14, not python3.14.x
262
- echo "python${version}"
263
- ;;
264
- *)
265
- echo ""
266
- ;;
267
- esac
268
- }
269
-
270
262
  # Check dependencies
271
263
  check_dependencies() {
272
264
  local runtime="$1"
@@ -316,6 +308,7 @@ check_dependencies() {
316
308
  # Zip command handler - creates local zip files
317
309
  handle_zip() {
318
310
  local runtime=""
311
+ local is_wheel=false
319
312
  local packages=""
320
313
 
321
314
  # Parse runtime flag
@@ -327,6 +320,10 @@ handle_zip() {
327
320
  --python|--py|-p)
328
321
  runtime="python"
329
322
  shift
323
+ if [[ "${1:-}" == "--wheel" || "${1:-}" == "-w" || "${1:-}" == "--whl" ]]; then
324
+ is_wheel=true
325
+ shift
326
+ fi
330
327
  ;;
331
328
  --runtime=*)
332
329
  runtime="${1#*=}"
@@ -395,7 +392,7 @@ handle_zip() {
395
392
  local current_dir=$(pwd)
396
393
  cd "$output_dir"
397
394
 
398
- # Pass arguments to the appropriate script with -i
395
+ # Pass arguments to the appropriate script
399
396
  if [ "$runtime" = "nodejs" ]; then
400
397
  if [ ! -f "$NODE_SCRIPT" ]; then
401
398
  cd "$current_dir"
@@ -409,23 +406,37 @@ handle_zip() {
409
406
  cd "$current_dir"
410
407
  exit $exit_code
411
408
  elif [ "$runtime" = "python" ]; then
412
- if [ ! -f "$PYTHON_SCRIPT" ]; then
409
+ if [ "$is_wheel" = true ]; then
410
+ if [ ! -f "$WHEEL_SCRIPT" ]; then
411
+ cd "$current_dir"
412
+ printf "${RED}Error: Wheel script not found at $WHEEL_SCRIPT${NC}\n"
413
+ exit 1
414
+ fi
415
+ printf "${BLUE}Creating Lambda layer from Wheel...${NC}\n"
416
+ bash "$WHEEL_SCRIPT" -w "$packages" "$@"
417
+ local exit_code=$?
413
418
  cd "$current_dir"
414
- printf "${RED}Error: Python script not found at $PYTHON_SCRIPT${NC}\n"
415
- printf "Please run scripts/install.sh first\n"
416
- exit 1
419
+ exit $exit_code
420
+ else
421
+ if [ ! -f "$PYTHON_SCRIPT" ]; then
422
+ cd "$current_dir"
423
+ printf "${RED}Error: Python script not found at $PYTHON_SCRIPT${NC}\n"
424
+ printf "Please run scripts/install.sh first\n"
425
+ exit 1
426
+ fi
427
+ printf "${BLUE}Creating Python Lambda layer (local zip)...${NC}\n"
428
+ bash "$PYTHON_SCRIPT" -i "$packages" "$@"
429
+ local exit_code=$?
430
+ cd "$current_dir"
431
+ exit $exit_code
417
432
  fi
418
- printf "${BLUE}Creating Python Lambda layer (local zip)...${NC}\n"
419
- bash "$PYTHON_SCRIPT" -i "$packages" "$@"
420
- local exit_code=$?
421
- cd "$current_dir"
422
- exit $exit_code
423
433
  fi
424
434
  }
425
435
 
426
436
  # Publish command handler - publishes layer directly to AWS
427
437
  handle_publish() {
428
438
  local runtime=""
439
+ local is_wheel=false
429
440
  local description=""
430
441
  local layer_name=""
431
442
  local packages=""
@@ -511,6 +522,11 @@ handle_publish() {
511
522
  --python|--py|-p)
512
523
  runtime="python"
513
524
  shift
525
+ # Check if the next argument is --wheel
526
+ if [[ "${1:-}" == "--wheel" || "${1:-}" == "-w" || "${1:-}" == "--whl" ]]; then
527
+ is_wheel=true
528
+ shift
529
+ fi
514
530
  ;;
515
531
  --runtime=*)
516
532
  runtime="${1#*=}"
@@ -543,14 +559,14 @@ handle_publish() {
543
559
  esac
544
560
  else
545
561
  printf "${RED}Error: --runtime requires an argument${NC}\n"
546
- printf "Example: --runtime=nodejs or --runtime python\n"
562
+ printf "Example: --runtime=nodejs, --runtime python\n"
547
563
  exit 1
548
564
  fi
549
565
  ;;
550
566
  *)
551
567
  printf "${RED}Error: Missing or invalid runtime specification${NC}\n"
552
- printf "Use --nodejs, --node, -n for Node.js or --python, --py, -p for Python\n"
553
- printf "Or use --runtime=nodejs or --runtime=python\n"
568
+ printf "Use --nodejs (-n) or --python (-p)\n"
569
+ printf "Or use --runtime=nodejs, --runtime=python\n"
554
570
  exit 1
555
571
  ;;
556
572
  esac
@@ -560,7 +576,7 @@ handle_publish() {
560
576
  packages="$1"
561
577
  shift
562
578
  else
563
- printf "${RED}Error: Missing packages argument${NC}\n"
579
+ printf "${RED}Error: Missing argument (packages or wheel file)${NC}\n"
564
580
  printf "Usage: aws-lambda-layer publish --nodejs <packages> [options]\n"
565
581
  printf "Example: aws-lambda-layer publish --nodejs express,axios --description \"My layer\"\n"
566
582
  exit 1
@@ -590,7 +606,13 @@ handle_publish() {
590
606
  printf "${BLUE}Building Lambda layer in output directory...${NC}\n"
591
607
 
592
608
  # Build the layer using the appropriate script
593
- local build_args=("-i" "$packages")
609
+ local build_args=()
610
+ if [ "$is_wheel" = true ]; then
611
+ build_args+=("-w" "$packages")
612
+ else
613
+ build_args+=("-i" "$packages")
614
+ fi
615
+
594
616
  if [ -n "$layer_name" ]; then
595
617
  build_args+=("--name" "${layer_name}.zip")
596
618
  fi
@@ -634,36 +656,75 @@ handle_publish() {
634
656
  local compatible_runtimes="nodejs${node_version}.x"
635
657
 
636
658
  elif [ "$runtime" = "python" ]; then
637
- if [ ! -f "$PYTHON_SCRIPT" ]; then
638
- cd "$current_dir"
639
- printf "${RED}Error: Python script not found${NC}\n"
640
- exit 1
641
- fi
642
- bash "$PYTHON_SCRIPT" "${build_args[@]}" 2>&1 | tee build.log
643
-
644
- # Check if build script succeeded
645
- if [ ${PIPESTATUS[0]} -ne 0 ]; then
646
- cd "$current_dir"
647
- printf "${RED}Error: Python layer build failed${NC}\n"
648
- printf "Check build log: $output_dir/build.log\n"
649
- exit 1
650
- fi
651
-
652
- # Extract zip file name from build output (just the filename, not full path)
653
- zip_file=$(grep -o "File: .*\.zip" "build.log" | cut -d' ' -f2 | tail -1)
654
- if [ -n "$zip_file" ]; then
655
- zip_file=$(basename "$zip_file")
659
+ if [ "$is_wheel" = true ]; then
660
+ if [ ! -f "$WHEEL_SCRIPT" ]; then
661
+ cd "$current_dir"
662
+ printf "${RED}Error: Wheel script not found${NC}\n"
663
+ exit 1
664
+ fi
665
+ bash "$WHEEL_SCRIPT" "${build_args[@]}" 2>&1 | tee build.log
666
+
667
+ if [ ${PIPESTATUS[0]} -ne 0 ]; then
668
+ cd "$current_dir"
669
+ printf "${RED}Error: Wheel layer build failed${NC}\n"
670
+ printf "Check build log: $output_dir/build.log\n"
671
+ exit 1
672
+ fi
673
+
674
+ zip_file=$(grep -o "File: .*\.zip" "build.log" | cut -d' ' -f2 | tail -1)
675
+ if [ -n "$zip_file" ]; then
676
+ zip_file=$(basename "$zip_file")
677
+ else
678
+ zip_file=$(find . -maxdepth 1 -name "*.zip" -type f | head -1 | sed 's|^\./||')
679
+ fi
680
+
681
+ local python_version=$(grep "Detected Python: " "build.log" | awk -F': ' '{print $2}' | tail -1)
682
+ if [ -z "$python_version" ]; then python_version="3.12"; fi
683
+ compatible_runtimes="python${python_version}"
684
+
685
+ local detected_arch=$(grep "Detected Architecture: " "build.log" | awk -F': ' '{print $2}' | tail -1)
686
+ if [ -n "$detected_arch" ]; then
687
+ if [ "$detected_arch" = "aarch64" ]; then compatible_architectures="arm64";
688
+ elif [ "$detected_arch" = "x86_64" ]; then compatible_architectures="x86_64";
689
+ fi
690
+ fi
691
+
692
+ if [ -n "${compatible_architectures:-}" ]; then
693
+ printf "${CYAN}Info: Using detected architecture: $compatible_architectures${NC}\n"
694
+ fi
656
695
  else
657
- # Try to find zip file in current directory
658
- zip_file=$(find . -maxdepth 1 -name "*.zip" -type f | head -1 | sed 's|^\./||')
659
- fi
660
-
661
- # Extract Python version for compatible runtimes
662
- local python_version=$(grep -o "Python: [0-9.]*" "build.log" | cut -d' ' -f2 | tail -1)
663
- if [ -z "$python_version" ]; then
664
- python_version="3.14"
696
+ if [ ! -f "$PYTHON_SCRIPT" ]; then
697
+ cd "$current_dir"
698
+ printf "${RED}Error: Python script not found${NC}\n"
699
+ exit 1
700
+ fi
701
+ bash "$PYTHON_SCRIPT" "${build_args[@]}" 2>&1 | tee build.log
702
+
703
+ # Check if build script succeeded
704
+ if [ ${PIPESTATUS[0]} -ne 0 ]; then
705
+ cd "$current_dir"
706
+ printf "${RED}Error: Python layer build failed${NC}\n"
707
+ printf "Check build log: $output_dir/build.log\n"
708
+ exit 1
709
+ fi
710
+
711
+ # Extract zip file name from build output (just the filename, not full path)
712
+ zip_file=$(grep -o "File: .*\.zip" "build.log" | cut -d' ' -f2 | tail -1)
713
+ if [ -n "$zip_file" ]; then
714
+ zip_file=$(basename "$zip_file")
715
+ else
716
+ # Try to find zip file in current directory
717
+ zip_file=$(find . -maxdepth 1 -name "*.zip" -type f | head -1 | sed 's|^\./||')
718
+ fi
719
+
720
+ # Extract Python version for compatible runtimes
721
+ # Pattern matches "Python version: X.Y" or "Python Version: X.Y"
722
+ local python_version=$(grep -i "Python.*version: [0-9.]*" "build.log" | awk '{print $NF}' | tail -1)
723
+ if [ -z "$python_version" ]; then
724
+ python_version="3.14"
725
+ fi
726
+ local compatible_runtimes="python${python_version}"
665
727
  fi
666
- local compatible_runtimes="python${python_version}"
667
728
  fi
668
729
 
669
730
  # Check if zip file was created
@@ -679,7 +740,11 @@ handle_publish() {
679
740
  if [ "$runtime" = "nodejs" ]; then
680
741
  packages_info=$(grep -o "Installed packages: .*" "build.log" | cut -d' ' -f3- | tail -1)
681
742
  elif [ "$runtime" = "python" ]; then
682
- packages_info=$(grep -o "Installed packages: .*" "build.log" | cut -d' ' -f3- | tail -1)
743
+ if [ "$is_wheel" = true ]; then
744
+ packages_info=$(basename "$packages")
745
+ else
746
+ packages_info=$(grep -o "Installed packages: .*" "build.log" | cut -d' ' -f3- | tail -1)
747
+ fi
683
748
  fi
684
749
 
685
750
  # Determine layer name - use just the first package name if not specified
@@ -692,7 +757,11 @@ handle_publish() {
692
757
  layer_name=$(echo "$first_package" | sed 's/@[0-9^~<>=].*$//' | sed 's/^@//' | tr '/' '-')
693
758
  else
694
759
  # For Python: remove ==version, >=version, etc.
695
- layer_name=$(echo "$first_package" | sed 's/[=<>~!].*$//')
760
+ if [ "$is_wheel" = true ]; then
761
+ layer_name=$(basename "$first_package" | cut -d'-' -f1)
762
+ else
763
+ layer_name=$(echo "$first_package" | sed 's/[=<>~!].*$//')
764
+ fi
696
765
  fi
697
766
  fi
698
767
 
@@ -748,16 +817,32 @@ handle_publish() {
748
817
  printf " --layer-name \"$layer_name\"\n"
749
818
  printf " --description \"$final_description\"\n"
750
819
  printf " --zip-file \"fileb://$(convert_path "$current_dir/$output_dir/$zip_file")\"\n"
751
- printf " --compatible-runtimes \"$compatible_runtimes\"\n\n"
820
+ printf " --compatible-runtimes \"$compatible_runtimes\"\n"
821
+ if [ -n "${compatible_architectures:-}" ]; then
822
+ printf " --compatible-architectures \"$compatible_architectures\"\n"
823
+ fi
824
+ printf "\n"
752
825
 
826
+ # Build AWS CLI command array
827
+ local publish_cmd=(aws)
828
+ if [ ${#publish_aws_opts[@]} -gt 0 ]; then
829
+ publish_cmd+=("${publish_aws_opts[@]}")
830
+ fi
831
+ publish_cmd+=(lambda publish-layer-version)
832
+ publish_cmd+=(--layer-name "$layer_name")
833
+ publish_cmd+=(--description "$final_description")
834
+ publish_cmd+=(--zip-file "fileb://$(convert_path "$current_dir/$output_dir/$zip_file")")
835
+ publish_cmd+=(--compatible-runtimes "$compatible_runtimes")
836
+
837
+ if [ -n "${compatible_architectures:-}" ]; then
838
+ publish_cmd+=(--compatible-architectures "$compatible_architectures")
839
+ fi
840
+
841
+ publish_cmd+=(--query '[LayerVersionArn, Version, Description]')
842
+ publish_cmd+=(--output table)
843
+
753
844
  # Run AWS CLI command and capture output and exit code
754
- aws ${publish_aws_opts[@]+"${publish_aws_opts[@]}"} lambda publish-layer-version \
755
- --layer-name "$layer_name" \
756
- --description "$final_description" \
757
- --zip-file "fileb://$(convert_path "$current_dir/$output_dir/$zip_file")" \
758
- --compatible-runtimes "$compatible_runtimes" \
759
- --query '[LayerVersionArn, Version, Description]' \
760
- --output table 2>&1 | tee publish.log
845
+ "${publish_cmd[@]}" 2>&1 | tee publish.log
761
846
 
762
847
  local aws_exit_code=${PIPESTATUS[0]}
763
848
 
@@ -25,6 +25,7 @@ cp "$BASE_DIR/VERSION.txt" "$BUILD_DIR/"
25
25
  cp "$BASE_DIR/scripts/aws-lambda-layer-cli" "$ASSETS_DIR/"
26
26
  cp "$BASE_DIR/scripts/create_nodejs_layer.sh" "$ASSETS_DIR/"
27
27
  cp "$BASE_DIR/scripts/create_python_layer.sh" "$ASSETS_DIR/"
28
+ cp "$BASE_DIR/scripts/create_wheel_layer.sh" "$ASSETS_DIR/"
28
29
  cp "$BASE_DIR/scripts/uninstall.sh" "$ASSETS_DIR/"
29
30
 
30
31
  # Copy completion files
@@ -40,6 +41,6 @@ cd "$BASE_DIR"
40
41
  python3 -m build
41
42
 
42
43
  echo "Cleaning up temporary package files..."
43
- # rm -rf "$BUILD_DIR" "$BASE_DIR/aws_lambda_layer_cli.egg-info" "$BASE_DIR/build"
44
+ rm -rf "$BUILD_DIR" "$BASE_DIR/aws_lambda_layer_cli.egg-info" "$BASE_DIR/build"
44
45
 
45
46
  echo "Build complete! Artifacts are in dist/"
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
2
 
3
- # Python Lambda Layer Creator with UV and version specification
3
+ # Python Lambda Layer Creator with version specification
4
4
  # Usage:
5
5
  # ./create_python_layer.sh -i numpy==1.26.0,pandas==2.1.3
6
6
  # ./create_python_layer.sh -i numpy==1.26.0,pandas,boto3==1.34.0 -n my-layer.zip
7
- # ./create_python_layer.sh --packages=requests==2.31.0,boto3 --no-uv
7
+ # ./create_python_layer.sh --packages=requests==2.31.0,boto3
8
8
 
9
9
  set -e # Exit on error
10
10
  set -u # Treat unset variables as errors
@@ -19,8 +19,11 @@ LAYER_NAME=""
19
19
  PYTHON_VERSION="3.14" # Default to Python 3.14
20
20
  PYTHON_VERSION_SPECIFIED=false
21
21
  VENV_DIR="python"
22
- USE_UV=true
23
22
  ORIGINAL_DIR=$(pwd)
23
+ PLATFORM="" # Optional platform targeting
24
+ IMPLEMENTATION="cp"
25
+ ABI=""
26
+ ARCHITECTURE="x86_64" # Default architecture
24
27
 
25
28
  # Colors for output
26
29
  RED='\033[0;31m'
@@ -137,24 +140,33 @@ while [[ $# -gt 0 ]]; do
137
140
  validate_python_version "$PYTHON_VERSION"
138
141
  shift
139
142
  ;;
140
- --no-uv)
141
- USE_UV=false
143
+ -a|--architecture)
144
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
145
+ ARCHITECTURE="$2"
146
+ shift 2
147
+ else
148
+ printf "${RED}Error: $1 requires an argument${NC}\n"
149
+ printf "Example: $1 arm64\n"
150
+ exit 1
151
+ fi
152
+ ;;
153
+ --architecture=*)
154
+ ARCHITECTURE="${1#*=}"
142
155
  shift
143
156
  ;;
144
157
  -h|--help)
145
158
  cat << 'EOF'
146
- Python Lambda Layer Creator with UV
159
+ Python Lambda Layer Creator
147
160
 
148
161
  Usage:
149
162
  ./create_python_layer.sh -i numpy==1.26.0,pandas==2.1.3
150
163
  ./create_python_layer.sh --packages=numpy==1.26.0,pandas,boto3==1.34.0 -n my-layer.zip
151
- ./create_python_layer.sh -i flask==3.0.0 --no-uv
152
164
 
153
165
  Options:
154
166
  -i, --packages Comma-separated list of Python packages (with optional versions)
155
167
  -n, --name Name of the output zip file
156
168
  --python-version Python version (default: 3.14)
157
- --no-uv Use pip/venv instead of uv
169
+ -a, --architecture Target architecture (x86_64 or arm64, default: x86_64)
158
170
  -h, --help Show this help message
159
171
 
160
172
  Version Specification:
@@ -167,9 +179,14 @@ Version Specification:
167
179
  Django!=3.2.0 # Version exclusion
168
180
 
169
181
  Examples:
182
+ # Basic usage
170
183
  ./create_python_layer.sh -i numpy==1.26.0
171
- ./create_python_layer.sh -i requests==2.31.0,boto3==1.34.0 --python-version=3.14
172
- ./create_python_layer.sh --packages=pandas==2.1.3,scikit-learn==1.3.0 --no-uv -n ml-layer.zip
184
+
185
+ # With platform targeting for Amazon Linux 2023
186
+ ./create_python_layer.sh -i requests==2.31.0,boto3==1.34.0 --python-version=3.13 --platform=manylinux_2_28_x86_64
187
+
188
+ # With platform targeting for ARM64
189
+ ./create_python_layer.sh --packages=pandas==2.1.3,scikit-learn==1.3.0 --platform=manylinux_2_28_aarch64 -n ml-layer.zip
173
190
  EOF
174
191
  exit 0
175
192
  ;;
@@ -189,14 +206,6 @@ if [ -z "$PACKAGES" ]; then
189
206
  exit 1
190
207
  fi
191
208
 
192
- # Check if uv is available safely
193
- if [ "$USE_UV" = true ]; then
194
- if ! command -v uv >/dev/null 2>&1; then
195
- printf "${YELLOW}Warning: uv not found, falling back to pip/venv${NC}\n"
196
- USE_UV=false
197
- fi
198
- fi
199
-
200
209
  # Check dependencies
201
210
  if ! command -v zip &> /dev/null; then
202
211
  printf "${RED}Error: 'zip' command is not installed${NC}\n"
@@ -236,12 +245,29 @@ if [ "$PACKAGES" != "$SANITIZED_PACKAGES" ]; then
236
245
  PACKAGES="$SANITIZED_PACKAGES"
237
246
  fi
238
247
 
248
+ # Normalize Architecture
249
+ AWS_ARCH="$ARCHITECTURE"
250
+ if [ "$ARCHITECTURE" = "arm64" ]; then
251
+ ARCHITECTURE="aarch64"
252
+ AWS_ARCH="arm64"
253
+ elif [ "$ARCHITECTURE" = "amd64" ]; then
254
+ ARCHITECTURE="x86_64"
255
+ AWS_ARCH="x86_64"
256
+ elif [ "$ARCHITECTURE" = "x86_64" ]; then
257
+ AWS_ARCH="x86_64"
258
+ elif [ "$ARCHITECTURE" = "aarch64" ]; then
259
+ AWS_ARCH="arm64"
260
+ fi
261
+
239
262
  printf "${BLUE}=========================================${NC}\n"
240
263
  printf "${GREEN}Python Lambda Layer Creator${NC}\n"
241
264
  printf "${BLUE}=========================================${NC}\n"
242
265
  printf "Packages: $PACKAGES\n"
243
266
  printf "Python version: $PYTHON_VERSION\n"
244
- printf "Using UV: $USE_UV\n"
267
+ printf "Target Architecture: $AWS_ARCH\n"
268
+ if [ -n "$PLATFORM" ]; then
269
+ printf "Platform: $PLATFORM\n"
270
+ fi
245
271
  if [ -n "$LAYER_NAME" ]; then
246
272
  printf "Output name: $LAYER_NAME\n"
247
273
  fi
@@ -277,20 +303,12 @@ if ! command -v "$TARGET_PYTHON" >/dev/null 2>&1; then
277
303
  fi
278
304
  fi
279
305
 
280
- if [ "$USE_UV" = true ]; then
281
- printf " Using UV to create venv...\n"
282
- if ! uv venv --python "$TARGET_PYTHON" "$VENV_DIR"; then
283
- printf "${RED}Error: Failed to create venv with uv${NC}\n"
284
- exit 1
285
- fi
306
+ printf " Using venv module...\n"
307
+ if command -v "$TARGET_PYTHON" >/dev/null 2>&1; then
308
+ "$TARGET_PYTHON" -m venv "$VENV_DIR"
286
309
  else
287
- printf " Using venv module...\n"
288
- if command -v "$TARGET_PYTHON" >/dev/null 2>&1; then
289
- "$TARGET_PYTHON" -m venv "$VENV_DIR"
290
- else
291
- printf "${RED}Error: $TARGET_PYTHON not found${NC}\n"
292
- exit 1
293
- fi
310
+ printf "${RED}Error: $TARGET_PYTHON not found${NC}\n"
311
+ exit 1
294
312
  fi
295
313
 
296
314
  # Activate virtual environment
@@ -307,16 +325,64 @@ set -u
307
325
 
308
326
  # Step 3: Install packages with versions
309
327
  printf "[3/7] Installing packages...\n"
310
- if [ "$USE_UV" = true ]; then
311
- printf " Installing with UV...\n"
312
- # Convert to array for safe expansion
313
- IFS=',' read -ra PKG_ARRAY <<< "$PACKAGES"
314
- uv pip install "${PKG_ARRAY[@]}"
328
+
329
+ # Auto-detect platform if not specified
330
+ if [ -z "$PLATFORM" ]; then
331
+ # Calculate major/minor version
332
+ # PYTHON_VERSION is like 3.14 or 3.14.2
333
+ PY_VER_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1)
334
+ PY_VER_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
335
+
336
+ # Platform selection based on AWS Lambda Runtime
337
+ if [ "$PY_VER_MAJOR" -eq 3 ] && [ "$PY_VER_MINOR" -ge 12 ]; then
338
+ # Python 3.12+ runs on Amazon Linux 2023 (GLIBC 2.34)
339
+ # We use manylinux_2_28 (GLIBC 2.28) which is well-supported
340
+ PLATFORM_PREFIX="manylinux_2_28"
341
+ printf " Targeting Amazon Linux 2023 (Python $PYTHON_VERSION)\n"
342
+ else
343
+ # Python 3.11- runs on Amazon Linux 2 (GLIBC 2.26)
344
+ # We use manylinux2014 (GLIBC 2.17) for max compatibility
345
+ PLATFORM_PREFIX="manylinux2014"
346
+ printf " Targeting Amazon Linux 2 (Python $PYTHON_VERSION)\n"
347
+ fi
348
+
349
+ PLATFORM="${PLATFORM_PREFIX}_${ARCHITECTURE}"
350
+ printf "Auto-detected platform: $PLATFORM (Python $PYTHON_VERSION, Arch $ARCHITECTURE)\n"
351
+ fi
352
+
353
+ # Prepare platform-specific options
354
+ INSTALL_OPTS=()
355
+ if [ -n "$PLATFORM" ]; then
356
+ # Calculate ABI tag based on Python version (e.g., 3.12 -> cp312)
357
+ # We use cut instead of potentially fragile regex
358
+ PY_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1)
359
+ PY_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
360
+ ABI="cp${PY_MAJOR}${PY_MINOR}"
361
+
362
+ INSTALL_OPTS+=("--platform" "$PLATFORM")
363
+ INSTALL_OPTS+=("--implementation" "$IMPLEMENTATION")
364
+ INSTALL_OPTS+=("--python-version" "$PYTHON_VERSION")
365
+ INSTALL_OPTS+=("--abi" "$ABI")
366
+ INSTALL_OPTS+=("--only-binary=:all:")
367
+ printf " Using platform-specific installation: $PLATFORM\n"
368
+ printf " ABI tag: $ABI\n"
369
+ fi
370
+
371
+ printf " Installing with pip...\n"
372
+ # Convert to array for safe expansion
373
+ IFS=',' read -ra PKG_ARRAY <<< "$PACKAGES"
374
+ if [ ${#INSTALL_OPTS[@]} -gt 0 ]; then
375
+ # When using platform specific options, we must specify --target
376
+ # We use the site-packages directory of the current venv
377
+ SITE_PACKAGES=$(python -c "import site; print(site.getsitepackages()[0])")
378
+ printf " Targeting site-packages: $SITE_PACKAGES\n"
379
+ CMD=(pip install "${PKG_ARRAY[@]}" "${INSTALL_OPTS[@]}" --target "$SITE_PACKAGES")
380
+ echo " Running: ${CMD[*]}"
381
+ "${CMD[@]}"
315
382
  else
316
- printf " Installing with pip...\n"
317
- # Convert to array for safe expansion
318
- IFS=',' read -ra PKG_ARRAY <<< "$PACKAGES"
319
- pip install "${PKG_ARRAY[@]}"
383
+ CMD=(pip install "${PKG_ARRAY[@]}")
384
+ echo " Running: ${CMD[*]}"
385
+ "${CMD[@]}"
320
386
  fi
321
387
 
322
388
  # Count packages from command argument
@@ -333,11 +399,7 @@ if [ -z "$LAYER_NAME" ]; then
333
399
  printf " Single package: $PKG_NAME\n"
334
400
 
335
401
  # Extract version from installed package
336
- if [ "$USE_UV" = true ]; then
337
- PKG_INFO=$(uv pip show "$PKG_NAME" 2>/dev/null || true)
338
- else
339
- PKG_INFO=$(pip show "$PKG_NAME" 2>/dev/null || true)
340
- fi
402
+ PKG_INFO=$(pip show "$PKG_NAME" 2>/dev/null || true)
341
403
 
342
404
  if [ -n "$PKG_INFO" ]; then
343
405
  # Use safer extraction methods
@@ -400,11 +462,7 @@ fi
400
462
 
401
463
  # Step 5: Show installed packages
402
464
  printf "[5/7] Listing installed packages...\n"
403
- if [ "$USE_UV" = true ]; then
404
- uv pip list --format freeze
405
- else
406
- pip list --format freeze
407
- fi
465
+ pip list --format freeze
408
466
 
409
467
  # Deactivate virtual environment
410
468
  set +u
@@ -433,7 +491,7 @@ printf "${GREEN}✅ SUCCESS: Python Lambda Layer Created${NC}\n"
433
491
  printf "${BLUE}=========================================${NC}\n"
434
492
  printf "📁 File: $ORIGINAL_DIR/$LAYER_NAME\n"
435
493
  printf "🐍 Python Version: $PYTHON_VERSION\n"
436
- printf "⚡ Tool: $(if [ "$USE_UV" = true ]; then echo "UV"; else echo "pip/venv"; fi)\n"
494
+ printf "⚡ Tool: pip/venv\n"
437
495
  printf "📦 Size: $(du -h "$ORIGINAL_DIR/$LAYER_NAME" | cut -f1)\n"
438
496
  printf "📊 Package Count: $PACKAGE_COUNT\n"
439
497
 
@@ -446,11 +504,7 @@ for pkg_full in "${PKG_ARRAY[@]}"; do
446
504
  pkg_name=$(extract_package_name "$pkg_full")
447
505
 
448
506
  # Get installed version from pip show or metadata
449
- if [ "$USE_UV" = true ]; then
450
- installed_ver=$(find . -type f -name "METADATA" -path "*/${pkg_name}-*.dist-info/METADATA" -exec grep -h "^Version:" {} \; | head -1 | cut -d' ' -f2)
451
- else
452
- installed_ver=$(find . -type f -name "METADATA" -path "*/${pkg_name}-*.dist-info/METADATA" -exec grep -h "^Version:" {} \; | head -1 | cut -d' ' -f2)
453
- fi
507
+ installed_ver=$(find . -type f -name "METADATA" -path "*/${pkg_name}-*.dist-info/METADATA" -exec grep -h "^Version:" {} \; | head -1 | cut -d' ' -f2)
454
508
 
455
509
  if [ -n "$installed_ver" ]; then
456
510
  if [ -n "$INSTALLED_PKGS" ]; then
@@ -0,0 +1,458 @@
1
+ #!/bin/bash
2
+
3
+ # Python Lambda Layer Creator from Wheel
4
+ # Usage:
5
+ # ./create_wheel_layer.sh -w mypackage.whl
6
+ # ./create_wheel_layer.sh -w mypackage.whl -i "pandas,boto3" -n my-layer.zip
7
+
8
+ set -e
9
+ set -u
10
+
11
+ # Default values
12
+ WHEEL_FILE=""
13
+ PACKAGES=""
14
+ LAYER_NAME=""
15
+ # We now track user provided values separate from defaults
16
+ USER_PYTHON_VERSION=""
17
+ USER_ARCHITECTURE=""
18
+ USER_PLATFORM=""
19
+
20
+ DEFAULT_PYTHON_VERSION="3.12"
21
+ DEFAULT_ARCHITECTURE="x86_64"
22
+
23
+ PYTHON_VERSION="$DEFAULT_PYTHON_VERSION"
24
+ ARCHITECTURE="$DEFAULT_ARCHITECTURE"
25
+ PLATFORM=""
26
+ IMPLEMENTATION="cp"
27
+ ABI="" # Will be calculated
28
+
29
+ # Colors
30
+ RED='\033[0;31m'
31
+ GREEN='\033[0;32m'
32
+ YELLOW='\033[1;33m'
33
+ NC='\033[0m'
34
+
35
+ # Parse arguments
36
+ while [[ $# -gt 0 ]]; do
37
+ case "$1" in
38
+ -w|--wheel)
39
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
40
+ WHEEL_FILE="$2"
41
+ shift 2
42
+ else
43
+ printf "${RED}Error: $1 requires an argument${NC}\n"
44
+ exit 1
45
+ fi
46
+ ;;
47
+ --wheel=*)
48
+ WHEEL_FILE="${1#*=}"
49
+ shift
50
+ ;;
51
+ -i|--packages)
52
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
53
+ PACKAGES="$2"
54
+ shift 2
55
+ else
56
+ printf "${RED}Error: $1 requires an argument${NC}\n"
57
+ exit 1
58
+ fi
59
+ ;;
60
+ --packages=*)
61
+ PACKAGES="${1#*=}"
62
+ shift
63
+ ;;
64
+ -n|--name)
65
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
66
+ LAYER_NAME="$2"
67
+ shift 2
68
+ else
69
+ printf "${RED}Error: $1 requires an argument${NC}\n"
70
+ exit 1
71
+ fi
72
+ ;;
73
+ --name=*)
74
+ LAYER_NAME="${1#*=}"
75
+ shift
76
+ ;;
77
+ --python-version)
78
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
79
+ USER_PYTHON_VERSION="$2"
80
+ PYTHON_VERSION="$2"
81
+ shift 2
82
+ else
83
+ printf "${RED}Error: $1 requires an argument${NC}\n"
84
+ exit 1
85
+ fi
86
+ ;;
87
+ --python-version=*)
88
+ USER_PYTHON_VERSION="${1#*=}"
89
+ PYTHON_VERSION="${1#*=}"
90
+ shift
91
+ ;;
92
+ --architecture|-a)
93
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
94
+ USER_ARCHITECTURE="$2"
95
+ ARCHITECTURE="$2"
96
+ shift 2
97
+ else
98
+ printf "${RED}Error: $1 requires an argument${NC}\n"
99
+ exit 1
100
+ fi
101
+ ;;
102
+ --architecture=*)
103
+ USER_ARCHITECTURE="${1#*=}"
104
+ ARCHITECTURE="${1#*=}"
105
+ shift
106
+ ;;
107
+ -h|--help)
108
+ cat << 'EOF'
109
+ Usage: ./create_wheel_layer.sh -w <wheel_file> [-i <packages>] [-n <zip_name>]
110
+
111
+ Options:
112
+ -w, --wheel Path to .whl file
113
+ -i, --packages Additional packages (comma or space separated)
114
+ -n, --name Output zip filename
115
+ --python-version Target Python version (default: 3.12)
116
+ -a, --architecture Target architecture (x86_64, arm64)
117
+
118
+ Supported Architectures:
119
+ x86_64 (amd64) # Standard Intel/AMD 64-bit
120
+ arm64 (aarch64) # AWS Graviton (ARM 64-bit)
121
+
122
+ Examples:
123
+ # Build for Amazon Linux 2 (Python 3.12, x86_64)
124
+ ./create_wheel_layer.sh -w mypackage.whl --python-version=3.12
125
+
126
+ # Build for ARM64
127
+ ./create_wheel_layer.sh -w mypackage.whl -a arm64
128
+ EOF
129
+ exit 0
130
+ ;;
131
+ *)
132
+ printf "${RED}Unknown option: $1${NC}\n"
133
+ exit 1
134
+ ;;
135
+ esac
136
+ done
137
+
138
+ # Validation
139
+ if [ -z "$WHEEL_FILE" ]; then
140
+ printf "${RED}Error: Wheel file is required (-w)${NC}\n"
141
+ exit 1
142
+ fi
143
+ # Detect pip
144
+ PIP_EXE=""
145
+ if command -v pip &> /dev/null; then
146
+ PIP_EXE="pip"
147
+ elif command -v pip3 &> /dev/null; then
148
+ PIP_EXE="pip3"
149
+ else
150
+ printf "${RED}Error: pip or pip3 not found. Please install Python and pip.${NC}\n"
151
+ exit 1
152
+ fi
153
+ if [ ! -f "$WHEEL_FILE" ]; then
154
+ printf "${RED}Error: File $WHEEL_FILE not found${NC}\n"
155
+ exit 1
156
+ fi
157
+
158
+ # ------------------------------------------------------------------
159
+ # WHEEL METADATA AUTO-DETECTION
160
+ # ------------------------------------------------------------------
161
+ # Extract metadata directly from filename/wheel to enforce strictness
162
+ # Format: Name-Ver-PyTag-AbiTag-PlatTag.whl
163
+
164
+ DETECTED_PY="any"
165
+ DETECTED_ABI="none"
166
+ DETECTED_PLAT="any"
167
+ DETECTED_ARCH="any"
168
+
169
+ # Helper python script to parse filename strictly
170
+ META_OUT=$(python3 -c "
171
+ import sys
172
+ import os
173
+
174
+ filename = os.path.basename(sys.argv[1])
175
+ if filename.endswith('.whl'):
176
+ filename = filename[:-4]
177
+
178
+ parts = filename.split('-')
179
+ # Minimal check: Name-Ver-Py-Abi-Plat
180
+ if len(parts) >= 5:
181
+ plat = parts[-1]
182
+ abi = parts[-2]
183
+ py = parts[-3]
184
+
185
+ # Arch mapping
186
+ arch = 'any'
187
+ if 'x86_64' in plat or 'amd64' in plat:
188
+ arch = 'x86_64'
189
+ elif 'aarch64' in plat or 'arm64' in plat:
190
+ arch = 'aarch64'
191
+
192
+ # Py Ver extraction (cp312->3.12)
193
+ py_ver = 'any'
194
+ if py.startswith('cp') and len(py) > 2 and py[2:].isdigit():
195
+ pv_raw = py[2:]
196
+ if len(pv_raw) == 2: # 39
197
+ py_ver = f'{pv_raw[0]}.{pv_raw[1]}'
198
+ elif len(pv_raw) >= 3: # 312
199
+ major = pv_raw[0]
200
+ minor = pv_raw[1:]
201
+ py_ver = f'{major}.{minor}'
202
+
203
+ print(f'{py_ver}|{arch}|{plat}|{abi}')
204
+ else:
205
+ print('any|any|any|none')
206
+ " "$WHEEL_FILE")
207
+
208
+ IFS='|' read -r DET_PY DET_ARCH DET_PLAT DET_ABI <<< "$META_OUT"
209
+
210
+ # STRICT MODE ENFORCEMENT
211
+ # If the wheel is binary (specific architecture or python), we ENFORCE it.
212
+ # If the wheel is 'any', we allow defaults or user args.
213
+
214
+ if [ "$DET_PY" != "any" ]; then
215
+ if [ -n "$USER_PYTHON_VERSION" ] && [ "$USER_PYTHON_VERSION" != "$DET_PY" ]; then
216
+ printf "${RED}Error: Wheel is for Python $DET_PY, but you requested $USER_PYTHON_VERSION.${NC}\n"
217
+ printf "Please remove the argument or use matching version.\n"
218
+ exit 1
219
+ fi
220
+ # Auto-set
221
+ PYTHON_VERSION="$DET_PY"
222
+ printf "Detected Python: $PYTHON_VERSION\n"
223
+ fi
224
+
225
+ if [ "$DET_ARCH" != "any" ]; then
226
+ if [ -n "$USER_ARCHITECTURE" ]; then
227
+ # Normalize user input for comparison
228
+ NORM_USER_ARCH="$USER_ARCHITECTURE"
229
+ if [ "$USER_ARCHITECTURE" == "arm64" ]; then NORM_USER_ARCH="aarch64"; fi
230
+ if [ "$USER_ARCHITECTURE" == "amd64" ]; then NORM_USER_ARCH="x86_64"; fi
231
+
232
+ if [ "$NORM_USER_ARCH" != "$DET_ARCH" ]; then
233
+ printf "${RED}Error: Wheel is for $DET_ARCH, but you requested $USER_ARCHITECTURE.${NC}\n"
234
+ exit 1
235
+ fi
236
+ fi
237
+ # Auto-set
238
+ ARCHITECTURE="$DET_ARCH"
239
+ printf "Detected Architecture: $ARCHITECTURE\n"
240
+ fi
241
+
242
+ if [ "$DET_PLAT" != "any" ] && [ -z "$USER_PLATFORM" ]; then
243
+ # Only override platform if user didn't specify one (pip might need specific one)
244
+ # But usually filename platform is correct for install
245
+ PLATFORM="$DET_PLAT"
246
+ fi
247
+
248
+ if [ "$DET_ABI" != "none" ]; then
249
+ ABI="$DET_ABI"
250
+ fi
251
+
252
+ # Normalize Architecture
253
+ AWS_ARCH="$ARCHITECTURE"
254
+ if [ "$ARCHITECTURE" = "arm64" ] || [ "$ARCHITECTURE" = "aarch64" ]; then
255
+ ARCHITECTURE="aarch64"
256
+ AWS_ARCH="arm64"
257
+ elif [ "$ARCHITECTURE" = "amd64" ] || [ "$ARCHITECTURE" = "x86_64" ]; then
258
+ ARCHITECTURE="x86_64"
259
+ AWS_ARCH="x86_64"
260
+ fi
261
+
262
+ # Determine Platform if still empty
263
+ if [ -z "$PLATFORM" ]; then
264
+ # Default to manylinux2014 as it is safe for both AL2 and AL2023
265
+ PLATFORM="manylinux2014_${ARCHITECTURE}"
266
+ fi
267
+
268
+
269
+ # Validate Wheel Suitability for Lambda (Linux) using metadata
270
+ printf "Validating wheel compatibility...\n"
271
+
272
+ # Only run python validation if python is available (it should be, given pip is used)
273
+ PYTHON_EXE=""
274
+ if command -v python3 &> /dev/null; then
275
+ PYTHON_EXE="python3"
276
+ elif command -v python &> /dev/null; then
277
+ PYTHON_EXE="python"
278
+ fi
279
+
280
+ if [ -n "$PYTHON_EXE" ]; then
281
+ # Use Python to inspect the WHEEL metadata for accurate tags
282
+ $PYTHON_EXE -c "
283
+ import sys, zipfile, os
284
+
285
+ try:
286
+ wheel_path = sys.argv[1]
287
+ target_arch = sys.argv[2]
288
+
289
+ # Define compatibility
290
+ compatible_os = ['manylinux', 'linux', 'any']
291
+
292
+ arch_map = {
293
+ 'x86_64': ['x86_64', 'amd64', 'any'],
294
+ 'arm64': ['aarch64', 'arm64', 'any'],
295
+ 'aarch64': ['aarch64', 'arm64', 'any']
296
+ }
297
+
298
+ with zipfile.ZipFile(wheel_path, 'r') as z:
299
+ # Find .dist-info/WHEEL
300
+ wheel_files = [f for f in z.namelist() if f.endswith('.dist-info/WHEEL')]
301
+ if not wheel_files:
302
+ # Fallback for old wheels without WHEEL metadata (rare)
303
+ print('Warning: No .dist-info/WHEEL found, skipping strict validation.')
304
+ sys.exit(0)
305
+
306
+ content = z.read(wheel_files[0]).decode('utf-8')
307
+ tags = []
308
+ for line in content.splitlines():
309
+ if line.startswith('Tag:'):
310
+ tags.append(line.split(':', 1)[1].strip())
311
+
312
+ has_linux = False
313
+ has_arch = False
314
+ detected_plats = set()
315
+
316
+ for tag in tags:
317
+ parts = tag.split('-')
318
+ if len(parts) >= 3:
319
+ plat = parts[2]
320
+ detected_plats.add(plat)
321
+
322
+ # Check OS
323
+ if any(x in plat for x in compatible_os):
324
+ has_linux = True
325
+
326
+ # Check Arch
327
+ target_valid_archs = arch_map.get(target_arch, [])
328
+
329
+ # Special handling for 'any' to avoid matching 'manylinux'
330
+ if plat == 'any' and 'any' in target_valid_archs:
331
+ has_arch = True
332
+ else:
333
+ # Filter out 'any' from search strings for substring check
334
+ search_archs = [x for x in target_valid_archs if x != 'any']
335
+ if any(x in plat for x in search_archs):
336
+ has_arch = True
337
+
338
+ if not has_linux:
339
+ print(f'Error: Wheel is not compatible with Linux.\nDetected platforms: {', '.join(sorted(detected_plats))}')
340
+ sys.exit(1)
341
+
342
+ if not has_arch:
343
+ print(f'Error: Wheel architecture mismatch.\nTarget: {target_arch}\nDetected platforms: {', '.join(sorted(detected_plats))}')
344
+ sys.exit(1)
345
+
346
+ except Exception as e:
347
+ print(f'Warning: Could not validate wheel metadata: {e}')
348
+ sys.exit(0) # Don't block build if validation script itself crashes
349
+ " "$WHEEL_FILE" "$ARCHITECTURE"
350
+
351
+ # Check exit code of python script
352
+ if [ $? -ne 0 ]; then
353
+ exit 1
354
+ fi
355
+ else
356
+ # Fallback to simple filename check if python not found (unlikely)
357
+ WHEEL_BASENAME=$(basename "$WHEEL_FILE")
358
+ if [[ "$WHEEL_BASENAME" == *"macosx"* ]] || [[ "$WHEEL_BASENAME" == *"win32"* ]]; then
359
+ printf "${YELLOW}Warning: Filename suggests non-Linux wheel ($WHEEL_BASENAME)${NC}\n"
360
+ fi
361
+ fi
362
+
363
+ # Determine ABI tag (If not auto-detected)
364
+ # e.g., 3.12 -> cp312, 3.10 -> cp310
365
+ if [ -z "$ABI" ] || [ "$ABI" == "none" ]; then
366
+ PY_MAJOR=$(echo "$PYTHON_VERSION" | cut -d. -f1)
367
+ PY_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2)
368
+ ABI="cp${PY_MAJOR}${PY_MINOR}"
369
+ fi
370
+
371
+ if [ -z "$LAYER_NAME" ]; then
372
+ BASENAME=$(basename "$WHEEL_FILE" .whl)
373
+ LAYER_NAME="${BASENAME}_layer.zip"
374
+ fi
375
+
376
+ # Setup workspace
377
+ LAYER_DIR="layer_build_$(date +%s)"
378
+ ORIGINAL_DIR=$(pwd)
379
+
380
+ # Convert relative paths to absolute
381
+ if [[ "$WHEEL_FILE" != /* ]]; then
382
+ WHEEL_FILE="$ORIGINAL_DIR/$WHEEL_FILE"
383
+ fi
384
+
385
+ printf "${GREEN}Creating Lambda layer from wheel...${NC}\n"
386
+ printf "Wheel: $WHEEL_FILE\n"
387
+ if [ -n "$PACKAGES" ]; then
388
+ printf "Extra Packages: $PACKAGES\n"
389
+ fi
390
+ printf "Target Architecture: $AWS_ARCH\n"
391
+ printf "Platform Tag: $PLATFORM\n"
392
+ printf "Python: $PYTHON_VERSION (ABI: $ABI)\n"
393
+
394
+ mkdir -p "$LAYER_DIR/python"
395
+
396
+ # Install
397
+ printf "${GREEN}Installing packages...${NC}\n"
398
+ CMD=("$PIP_EXE" "install" "$WHEEL_FILE")
399
+
400
+ if [ -n "$PACKAGES" ]; then
401
+ # Replace commas with spaces
402
+ PKG_SPACE=$(echo "$PACKAGES" | tr ',' ' ')
403
+ # Split into array
404
+ read -ra PKG_ARRAY <<< "$PKG_SPACE"
405
+ CMD+=("${PKG_ARRAY[@]}")
406
+ fi
407
+
408
+ CMD+=("--target" "$LAYER_DIR/python")
409
+
410
+ # Handle multiple platform tags (e.g. manylinux1_x86_64.linux_x86_64)
411
+ # Split by dot and add each as separate --platform argument
412
+ IFS='.' read -ra PLAT_TAGS <<< "$PLATFORM"
413
+ for tag in "${PLAT_TAGS[@]}"; do
414
+ CMD+=("--platform" "$tag")
415
+ done
416
+
417
+ CMD+=("--implementation" "$IMPLEMENTATION")
418
+ CMD+=("--python-version" "$PYTHON_VERSION")
419
+ CMD+=("--abi" "$ABI")
420
+ CMD+=("--only-binary=:all:")
421
+ CMD+=("--upgrade")
422
+
423
+ echo "Running: ${CMD[*]}"
424
+ if ! "${CMD[@]}"; then
425
+ printf "${RED}Installation failed${NC}\n"
426
+ rm -rf "$LAYER_DIR"
427
+ exit 1
428
+ fi
429
+
430
+ # Cleanup
431
+ printf "${GREEN}Removing cache and metadata...${NC}\n"
432
+ find "$LAYER_DIR" -type d -name "__pycache__" -exec rm -rf {} +
433
+ # Removing dist-info might break some packages (entry points, metadata), but user requested space saving
434
+ # Making it optional or just following user's script. Following user script:
435
+ find "$LAYER_DIR" -type d -name "*.dist-info" -exec rm -rf {} +
436
+
437
+ # Zip
438
+ printf "${GREEN}Zipping to $LAYER_NAME...${NC}\n"
439
+ cd "$LAYER_DIR"
440
+
441
+ # Handle absolute vs relative path for LAYER_NAME
442
+ ZIP_DEST="$LAYER_NAME"
443
+ if [[ "$LAYER_NAME" != /* ]]; then
444
+ ZIP_DEST="$ORIGINAL_DIR/$LAYER_NAME"
445
+ fi
446
+
447
+ if zip -r "$ZIP_DEST" python > /dev/null; then
448
+ printf "${GREEN}✅ Done! Created $LAYER_NAME${NC}\n"
449
+ printf "File: $(basename "$LAYER_NAME")\n"
450
+ else
451
+ printf "${RED}Error creating zip file at $ZIP_DEST${NC}\n"
452
+ cd "$ORIGINAL_DIR"
453
+ rm -rf "$LAYER_DIR"
454
+ exit 1
455
+ fi
456
+
457
+ cd "$ORIGINAL_DIR"
458
+ rm -rf "$LAYER_DIR"
@@ -44,7 +44,7 @@ if ([string]::IsNullOrEmpty($InstallDir)) {
44
44
  # Configuration
45
45
  $RepoUrl = "https://github.com/yukcw/aws-lambda-layer-cli"
46
46
  $ToolName = "aws-lambda-layer-cli"
47
- $Version = "2.0.4" # Fallback version
47
+ $Version = "2.2.0" # Fallback version
48
48
 
49
49
  # Colors for output
50
50
  $Green = "Green"
File without changes