From df954134502197f6e752d6d20b65cd9673134690 Mon Sep 17 00:00:00 2001 From: dcabib Date: Tue, 17 Mar 2026 10:04:13 -0300 Subject: [PATCH] fix: include lock files in dependency hash for cache invalidation Resolves #8242 When using `sam build --cached`, changes to lock files (package-lock.json, Gemfile.lock, etc.) were not triggering cache invalidation, causing the build to skip dependency installation even when lock files specified different versions. This left functions with outdated or vulnerable dependencies. This fix adds a mapping of dependency managers to their lock file names in DependencyHashGenerator. When a lock file exists, the cache hash now includes both the manifest and lock file, ensuring proper cache invalidation when lock files change. Supported lock files: - npm: package-lock.json - npm-esbuild: package-lock.json - bundler: Gemfile.lock - gradle: gradle.lockfile - cli-package (dotnet): packages.lock.json - modules (go): go.sum - cargo (rust): Cargo.lock - uv (python): uv.lock - poetry (python): poetry.lock The implementation is backward compatible - lock files are optional and the behavior remains unchanged for projects without lock files. Co-Authored-By: Claude Sonnet 4.5 --- samcli/lib/build/dependency_hash_generator.py | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/samcli/lib/build/dependency_hash_generator.py b/samcli/lib/build/dependency_hash_generator.py index fb95f2117de..50ff3af1a42 100644 --- a/samcli/lib/build/dependency_hash_generator.py +++ b/samcli/lib/build/dependency_hash_generator.py @@ -1,5 +1,6 @@ """Utility Class for Getting Function or Layer Manifest Dependency Hashes""" +import hashlib import pathlib from typing import Any, Optional @@ -7,6 +8,20 @@ from samcli.lib.utils.hash import file_checksum +# Mapping of dependency managers to their lock file names +LOCK_FILE_MAPPING = { + "npm": "package-lock.json", + "npm-esbuild": "package-lock.json", + "bundler": "Gemfile.lock", + "gradle": "gradle.lockfile", + "cli-package": "packages.lock.json", + "modules": "go.sum", + "cargo": "Cargo.lock", + "uv": "uv.lock", + "poetry": "poetry.lock", +} + + # TODO Expand this class to hash specific sections of the manifest class DependencyHashGenerator: _code_uri: str @@ -50,15 +65,17 @@ def __init__( self._hash = None def _calculate_dependency_hash(self) -> Optional[str]: - """Calculate the manifest file hash + """Calculate the manifest file hash, including lock file if applicable Returns ------- Optional[str] - Returns manifest hash. If manifest does not exist or not supported, None will be returned. + Returns combined hash of manifest and lock file (if present). + If manifest does not exist or not supported, None will be returned. """ if self._manifest_path_override: manifest_file = self._manifest_path_override + config = None else: config = get_workflow_config(self._runtime, self._code_dir, self._base_dir) manifest_file = config.manifest_name @@ -70,7 +87,24 @@ def _calculate_dependency_hash(self) -> Optional[str]: if not manifest_path.is_file(): return None - return file_checksum(str(manifest_path), hash_generator=self._hash_generator) + manifest_hash = file_checksum(str(manifest_path), hash_generator=self._hash_generator) + + # Check if there's a lock file for this dependency manager + if config and config.dependency_manager in LOCK_FILE_MAPPING: + lock_file_name = LOCK_FILE_MAPPING[config.dependency_manager] + lock_file_path = pathlib.Path(self._code_dir, lock_file_name).resolve() + + # If lock file exists, combine hashes + if lock_file_path.is_file(): + lock_file_hash = file_checksum(str(lock_file_path), hash_generator=self._hash_generator) + + # Combine both hashes into a single hash + combined = f"{manifest_hash}:{lock_file_hash}" + hasher = self._hash_generator() if self._hash_generator else hashlib.md5() + hasher.update(combined.encode("utf-8")) + return hasher.hexdigest() + + return manifest_hash @property def hash(self) -> Optional[str]: