aws-lambda-layer-cli 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,465 @@
1
+ #!/bin/bash
2
+
3
+ # Python Lambda Layer Creator with UV and version specification
4
+ # Usage:
5
+ # ./create_python_layer.sh -i numpy==1.26.0,pandas==2.1.3
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
8
+
9
+ set -e # Exit on error
10
+ set -u # Treat unset variables as errors
11
+
12
+ # Generate unique temporary directory
13
+ TEMP_DIR=$(mktemp -d)
14
+ WORK_DIR="$TEMP_DIR/layer-build"
15
+
16
+ # Default values
17
+ PACKAGES=""
18
+ LAYER_NAME=""
19
+ PYTHON_VERSION="3.14" # Default to Python 3.14
20
+ PYTHON_VERSION_SPECIFIED=false
21
+ VENV_DIR="python"
22
+ USE_UV=true
23
+ ORIGINAL_DIR=$(pwd)
24
+
25
+ # Colors for output
26
+ RED='\033[0;31m'
27
+ GREEN='\033[0;32m'
28
+ YELLOW='\033[1;33m'
29
+ BLUE='\033[0;34m'
30
+ NC='\033[0m'
31
+
32
+ # Security functions
33
+ sanitize_filename() {
34
+ local filename="$1"
35
+ # Remove dangerous characters: /, \, :, |, <, >, ?, *, ", ', `, $, (, ), {, }, ;, &, !
36
+ filename=$(echo "$filename" | sed 's/[\/\\:|<>?*"\`$(){};&!]//g')
37
+ # Remove leading/trailing dots and hyphens
38
+ filename=$(echo "$filename" | sed 's/^[.-]*//' | sed 's/[.-]*$//')
39
+ # Limit length
40
+ echo "${filename:0:100}"
41
+ }
42
+
43
+ validate_python_version() {
44
+ local version="$1"
45
+ # Allow only numbers and dots in Python version (e.g., 3.9, 3.14.2)
46
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
47
+ printf "${RED}Error: Invalid Python version format: $version${NC}\n"
48
+ printf "Python version must be in format X.Y or X.Y.Z (e.g., 3.14, 3.14.2)\n"
49
+ exit 1
50
+ fi
51
+ }
52
+
53
+ escape_package_name() {
54
+ local pkg="$1"
55
+ # Whitelist for Python: A-Za-z0-9._- (with version operators: = > < ~ !)
56
+ # FIXED: Place hyphen at the end of character class to avoid regex range interpretation
57
+ echo "$pkg" | sed 's/[^A-Za-z0-9._=><~!+-]//g'
58
+ }
59
+
60
+ # Extract base package name from version specification
61
+ # Example: numpy==1.26.0 -> numpy
62
+ # Example: requests>=2.31.0 -> requests
63
+ extract_package_name() {
64
+ local pkg="$1"
65
+ # Remove version specification operators and everything after
66
+ echo "$pkg" | sed 's/[=<>!~].*$//'
67
+ }
68
+
69
+ # Extract version specification from package string
70
+ # Example: numpy==1.26.0 -> ==1.26.0
71
+ # Example: requests>=2.31.0 -> >=2.31.0
72
+ extract_version_spec() {
73
+ local pkg="$1"
74
+ if [[ "$pkg" =~ [\=\<\>!~] ]]; then
75
+ echo "$pkg" | grep -o '[=<>!~].*' || echo ""
76
+ else
77
+ echo ""
78
+ fi
79
+ }
80
+
81
+ # Cleanup function
82
+ cleanup() {
83
+ if [ -d "$TEMP_DIR" ]; then
84
+ rm -rf "$TEMP_DIR"
85
+ fi
86
+ }
87
+
88
+ # Trap to ensure cleanup on exit
89
+ trap cleanup EXIT
90
+
91
+ # Parse command line arguments
92
+ while [[ $# -gt 0 ]]; do
93
+ case "$1" in
94
+ -i|--packages)
95
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
96
+ PACKAGES="$2"
97
+ shift 2
98
+ else
99
+ printf "${RED}Error: $1 requires an argument${NC}\n"
100
+ printf "Example: $1 numpy==1.26.0,requests\n"
101
+ exit 1
102
+ fi
103
+ ;;
104
+ --packages=*)
105
+ PACKAGES="${1#*=}"
106
+ shift
107
+ ;;
108
+ -n|--name)
109
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
110
+ LAYER_NAME="$2"
111
+ shift 2
112
+ else
113
+ printf "${RED}Error: $1 requires an argument${NC}\n"
114
+ printf "Example: $1 my-python-layer.zip\n"
115
+ exit 1
116
+ fi
117
+ ;;
118
+ --name=*)
119
+ LAYER_NAME="${1#*=}"
120
+ shift
121
+ ;;
122
+ --python-version)
123
+ if [[ -n "${2:-}" && "${2:-}" != -* ]]; then
124
+ PYTHON_VERSION="$2"
125
+ PYTHON_VERSION_SPECIFIED=true
126
+ validate_python_version "$PYTHON_VERSION"
127
+ shift 2
128
+ else
129
+ printf "${RED}Error: $1 requires an argument${NC}\n"
130
+ printf "Example: $1 3.14\n"
131
+ exit 1
132
+ fi
133
+ ;;
134
+ --python-version=*)
135
+ PYTHON_VERSION="${1#*=}"
136
+ PYTHON_VERSION_SPECIFIED=true
137
+ validate_python_version "$PYTHON_VERSION"
138
+ shift
139
+ ;;
140
+ --no-uv)
141
+ USE_UV=false
142
+ shift
143
+ ;;
144
+ -h|--help)
145
+ cat << 'EOF'
146
+ Python Lambda Layer Creator with UV
147
+
148
+ Usage:
149
+ ./create_python_layer.sh -i numpy==1.26.0,pandas==2.1.3
150
+ ./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
+
153
+ Options:
154
+ -i, --packages Comma-separated list of Python packages (with optional versions)
155
+ -n, --name Name of the output zip file
156
+ --python-version Python version (default: 3.14)
157
+ --no-uv Use pip/venv instead of uv
158
+ -h, --help Show this help message
159
+
160
+ Version Specification:
161
+ Package versions can be specified using standard Python version specifiers:
162
+ numpy==1.26.0 # Exact version
163
+ pandas>=2.1.0 # Greater than or equal
164
+ requests>2.30.0 # Greater than
165
+ scipy<=1.11.0 # Less than or equal
166
+ tensorflow~=2.15.0 # Compatible release
167
+ Django!=3.2.0 # Version exclusion
168
+
169
+ Examples:
170
+ ./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
173
+ EOF
174
+ exit 0
175
+ ;;
176
+ *)
177
+ printf "${RED}Unknown option: $1${NC}\n"
178
+ printf "Use -h or --help for usage information\n"
179
+ exit 1
180
+ ;;
181
+ esac
182
+ done
183
+
184
+ # Check if packages are provided
185
+ if [ -z "$PACKAGES" ]; then
186
+ printf "${RED}Error: Packages argument is required${NC}\n"
187
+ printf "Use -i or --packages to specify packages (comma-separated)\n"
188
+ printf "Example: ./create_python_layer.sh -i numpy==1.26.0,requests\n"
189
+ exit 1
190
+ fi
191
+
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
+ # Check dependencies
201
+ if ! command -v zip &> /dev/null; then
202
+ printf "${RED}Error: 'zip' command is not installed${NC}\n"
203
+ exit 1
204
+ fi
205
+
206
+ if ! command -v python3 &> /dev/null && ! command -v python &> /dev/null; then
207
+ printf "${RED}Error: 'python' command is not installed${NC}\n"
208
+ exit 1
209
+ fi
210
+
211
+ # Sanitize packages input using whitelist
212
+ SANITIZED_PACKAGES=""
213
+ IFS=',' read -ra PACKAGE_ARRAY <<< "$PACKAGES"
214
+ for pkg in "${PACKAGE_ARRAY[@]}"; do
215
+ # Trim whitespace
216
+ pkg=$(echo "$pkg" | xargs)
217
+ # Escape package name using whitelist
218
+ escaped_pkg=$(escape_package_name "$pkg")
219
+ if [ -n "$escaped_pkg" ]; then
220
+ SANITIZED_PACKAGES="${SANITIZED_PACKAGES}${SANITIZED_PACKAGES:+,}$escaped_pkg"
221
+ else
222
+ printf "${YELLOW}Warning: Package name '$pkg' contains no valid characters after sanitization${NC}\n"
223
+ fi
224
+ done
225
+
226
+ if [ -z "$SANITIZED_PACKAGES" ]; then
227
+ printf "${RED}Error: No valid packages provided after sanitization${NC}\n"
228
+ exit 1
229
+ fi
230
+
231
+ # Check if any package names were changed
232
+ if [ "$PACKAGES" != "$SANITIZED_PACKAGES" ]; then
233
+ printf "${YELLOW}Warning: Some package names were sanitized:${NC}\n"
234
+ printf " Original: $PACKAGES\n"
235
+ printf " Sanitized: $SANITIZED_PACKAGES\n"
236
+ PACKAGES="$SANITIZED_PACKAGES"
237
+ fi
238
+
239
+ printf "${BLUE}=========================================${NC}\n"
240
+ printf "${GREEN}Python Lambda Layer Creator${NC}\n"
241
+ printf "${BLUE}=========================================${NC}\n"
242
+ printf "Packages: $PACKAGES\n"
243
+ printf "Python version: $PYTHON_VERSION\n"
244
+ printf "Using UV: $USE_UV\n"
245
+ if [ -n "$LAYER_NAME" ]; then
246
+ printf "Output name: $LAYER_NAME\n"
247
+ fi
248
+ printf "\n"
249
+
250
+ # Step 1: Create working directory
251
+ printf "[1/7] Creating directory structure...\n"
252
+ mkdir -p "$WORK_DIR"
253
+ cd "$WORK_DIR"
254
+
255
+ # Step 2: Create virtual environment
256
+ printf "[2/7] Creating virtual environment...\n"
257
+
258
+ TARGET_PYTHON="python${PYTHON_VERSION}"
259
+
260
+ # Check if target python exists
261
+ if ! command -v "$TARGET_PYTHON" >/dev/null 2>&1; then
262
+ if [ "$PYTHON_VERSION_SPECIFIED" = false ]; then
263
+ printf "${YELLOW}Warning: Default $TARGET_PYTHON not found. Checking for fallback...${NC}\n"
264
+ if command -v python3 >/dev/null 2>&1 && python3 -V >/dev/null 2>&1; then
265
+ TARGET_PYTHON="python3"
266
+ elif command -v python >/dev/null 2>&1 && python -V >/dev/null 2>&1; then
267
+ TARGET_PYTHON="python"
268
+ else
269
+ printf "${RED}Error: No python interpreter found${NC}\n"
270
+ exit 1
271
+ fi
272
+
273
+ # Update PYTHON_VERSION to match the fallback
274
+ DETECTED_VER=$($TARGET_PYTHON -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
275
+ printf "${YELLOW}Falling back to $TARGET_PYTHON ($DETECTED_VER)${NC}\n"
276
+ PYTHON_VERSION="$DETECTED_VER"
277
+ fi
278
+ fi
279
+
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
286
+ 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
294
+ fi
295
+
296
+ # Activate virtual environment
297
+ set +u
298
+ if [ -f "$VENV_DIR/Scripts/activate" ]; then
299
+ source "$VENV_DIR/Scripts/activate"
300
+ elif [ -f "$VENV_DIR/bin/activate" ]; then
301
+ source "$VENV_DIR/bin/activate"
302
+ else
303
+ printf "${RED}Error: Cannot find activation script in $VENV_DIR${NC}\n"
304
+ exit 1
305
+ fi
306
+ set -u
307
+
308
+ # Step 3: Install packages with versions
309
+ 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[@]}"
315
+ 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[@]}"
320
+ fi
321
+
322
+ # Count packages from command argument
323
+ PACKAGE_COUNT=$(echo "$PACKAGES" | tr ',' '\n' | wc -l | tr -d ' ')
324
+
325
+ # Step 4: Determine layer name
326
+ printf "[4/7] Determining layer name...\n"
327
+ if [ -z "$LAYER_NAME" ]; then
328
+ if [ "$PACKAGE_COUNT" -eq 1 ]; then
329
+ PKG_FULL="$PACKAGES"
330
+ PKG_NAME=$(extract_package_name "$PKG_FULL")
331
+ VERSION_SPEC=$(extract_version_spec "$PKG_FULL")
332
+
333
+ printf " Single package: $PKG_NAME\n"
334
+
335
+ # 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
341
+
342
+ if [ -n "$PKG_INFO" ]; then
343
+ # Use safer extraction methods
344
+ INSTALLED_VERSION=$(echo "$PKG_INFO" | grep -E '^Version:' | head -1 | awk '{print $2}')
345
+
346
+ if [ -z "$INSTALLED_VERSION" ]; then
347
+ INSTALLED_VERSION=$(echo "$PKG_INFO" | grep -E '^version:' | head -1 | awk '{print $2}')
348
+ fi
349
+
350
+ if [ -z "$INSTALLED_VERSION" ]; then
351
+ INSTALLED_VERSION=$(echo "$PKG_INFO" | grep -i '^version[[:space:]]*:' | head -1 | awk '{print $2}')
352
+ fi
353
+
354
+ if [ -n "$INSTALLED_VERSION" ]; then
355
+ # Sanitize version string using whitelist
356
+ INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/[^A-Za-z0-9._=><~!+-]//g')
357
+ INSTALLED_VERSION=$(echo "$INSTALLED_VERSION" | sed 's/\.post[0-9]*//' | sed 's/\.dev[0-9]*//' | sed 's/\+.*//')
358
+
359
+ # If user specified a version, use it in the name
360
+ if [ -n "$VERSION_SPEC" ]; then
361
+ # Extract just the version number from spec (remove operators)
362
+ SPEC_VERSION=$(echo "$VERSION_SPEC" | sed 's/^[=<>!~]*//')
363
+ LAYER_NAME="${PKG_NAME}-${SPEC_VERSION}-python${PYTHON_VERSION}"
364
+ printf " Specified version: $SPEC_VERSION\n"
365
+ printf " Using versioned name\n"
366
+ else
367
+ LAYER_NAME="${PKG_NAME}-${INSTALLED_VERSION}-python${PYTHON_VERSION}"
368
+ printf " Installed version: $INSTALLED_VERSION\n"
369
+ fi
370
+ else
371
+ LAYER_NAME="${PKG_NAME}-$(date +%Y%m%d)-python${PYTHON_VERSION}"
372
+ printf " Could not extract version, using date-based name\n"
373
+ fi
374
+ else
375
+ LAYER_NAME="${PKG_NAME}-$(date +%Y%m%d)-python${PYTHON_VERSION}"
376
+ printf " No package info found, using date-based name\n"
377
+ fi
378
+ else
379
+ LAYER_NAME="python-$(date +%Y%m%d)-python${PYTHON_VERSION}"
380
+ printf " Multiple packages, using date-based name\n"
381
+ fi
382
+
383
+ # Sanitize the layer name
384
+ LAYER_NAME=$(sanitize_filename "$LAYER_NAME")
385
+ fi
386
+
387
+ # Additional sanitization
388
+ LAYER_NAME=$(sanitize_filename "$LAYER_NAME")
389
+
390
+ # Check for path traversal in layer name
391
+ if [[ "$LAYER_NAME" =~ \.\. ]] || [[ "$LAYER_NAME" =~ ^/ ]]; then
392
+ printf "${RED}Error: Invalid layer name (path traversal detected)${NC}\n"
393
+ exit 1
394
+ fi
395
+
396
+ # Ensure .zip extension
397
+ if [[ ! "$LAYER_NAME" =~ \.zip$ ]]; then
398
+ LAYER_NAME="${LAYER_NAME}.zip"
399
+ fi
400
+
401
+ # Step 5: Show installed packages
402
+ 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
408
+
409
+ # Deactivate virtual environment
410
+ set +u
411
+ deactivate
412
+ set -u
413
+
414
+ # Step 6: Create zip file
415
+ printf "[6/7] Creating zip file: $LAYER_NAME\n"
416
+ cd "$WORK_DIR"
417
+ printf " Zipping 'python' directory...\n"
418
+ zip -r "$LAYER_NAME" "python" -q
419
+ printf " Zip file created successfully\n"
420
+
421
+ # Step 7: Move to final location
422
+ printf "[7/7] Moving to final location...\n"
423
+ if [[ -f "$LAYER_NAME" ]]; then
424
+ mv "$LAYER_NAME" "$ORIGINAL_DIR/"
425
+ else
426
+ printf "${RED}Error: Zip file not created${NC}\n"
427
+ exit 1
428
+ fi
429
+
430
+ printf "\n"
431
+ printf "${BLUE}=========================================${NC}\n"
432
+ printf "${GREEN}✅ SUCCESS: Python Lambda Layer Created${NC}\n"
433
+ printf "${BLUE}=========================================${NC}\n"
434
+ printf "📁 File: $ORIGINAL_DIR/$LAYER_NAME\n"
435
+ printf "🐍 Python Version: $PYTHON_VERSION\n"
436
+ printf "⚡ Tool: $(if [ "$USE_UV" = true ]; then echo "UV"; else echo "pip/venv"; fi)\n"
437
+ printf "📦 Size: $(du -h "$ORIGINAL_DIR/$LAYER_NAME" | cut -f1)\n"
438
+ printf "📊 Package Count: $PACKAGE_COUNT\n"
439
+
440
+ # Output installed packages with versions for description
441
+ printf "Installed packages: "
442
+ cd "$WORK_DIR/python"
443
+ INSTALLED_PKGS=""
444
+ IFS=',' read -ra PKG_ARRAY <<< "$PACKAGES"
445
+ for pkg_full in "${PKG_ARRAY[@]}"; do
446
+ pkg_name=$(extract_package_name "$pkg_full")
447
+
448
+ # 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
454
+
455
+ if [ -n "$installed_ver" ]; then
456
+ if [ -n "$INSTALLED_PKGS" ]; then
457
+ INSTALLED_PKGS="$INSTALLED_PKGS, ${pkg_name}==${installed_ver}"
458
+ else
459
+ INSTALLED_PKGS="${pkg_name}==${installed_ver}"
460
+ fi
461
+ fi
462
+ done
463
+ printf "$INSTALLED_PKGS\n"
464
+
465
+ printf "\n"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ const { execSync } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ try {
8
+ const isWindows = os.platform() === 'win32';
9
+ const scriptPath = isWindows ? path.join('scripts', 'install.ps1') : path.join('scripts', 'install.sh');
10
+ const fullPath = path.join(__dirname, '..', scriptPath);
11
+
12
+ if (!fs.existsSync(fullPath)) {
13
+ console.error(`Installation script not found: ${fullPath}`);
14
+ process.exit(1);
15
+ }
16
+
17
+ if (isWindows) {
18
+ // Run PowerShell script with proper execution policy
19
+ execSync(`powershell -ExecutionPolicy Bypass -File "${fullPath}"`, {
20
+ stdio: 'inherit',
21
+ shell: true
22
+ });
23
+ } else {
24
+ // Run bash script
25
+ execSync(`bash "${fullPath}"`, {
26
+ stdio: 'inherit'
27
+ });
28
+ }
29
+
30
+ console.log('✓ Installation completed successfully');
31
+ } catch (error) {
32
+ console.error('✗ Installation failed:', error.message);
33
+ process.exit(1);
34
+ }