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 +24 -9
- package/completion/aws-lambda-layer-completion.bash +1 -1
- package/completion/aws-lambda-layer-completion.zsh +1 -1
- package/package.json +1 -1
- package/scripts/aws-lambda-layer-cli +161 -76
- package/scripts/build_pypi.sh +2 -1
- package/scripts/create_python_layer.sh +111 -57
- package/scripts/create_wheel_layer.sh +458 -0
- package/scripts/install.ps1 +1 -1
- package/scripts/uninstall.ps1 +0 -0
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
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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 --
|
|
16
|
+
local python_opts="--python-version --platform"
|
|
17
17
|
|
|
18
18
|
case ${cword} in
|
|
19
19
|
1)
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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}--
|
|
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
|
|
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
|
|
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 [
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
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
|
|
553
|
-
printf "Or use --runtime=nodejs
|
|
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
|
|
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=(
|
|
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 [
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/scripts/build_pypi.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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 "
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 "
|
|
288
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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"
|
package/scripts/install.ps1
CHANGED
|
@@ -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
|
|
47
|
+
$Version = "2.2.0" # Fallback version
|
|
48
48
|
|
|
49
49
|
# Colors for output
|
|
50
50
|
$Green = "Green"
|
package/scripts/uninstall.ps1
CHANGED
|
File without changes
|