diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index dd4758434..02c814cad 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -851,47 +851,6 @@ class LocalDeps(NamedTuple): additional_contexts: list[pathlib.Path] = None -def _get_package_metadata_files(package_path: pathlib.Path) -> list[str]: - """Return list of metadata files that exist in package_path. - - These files are needed for dependency resolution and should be copied - before source code to optimize Docker layer caching. - """ - metadata_files = [ - # Core build files - "pyproject.toml", - "setup.py", - "setup.cfg", - "requirements.txt", - - # Referenced in pyproject.toml - "README.md", - "README.rst", - "README.txt", - "LICENSE", - "LICENSE.txt", - "MANIFEST.in", - - # Lock files for reproducible builds - "uv.lock", - "poetry.lock", - "Pipfile.lock", - "pdm.lock", - - # Version files - "VERSION", - "_version.py", - "__version__.py", - ] - - existing_files = [] - for file in metadata_files: - if (package_path / file).exists(): - existing_files.append(file) - - return existing_files - - def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps: config_path = config_path.resolve() # ensure reserved package names are not used @@ -964,6 +923,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps # set working_dir if local_dep == ".": working_dir = f"/deps/{container_name}" + requirement_path = f"/deps/{container_name}/requirements.txt" else: # We could not find a pyproject.toml or setup.py, so treat as a faux package if any(file == "__init__.py" for file in files): @@ -995,17 +955,18 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps faux_pkgs[resolved] = (local_dep, container_path) if local_dep == ".": working_dir = container_path + requirement_path = f"{container_path}/requirements.txt" - # If the faux package has a requirements.txt, we'll add - # the path to the list of requirements to install. - if "requirements.txt" in files: - rfile = resolved / "requirements.txt" - pip_reqs.append( - ( - rfile, - f"{container_path}/requirements.txt", - ) + # If the package has a requirements.txt, we'll add + # the path to the list of requirements to install. + if "requirements.txt" in files: + rfile = resolved / "requirements.txt" + pip_reqs.append( + ( + rfile, + requirement_path, ) + ) return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts) @@ -1395,73 +1356,18 @@ RUN set -ex && \\ for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items() ) - # Optimized two-stage copy for better Docker layer caching: - # Stage 1: Copy only metadata files (rarely change) - # Stage 2: Install dependencies (cached until metadata changes) - # Stage 3: Copy full source code (changes frequently) - # Stage 4: Quick reinstall without dependency resolution (fast) - - local_pkgs_metadata_str_parts = [] - local_pkgs_install_str_parts = [] - local_pkgs_source_str_parts = [] - local_pkgs_reinstall_str_parts = [] - - for fullpath, (relpath, name) in local_deps.real_pkgs.items(): - metadata_files = _get_package_metadata_files(fullpath) - - if fullpath in local_deps.additional_contexts: - # For additional contexts, use COPY --from - if metadata_files: - copy_commands = "\n".join( - f"COPY --from={name} {f} /deps/{name}/{f}" - for f in metadata_files - ) - local_pkgs_metadata_str_parts.append( - f"""# -- Adding package metadata for {name} -- -RUN mkdir -p /deps/{name} -{copy_commands} -# -- End of package metadata for {name} --""" - ) - - local_pkgs_source_str_parts.append( - f"""# -- Adding full source for {relpath} -- + local_pkgs_str = os.linesep.join( + ( + f"""# -- Adding local package {relpath} -- COPY --from={name} . /deps/{name} -# -- End of full source for {relpath} --""" - ) - else: - # For local paths, use ADD - if metadata_files: - copy_commands = "\n".join( - f"ADD {relpath}/{f} /deps/{name}/{f}" - for f in metadata_files - ) - local_pkgs_metadata_str_parts.append( - f"""# -- Adding package metadata for {name} -- -RUN mkdir -p /deps/{name} -{copy_commands} -# -- End of package metadata for {name} --""" - ) - - local_pkgs_source_str_parts.append( - f"""# -- Adding full source for {relpath} -- +# -- End of local package {relpath} --""" + if fullpath in local_deps.additional_contexts + else f"""# -- Adding local package {relpath} -- ADD {relpath} /deps/{name} -# -- End of full source for {relpath} --""" - ) - - # Install with dependency resolution (cached if metadata unchanged) - local_pkgs_install_str_parts.append( - f"RUN cd /deps/{name} && {global_reqs_pip_install} ." +# -- End of local package {relpath} --""" ) - - # Quick reinstall without dependency resolution (fast, after source changes) - local_pkgs_reinstall_str_parts.append( - f"RUN cd /deps/{name} && {global_reqs_pip_install} --no-deps ." - ) - - local_pkgs_metadata_str = os.linesep.join(local_pkgs_metadata_str_parts) - local_pkgs_install_str = os.linesep.join(local_pkgs_install_str_parts) - local_pkgs_source_str = os.linesep.join(local_pkgs_source_str_parts) - local_pkgs_reinstall_str = os.linesep.join(local_pkgs_reinstall_str_parts) + for fullpath, (relpath, name) in local_deps.real_pkgs.items() + ) install_node_str: str = ( "RUN /storage/install-node.sh" @@ -1477,11 +1383,8 @@ ADD {relpath} /deps/{name} pip_config_file_str, pip_pkgs_str, pip_reqs_str, - local_pkgs_metadata_str, # Metadata first for caching + local_pkgs_str, faux_pkgs_str, - local_pkgs_install_str, # Install with deps (cached layer) - local_pkgs_source_str, # Source code after dependencies - local_pkgs_reinstall_str, # Quick reinstall without deps ], ) ) @@ -1523,14 +1426,21 @@ ADD {relpath} /deps/{name} image_str = docker_tag(config, base_image, api_version) docker_file_contents = [ f"FROM {image_str}", - "# -- making use of two-stage-cache-optimizations --", "", + "# -- making use of requirements.txt optimizations --", os.linesep.join(config["dockerfile_lines"]), "", installs, "", - # Note: Individual package installs are now in 'installs' above - # This optimizes caching by separating metadata from source code + "# -- Installing all local dependencies --", + f"""RUN for dep in /deps/*; do \ + echo "Installing $dep"; \ + if [ -d "$dep" ]; then \ + echo "Installing $dep"; \ + (cd "$dep" && {global_reqs_pip_install} .); \ + fi; \ + done""", + "# -- End of local dependencies install --", os.linesep.join(env_vars), "", js_inst_str,