Files
wehub-resource-sync a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:18 +08:00

1040 lines
37 KiB
Python

import os
import pathlib
import re
import shlex
from dataclasses import dataclass
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - exercised on Python 3.10.
import tomli as tomllib
import click
import pathspec
from langgraph_cli._ignore import (
_build_dockerignore_negation_hints,
_build_ignore_spec,
_is_always_excluded,
)
from langgraph_cli.schemas import Config
@dataclass(frozen=True, slots=True)
class UvLockSourceEntry:
name: str
value: object
declared_root: pathlib.Path
pyproject_path: pathlib.Path
@dataclass(slots=True)
class UvLockPackage:
name: str
normalized_name: str
root: pathlib.Path
pyproject_path: pathlib.Path
raw_dependency_specs: object
raw_uv_tool: object
# Set after validation:
package_enabled: bool = False
dependency_names: tuple[str, ...] = ()
workspace_dependencies: tuple[str, ...] = ()
@dataclass(frozen=True, slots=True)
class UvLockWorkspace:
raw_root_source_entries: object
packages_by_name: dict[str, UvLockPackage]
packages_by_root: dict[pathlib.Path, UvLockPackage]
@dataclass(frozen=True, slots=True)
class UvLockPlan:
project_root: pathlib.Path
pyproject_path: pathlib.Path
uv_lock_path: pathlib.Path
target: UvLockPackage
target_root: pathlib.Path
install_order: tuple[UvLockPackage, ...]
container_roots: dict[pathlib.Path, pathlib.PurePosixPath]
working_dir: str
all_workspace_roots: frozenset[pathlib.Path] = frozenset()
@dataclass(slots=True)
class DockerBuildPlan:
lines: list[str]
def add_blank(self) -> None:
self.lines.append("")
def add_raw(self, line: str) -> None:
self.lines.append(line)
def add_instruction(self, opcode: str, value: str | None = None) -> None:
if value:
self.lines.append(f"{opcode} {value}")
else:
self.lines.append(opcode)
def extend_nonempty(self, items: list[str]) -> None:
self.lines.extend(item for item in items if item)
def render(self) -> str:
return os.linesep.join(self.lines)
def _normalize_package_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
def _parse_dependency_name(
dependency: str, *, package_name: str, pyproject_path: pathlib.Path
) -> str:
match = re.match(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)", dependency)
if not match:
raise click.UsageError(
"source.kind 'uv' only supports PEP 508 dependency strings "
f"with an explicit package name. Could not parse dependency "
f"{dependency!r} in {pyproject_path} for package '{package_name}'."
)
return _normalize_package_name(match.group(1))
def _load_pyproject(pyproject_path: pathlib.Path) -> dict:
with pyproject_path.open("rb") as pyproject_file:
return tomllib.load(pyproject_file)
def _get_dependency_names(
dependency_specs: object, *, package_name: str, pyproject_path: pathlib.Path
) -> tuple[str, ...]:
if dependency_specs is None:
dependency_specs = []
if not isinstance(dependency_specs, list) or not all(
isinstance(spec, str) for spec in dependency_specs
):
raise click.UsageError(
"source.kind 'uv' requires [project].dependencies to be a "
f"list of strings in {pyproject_path}."
)
return tuple(
_parse_dependency_name(
spec,
package_name=package_name,
pyproject_path=pyproject_path,
)
for spec in dependency_specs
)
def _validate_uv_lock_source_entry(
*,
source_name: str,
source_value: object,
declared_root: pathlib.Path,
pyproject_path: pathlib.Path,
project_root: pathlib.Path,
packages_by_name: dict[str, UvLockPackage],
packages_by_root: dict[pathlib.Path, UvLockPackage],
) -> set[str]:
workspace_dependencies: set[str] = set()
if isinstance(source_value, list):
for item in source_value:
workspace_dependencies.update(
_validate_uv_lock_source_entry(
source_name=source_name,
source_value=item,
declared_root=declared_root,
pyproject_path=pyproject_path,
project_root=project_root,
packages_by_name=packages_by_name,
packages_by_root=packages_by_root,
)
)
return workspace_dependencies
if not isinstance(source_value, dict):
return workspace_dependencies
normalized_source_name = _normalize_package_name(source_name)
package: UvLockPackage | None = None
if source_value.get("workspace") is True:
package = packages_by_name.get(normalized_source_name)
if package is None:
raise click.UsageError(
f"'{source_name}' in {pyproject_path} is marked as "
"`{{ workspace = true }}` but no matching workspace package was "
f"found under project_root ({project_root}). Check that "
f"'{source_name}' appears in [tool.uv.workspace].members."
)
if not _get_uv_lock_package_enabled(package):
raise click.UsageError(
f"'{source_name}' in {pyproject_path} is a workspace dependency "
f"but sets `tool.uv.package = false` in {package.pyproject_path}. "
"Workspace dependencies must be buildable packages."
)
workspace_dependencies.add(package.normalized_name)
path_ref = source_value.get("path")
if isinstance(path_ref, str):
if (
pathlib.Path(path_ref).is_absolute()
or pathlib.PurePosixPath(path_ref).is_absolute()
):
raise click.UsageError(
f"'{source_name}' in {pyproject_path} uses an absolute path "
f"({path_ref}), which is not supported. Use a relative path or "
"`{{ workspace = true }}` instead."
)
resolved = (declared_root / path_ref).resolve()
if project_root != resolved and project_root not in resolved.parents:
raise click.UsageError(
f"'{source_name}' in {pyproject_path} uses a path source that "
f"resolves to {resolved}, which is outside project_root "
f"({project_root})."
)
package = packages_by_root.get(resolved)
if package is None:
raise click.UsageError(
f"'{source_name}' in {pyproject_path} uses a path source that "
f"resolves to {resolved}, which is not a workspace package under "
f"project_root ({project_root})."
)
if package.normalized_name != normalized_source_name:
raise click.UsageError(
f"'{source_name}' in {pyproject_path} points to {resolved}, which "
f"defines package '{package.name}'. The dependency name and the "
"workspace package name must match."
)
if not _get_uv_lock_package_enabled(package):
raise click.UsageError(
f"'{source_name}' in {pyproject_path} resolves to workspace package "
f"'{package.name}', which sets `tool.uv.package = false` in "
f"{package.pyproject_path}. Workspace dependencies must be "
"buildable packages."
)
workspace_dependencies.add(package.normalized_name)
for key, nested_value in source_value.items():
if key in {"workspace", "path"}:
continue
workspace_dependencies.update(
_validate_uv_lock_source_entry(
source_name=source_name,
source_value=nested_value,
declared_root=declared_root,
pyproject_path=pyproject_path,
project_root=project_root,
packages_by_name=packages_by_name,
packages_by_root=packages_by_root,
)
)
return workspace_dependencies
def _get_uv_lock_package_enabled(package: UvLockPackage) -> bool:
uv_tool = package.raw_uv_tool
if uv_tool and not isinstance(uv_tool, dict):
raise click.UsageError(
"source.kind 'uv' requires [tool.uv] to be a table "
f"in {package.pyproject_path}."
)
package_enabled = (
uv_tool.get("package", True) if isinstance(uv_tool, dict) else True
)
if not isinstance(package_enabled, bool):
raise click.UsageError(
"source.kind 'uv' requires [tool.uv].package to be a boolean "
f"in {package.pyproject_path}."
)
return package_enabled
def _get_uv_lock_source_entries(
package: UvLockPackage,
*,
project_root: pathlib.Path,
root_pyproject_path: pathlib.Path,
raw_root_source_entries: object,
) -> tuple[UvLockSourceEntry, ...]:
if not isinstance(raw_root_source_entries, dict):
raise click.UsageError(
"source.kind 'uv' requires [tool.uv.sources] to be a table "
f"in {root_pyproject_path}."
)
uv_tool = package.raw_uv_tool
package_source_entries = (
uv_tool.get("sources", {}) if isinstance(uv_tool, dict) else {}
)
if not isinstance(package_source_entries, dict):
raise click.UsageError(
"source.kind 'uv' requires [tool.uv.sources] to be a table "
f"in {package.pyproject_path}."
)
source_entries: dict[str, UvLockSourceEntry] = {
source_name: UvLockSourceEntry(
name=source_name,
value=source_value,
declared_root=project_root,
pyproject_path=root_pyproject_path,
)
for source_name, source_value in raw_root_source_entries.items()
}
source_entries.update(
{
source_name: UvLockSourceEntry(
name=source_name,
value=source_value,
declared_root=package.root,
pyproject_path=package.pyproject_path,
)
for source_name, source_value in package_source_entries.items()
}
)
return tuple(source_entries.values())
def _validate_uv_lock_package(
package: UvLockPackage,
*,
project_root: pathlib.Path,
root_pyproject_path: pathlib.Path,
raw_root_source_entries: object,
packages_by_name: dict[str, UvLockPackage],
packages_by_root: dict[pathlib.Path, UvLockPackage],
) -> None:
dependency_names = _get_dependency_names(
package.raw_dependency_specs,
package_name=package.name,
pyproject_path=package.pyproject_path,
)
dependency_name_set = set(dependency_names)
workspace_dependency_names: set[str] = set()
for source_entry in _get_uv_lock_source_entries(
package,
project_root=project_root,
root_pyproject_path=root_pyproject_path,
raw_root_source_entries=raw_root_source_entries,
):
if _normalize_package_name(source_entry.name) not in dependency_name_set:
continue
workspace_dependency_names.update(
_validate_uv_lock_source_entry(
source_name=source_entry.name,
source_value=source_entry.value,
declared_root=source_entry.declared_root,
pyproject_path=source_entry.pyproject_path,
project_root=project_root,
packages_by_name=packages_by_name,
packages_by_root=packages_by_root,
)
)
package.package_enabled = _get_uv_lock_package_enabled(package)
package.dependency_names = dependency_names
package.workspace_dependencies = tuple(
dependency_name
for dependency_name in dependency_names
if dependency_name in workspace_dependency_names
)
def _discover_uv_lock_workspace_packages(
project_root: pathlib.Path, pyproject_path: pathlib.Path
) -> UvLockWorkspace:
root_data = _load_pyproject(pyproject_path)
root_source_entries = root_data.get("tool", {}).get("uv", {}).get("sources", {})
candidate_roots: list[pathlib.Path] = []
root_project = root_data.get("project", {})
if isinstance(root_project, dict) and isinstance(root_project.get("name"), str):
candidate_roots.append(project_root)
workspace_members = (
root_data.get("tool", {}).get("uv", {}).get("workspace", {}).get("members", [])
)
if workspace_members and not isinstance(workspace_members, list):
raise click.UsageError(
"source.kind 'uv' requires [tool.uv.workspace].members to be a list."
)
for pattern in workspace_members:
if not isinstance(pattern, str):
raise click.UsageError(
"source.kind 'uv' requires every [tool.uv.workspace].members "
"entry to be a string."
)
for match in sorted(project_root.glob(pattern)):
package_root = match if match.is_dir() else match.parent
package_root = package_root.resolve()
if (package_root / "pyproject.toml").is_file():
candidate_roots.append(package_root)
unique_roots: list[pathlib.Path] = []
seen_roots: set[pathlib.Path] = set()
for root in candidate_roots:
if root not in seen_roots:
unique_roots.append(root)
seen_roots.add(root)
packages: list[UvLockPackage] = []
for package_root in unique_roots:
member_pyproject_path = package_root / "pyproject.toml"
pyproject_data = _load_pyproject(member_pyproject_path)
project_data = pyproject_data.get("project", {})
package_name = (
project_data.get("name") if isinstance(project_data, dict) else None
)
if not isinstance(package_name, str) or not package_name.strip():
raise click.UsageError(
"source.kind 'uv' requires every workspace package to define "
f"[project].name in {member_pyproject_path}."
)
packages.append(
UvLockPackage(
name=package_name,
normalized_name=_normalize_package_name(package_name),
root=package_root,
pyproject_path=member_pyproject_path,
raw_dependency_specs=project_data.get("dependencies", []),
raw_uv_tool=pyproject_data.get("tool", {}).get("uv", {}),
)
)
packages_by_name: dict[str, UvLockPackage] = {}
packages_by_root: dict[pathlib.Path, UvLockPackage] = {}
for package in packages:
existing = packages_by_name.get(package.normalized_name)
if existing is not None:
raise click.UsageError(
"source.kind 'uv' requires unique workspace package names, "
f"but both {existing.pyproject_path} and {package.pyproject_path} "
f"define '{package.name}'."
)
packages_by_name[package.normalized_name] = package
packages_by_root[package.root] = package
return UvLockWorkspace(
raw_root_source_entries=root_source_entries,
packages_by_name=packages_by_name,
packages_by_root=packages_by_root,
)
def _container_workspace_root() -> pathlib.PurePosixPath:
return pathlib.PurePosixPath("/deps/workspace")
def _container_root_for_uv_lock_package(
project_root: pathlib.Path, package_root: pathlib.Path
) -> pathlib.PurePosixPath:
container_root = _container_workspace_root()
relative_root = package_root.relative_to(project_root)
if relative_root == pathlib.Path("."):
return container_root
return container_root.joinpath(*relative_root.parts)
def _uv_lock_package_copy_items(
package: UvLockPackage,
plan: UvLockPlan,
ignore_spec: pathspec.PathSpec,
) -> tuple[tuple[pathlib.PurePosixPath, pathlib.PurePosixPath], ...]:
# Skip entries that .dockerignore / built-in exclusions would strip from
# the build context. Emitting `ADD <path>` for a file that Docker has
# filtered out causes the build to fail with
# "failed to compute cache key: <path> not found".
if package.root != plan.project_root:
relative_root = pathlib.PurePosixPath(
*package.root.relative_to(plan.project_root).parts
)
if _is_always_excluded(relative_root, is_dir=True) or ignore_spec.match_file(
f"{relative_root.as_posix()}/"
):
raise click.UsageError(
f"Workspace member '{package.name}' at {relative_root} is "
"excluded from the Docker build context, but uv.lock requires "
"it to be copied into the build context. Remove the matching "
"pattern or drop the member from [tool.uv.workspace].members."
)
return ((relative_root, plan.container_roots[package.root]),)
root_container = plan.container_roots[package.root]
workspace_member_roots = plan.all_workspace_roots - {plan.project_root}
negated_dockerignore_hints = _build_dockerignore_negation_hints(plan.project_root)
def iter_entries(
current_dir: pathlib.Path,
) -> tuple[tuple[pathlib.PurePosixPath, pathlib.PurePosixPath], ...]:
entries: list[tuple[pathlib.PurePosixPath, pathlib.PurePosixPath]] = []
for child in sorted(current_dir.iterdir(), key=lambda path: path.name):
if child in workspace_member_roots:
# Workspace members are copied separately if they are in the closure,
# and excluded entirely otherwise.
continue
relative_child = pathlib.PurePosixPath(
*child.relative_to(plan.project_root).parts
)
is_dir = child.is_dir()
if _is_always_excluded(relative_child, is_dir=is_dir):
continue
ignored = ignore_spec.match_file(
f"{relative_child.as_posix()}/" if is_dir else relative_child.as_posix()
)
is_workspace_parent = is_dir and any(
child in ws_root.parents for ws_root in workspace_member_roots
)
if is_workspace_parent:
entries.extend(iter_entries(child))
continue
if (
is_dir
and ignored
and negated_dockerignore_hints.requires_dir_walk(relative_child)
):
entries.extend(iter_entries(child))
continue
if ignored:
continue
entries.append(
(relative_child, root_container.joinpath(*relative_child.parts))
)
return tuple(entries)
return iter_entries(plan.project_root)
def _resolve_uv_lock_container_path(
host_path: pathlib.Path, plan: UvLockPlan
) -> pathlib.PurePosixPath | None:
for package_root in sorted(
plan.container_roots, key=lambda path: len(path.parts), reverse=True
):
if host_path == package_root or package_root in host_path.parents:
# Guard against the workspace root matching paths inside
# unrelated workspace members that are not in the closure.
if plan.all_workspace_roots and _path_in_unrelated_member(
host_path, package_root, plan
):
continue
relative_path = host_path.relative_to(package_root)
container_root = plan.container_roots[package_root]
if relative_path == pathlib.Path("."):
return container_root
return container_root.joinpath(*relative_path.parts)
return None
def _path_in_unrelated_member(
host_path: pathlib.Path,
matched_root: pathlib.Path,
plan: UvLockPlan,
) -> bool:
"""Return True if host_path is inside a workspace member NOT in the closure.
When matched_root is the project root (workspace root), it is a parent of
every file in the workspace. We need to reject paths that actually belong
to a sibling workspace member that was not selected for deployment.
"""
for ws_root in plan.all_workspace_roots:
if ws_root == matched_root:
continue
# ws_root is more specific than matched_root
if ws_root in matched_root.parents:
continue
if host_path == ws_root or ws_root in host_path.parents:
# host_path is inside this workspace member
if ws_root not in plan.container_roots:
return True
return False
def _infer_uv_lock_target_package(
*,
config_root: pathlib.Path,
project_root: pathlib.Path,
source: dict,
packages_by_name: dict[str, UvLockPackage],
packages_by_root: dict[pathlib.Path, UvLockPackage],
) -> UvLockPackage:
package_name = source.get("package")
if package_name is not None:
if not isinstance(package_name, str) or not package_name.strip():
raise click.UsageError("`source.package` must be a non-empty string.")
target = packages_by_name.get(_normalize_package_name(package_name))
if target is None:
available_packages = ", ".join(
sorted(package.name for package in packages_by_name.values())
)
raise click.UsageError(
"Could not find source.package "
f"'{package_name}' in the uv project at {project_root}. "
"It must match a [project].name from one of the discovered "
f"packages. Available packages: {available_packages or '(none)'}."
)
return target
containing_packages = sorted(
(
package
for package_root, package in packages_by_root.items()
if config_root == package_root or package_root in config_root.parents
),
key=lambda package: len(package.root.parts),
reverse=True,
)
if containing_packages:
target = containing_packages[0]
if (
target.root != project_root
or len(packages_by_name) == 1
or config_root == project_root
):
return target
if len(packages_by_name) == 1:
return next(iter(packages_by_name.values()))
available_packages = ", ".join(
sorted(package.name for package in packages_by_name.values())
)
raise click.UsageError(
"source.package is required because source.root resolves to a uv "
"workspace with multiple packages and no unique target package could be "
f"inferred from langgraph.json at {config_root}. Available packages: "
f"{available_packages or '(none)'}. "
"Move langgraph.json into the target package or set source.package."
)
def _plan_uv_lock_workspace(config_path: pathlib.Path, config: Config) -> UvLockPlan:
config_root = config_path.parent.resolve()
source = config["source"]
root = source.get("root", ".")
if not isinstance(root, str) or not root.strip():
raise click.UsageError('`source.root` must be a non-empty string. Use `"."`.')
project_root = (config_root / root).resolve()
pyproject_path = project_root / "pyproject.toml"
uv_lock_path = project_root / "uv.lock"
if not uv_lock_path.exists():
raise click.UsageError(
f"No uv.lock found at {uv_lock_path}. Your langgraph.json sets "
f"source.root={root!r}, which resolves to "
f"{project_root}. Make sure this is the directory where you run "
"`uv lock` (it should contain both pyproject.toml and uv.lock)."
)
if not pyproject_path.exists():
raise click.UsageError(
f"No pyproject.toml found at {pyproject_path}. Your langgraph.json "
f"sets source.root={root!r}, which resolves to "
f"{project_root}. This should be your uv workspace root."
)
workspace = _discover_uv_lock_workspace_packages(project_root, pyproject_path)
packages_by_name = workspace.packages_by_name
target = _infer_uv_lock_target_package(
config_root=config_root,
project_root=project_root,
source=source,
packages_by_name=packages_by_name,
packages_by_root=workspace.packages_by_root,
)
_validate_uv_lock_package(
target,
project_root=project_root,
root_pyproject_path=pyproject_path,
raw_root_source_entries=workspace.raw_root_source_entries,
packages_by_name=workspace.packages_by_name,
packages_by_root=workspace.packages_by_root,
)
if not target.package_enabled:
raise click.UsageError(
f"'{target.name}' has `tool.uv.package = false` in "
f"{target.pyproject_path}, so it cannot be deployed. Either remove "
"that setting or point `source.package` at a different workspace "
"member."
)
install_order: list[UvLockPackage] = []
visited: set[str] = set()
validated: set[str] = {target.normalized_name}
def visit(package: UvLockPackage) -> None:
if package.normalized_name in visited:
return
visited.add(package.normalized_name)
if package.normalized_name not in validated:
_validate_uv_lock_package(
package,
project_root=project_root,
root_pyproject_path=pyproject_path,
raw_root_source_entries=workspace.raw_root_source_entries,
packages_by_name=workspace.packages_by_name,
packages_by_root=workspace.packages_by_root,
)
validated.add(package.normalized_name)
for dependency_name in package.workspace_dependencies:
dependency = packages_by_name.get(dependency_name)
if dependency is not None:
visit(dependency)
install_order.append(package)
visit(target)
container_roots = {
package.root: _container_root_for_uv_lock_package(project_root, package.root)
for package in install_order
}
all_workspace_roots = frozenset(
package.root for package in packages_by_name.values()
)
working_dir = _resolve_uv_lock_container_path(
config_root,
UvLockPlan(
project_root=project_root,
pyproject_path=pyproject_path,
uv_lock_path=uv_lock_path,
target=target,
target_root=target.root,
install_order=tuple(install_order),
container_roots=container_roots,
working_dir=str(
_container_root_for_uv_lock_package(project_root, target.root)
),
all_workspace_roots=all_workspace_roots,
),
)
if working_dir is None:
working_dir = container_roots[target.root]
return UvLockPlan(
project_root=project_root,
pyproject_path=pyproject_path,
uv_lock_path=uv_lock_path,
target=target,
target_root=target.root,
install_order=tuple(install_order),
container_roots=container_roots,
working_dir=str(working_dir),
all_workspace_roots=all_workspace_roots,
)
def _rewrite_uv_lock_import_path(
config_path: pathlib.Path,
import_str: str,
plan: UvLockPlan,
*,
label: str,
) -> str:
module_str, _, attr_str = import_str.partition(":")
if not module_str or not attr_str:
raise ValueError(
f'Import string "{import_str}" must be in format "<module>:<attribute>".'
)
if "/" not in module_str and "\\" not in module_str:
return import_str
resolved = (config_path.parent / module_str).resolve()
if not resolved.exists():
raise FileNotFoundError(f"Could not find {label}: {resolved}")
if not resolved.is_file():
raise IsADirectoryError(f"{label.capitalize()} must be a file: {resolved}")
container_path = _resolve_uv_lock_container_path(resolved, plan)
if container_path is None:
copied_dirs = ", ".join(
package.root.relative_to(plan.project_root).as_posix() or "."
for package in plan.install_order
)
raise click.UsageError(
f"{label.capitalize()} '{import_str}' resolves to {resolved}, which is "
f"not inside the target package '{plan.target.name}' or any of its "
f"workspace dependencies. Only these directories are copied into the "
f"container: {copied_dirs}. If this file lives in another workspace "
f"package, add it as a dependency of '{plan.target.name}' with "
"`{ workspace = true }` in [tool.uv.sources]."
)
return f"{container_path.as_posix()}:{attr_str}"
def _update_uv_lock_graph_paths(
config_path: pathlib.Path, config: Config, plan: UvLockPlan
) -> None:
for graph_id, data in config["graphs"].items():
if isinstance(data, dict):
if "path" not in data:
raise ValueError(
f"Graph '{graph_id}' must contain a 'path' key if it is a dictionary."
)
config["graphs"][graph_id]["path"] = _rewrite_uv_lock_import_path(
config_path,
data["path"],
plan,
label=f"graph '{graph_id}'",
)
elif isinstance(data, str):
config["graphs"][graph_id] = _rewrite_uv_lock_import_path(
config_path,
data,
plan,
label=f"graph '{graph_id}'",
)
else:
raise ValueError(
f"Graph '{graph_id}' must be a string or a dictionary with a 'path' key."
)
def _update_uv_lock_component_path(
config_path: pathlib.Path,
config: Config,
plan: UvLockPlan,
*,
section: str,
key: str,
label: str,
) -> None:
section_config = config.get(section)
if not isinstance(section_config, dict):
return
path_str = section_config.get(key)
if not isinstance(path_str, str):
return
section_config[key] = _rewrite_uv_lock_import_path(
config_path,
path_str,
plan,
label=label,
)
def _update_uv_lock_ui_paths(
config_path: pathlib.Path, config: Config, plan: UvLockPlan
) -> None:
ui = config.get("ui")
if not isinstance(ui, dict):
return
for ui_name, path_str in ui.items():
if not isinstance(path_str, str):
continue
resolved = (config_path.parent / path_str).resolve()
if not resolved.exists():
raise FileNotFoundError(f"Could not find ui '{ui_name}': {resolved}")
if not resolved.is_file():
raise IsADirectoryError(f"Ui '{ui_name}' must be a file: {resolved}")
container_path = _resolve_uv_lock_container_path(resolved, plan)
if container_path is None:
copied_dirs = ", ".join(
package.root.relative_to(plan.project_root).as_posix() or "."
for package in plan.install_order
)
raise click.UsageError(
f"Ui '{ui_name}' resolves to {resolved}, which is not inside the "
f"target package '{plan.target.name}' or any of its workspace "
f"dependencies. Only these directories are copied into the "
f"container: {copied_dirs}. If this file lives in another "
f"workspace package, add it as a dependency of "
f"'{plan.target.name}' with `{{ workspace = true }}` in "
"[tool.uv.sources]."
)
ui[ui_name] = container_path.as_posix()
def python_config_to_docker_uv_lock(
config_path: pathlib.Path,
config: Config,
base_image: str,
api_version: str | None = None,
*,
build_tools_to_uninstall: tuple[str, ...] | None,
) -> tuple[str, dict[str, str]]:
from langgraph_cli.config import (
DEFAULT_NODE_VERSION,
_build_python_install_commands,
_build_runtime_env_vars,
_get_node_pm_install_cmd,
_get_pip_cleanup_lines,
_image_supports_uv,
docker_tag,
)
if not _image_supports_uv(base_image):
raise ValueError(
"source.kind 'uv' requires a base image with uv support "
"(langchain/langgraph-api >= 0.2.47)"
)
config_root = config_path.parent.resolve()
install_cmd = "uv pip install --system"
_, global_reqs_pip_install, pip_config_file_str = _build_python_install_commands(
config, install_cmd
)
plan = _plan_uv_lock_workspace(config_path, config)
_update_uv_lock_graph_paths(config_path, config, plan)
for section, key in [
("auth", "path"),
("encryption", "path"),
("checkpointer", "path"),
("http", "app"),
]:
_update_uv_lock_component_path(
config_path,
config,
plan,
section=section,
key=key,
label=f"{section}.{key}",
)
_update_uv_lock_ui_paths(config_path, config, plan)
additional_contexts: dict[str, str] = {}
workspace_context_name: str | None = None
if (
plan.project_root != config_root
and config_root not in plan.project_root.parents
):
workspace_context_name = "uv-workspace-root"
additional_contexts[workspace_context_name] = str(plan.project_root)
def copy_from_project_root(
relative_path: pathlib.PurePosixPath, destination: str
) -> str:
if workspace_context_name is not None:
source = relative_path.as_posix() or "."
return f"COPY --from={workspace_context_name} {source} {destination}"
source_path = plan.project_root / pathlib.Path(relative_path)
relative_source = source_path.relative_to(config_root).as_posix()
return f"ADD {relative_source} {destination}"
uv_export_project_dir = "/tmp/uv_export/project"
env_vars = _build_runtime_env_vars(config)
image_str = docker_tag(config, base_image, api_version)
docker_plan = DockerBuildPlan(lines=[])
if additional_contexts:
docker_plan.add_raw("# syntax=docker/dockerfile:1.4")
docker_plan.add_blank()
docker_plan.add_instruction("FROM", image_str)
docker_plan.add_blank()
docker_plan.extend_nonempty(config["dockerfile_lines"])
if config["dockerfile_lines"]:
docker_plan.add_blank()
if (config.get("ui") or config.get("node_version")) and plan.working_dir:
docker_plan.add_instruction("RUN", "/storage/install-node.sh")
docker_plan.add_blank()
if pip_config_file_str:
docker_plan.add_raw(pip_config_file_str)
docker_plan.add_blank()
docker_plan.add_raw("# -- Installing dependencies from uv.lock --")
docker_plan.add_raw(
copy_from_project_root(
pathlib.PurePosixPath("pyproject.toml"),
f"{uv_export_project_dir}/pyproject.toml",
)
)
docker_plan.add_raw(
copy_from_project_root(
pathlib.PurePosixPath("uv.lock"),
f"{uv_export_project_dir}/uv.lock",
)
)
docker_plan.add_instruction("WORKDIR", uv_export_project_dir)
docker_plan.add_instruction(
"RUN",
" ".join(
[
"uv export",
f"--package {shlex.quote(plan.target.name)}",
"--frozen",
"--no-hashes",
"--no-emit-project",
"--no-emit-workspace",
"-o uv_requirements.txt",
]
),
)
docker_plan.add_instruction(
"RUN", f"{global_reqs_pip_install} -r uv_requirements.txt"
)
docker_plan.add_instruction("RUN", "rm -rf /tmp/uv_export")
docker_plan.add_raw("# -- End of uv.lock dependencies install --")
docker_plan.add_blank()
ignore_spec = _build_ignore_spec(plan.project_root, include_gitignore=False)
for package in plan.install_order:
package_label = package.root.relative_to(plan.project_root).as_posix() or "."
docker_plan.add_raw(f"# -- Adding workspace package {package_label} --")
for source, destination in _uv_lock_package_copy_items(
package, plan, ignore_spec
):
docker_plan.add_raw(copy_from_project_root(source, destination.as_posix()))
docker_plan.add_instruction(
"WORKDIR", plan.container_roots[package.root].as_posix()
)
docker_plan.add_instruction("RUN", f"{global_reqs_pip_install} --no-deps -e .")
docker_plan.add_raw(f"# -- End of workspace package {package_label} --")
docker_plan.add_blank()
docker_plan.extend_nonempty(env_vars)
if env_vars:
docker_plan.add_blank()
if (config.get("ui") or config.get("node_version")) and plan.working_dir:
docker_plan.add_raw("# -- Installing JS dependencies --")
docker_plan.add_instruction(
"ENV", f"NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}"
)
docker_plan.add_instruction("WORKDIR", plan.working_dir)
docker_plan.add_instruction(
"RUN",
f"{_get_node_pm_install_cmd(plan.target_root)} && "
"tsx /api/langgraph_api/js/build.mts",
)
docker_plan.add_raw("# -- End of JS dependencies install --")
docker_plan.add_blank()
docker_plan.add_raw(
_get_pip_cleanup_lines(
install_cmd=install_cmd,
to_uninstall=build_tools_to_uninstall,
pip_installer="uv",
)
)
docker_plan.add_blank()
if plan.working_dir:
docker_plan.add_instruction("WORKDIR", plan.working_dir)
return docker_plan.render(), additional_contexts