Hotdry.
systems

Python 单仓库规模化:uv.lock 增量依赖解析与 Dagger hermetic 构建

针对 TB 级 Python monorepo,解析 uv.lock 依赖图实现分区、Dagger 驱动的 hermetic 增量构建、远程执行缓存,提升 CI/CD 效率。

Python monorepo 在大规模代码库中面临核心挑战:依赖图复杂导致全量重建、CI/CD 耗时小时级、缓存失效频繁。为 TB 级(terabyte 规模)代码库,提供 hermetic(纯净可复现)增量构建、远程执行缓存与依赖图分区的实战方案。本文聚焦单一技术点 —— 基于 uv.lock 的增量依赖解析,利用 Dagger 自动化构建,仅复制 transitive 依赖源代码,实现细粒度缓存。

为什么 uv + Dagger 适合 Python monorepo 规模化?

传统 Python polyrepo 依赖发布 / 消费循环引入版本漂移和技术债,而 monorepo 通过本地 editable 依赖确保兼容性。但规模化痛点在于:任意变更可能触发全 repo 重建,测试 / 镜像构建耗时。uv 工作区(workspace)标准化依赖管理,生成共享 uv.lock 文件精确捕捉跨包依赖图(manifest.members + package.dependencies)。Dagger 作为 Python 编写引擎,利用 BuildKit 图形化管道,支持解析 lockfile、选择性文件复制,实现 hermetic 容器化构建。

证据显示,此方案在 140k LoC、70+ 子项目中验证有效:“uv.lock 文件描述整个 monorepo 依赖树,source.editable 指向本地路径。”[1] HN 讨论确认,测试环节缓存是关键瓶颈,此法延伸端到端缓存。[2]

优势:

  • 增量:仅变动的依赖子图重建,缓存命中率 >90%。
  • Hermetic:容器隔离,避免 host 污染。
  • 远程缓存:Dagger Cloud 共享层,分布式团队复用。
  • 分区:递归遍历 dep graph,按项目隔离构建。

核心事实:uv.lock 依赖图解析

uv.lock 是 TOML 格式锁定文件,顶部 [manifest].members 列工作区成员,[[package]] 数组详述每个包(name, version, source.editable, dependencies)。

落地参数

  1. 初始化 monorepo:

    uv init
    mkdir projects
    uv init --package --lib projects/lib-one  # 重复添加多包
    uv add --group dev ruff pyright  # 根 dev 依赖
    uv lock  # 生成 uv.lock
    

    编辑根 pyproject.toml:workspace.members = ["projects/*"]

  2. 添加跨包依赖:

    uv add --package lib-two lib-one  # 自动更新 uv.lock
    

风险阈值:成员 >500 时,lockfile >10MB,解析延迟 <100ms(tomli.loads)。若超,预分区多 workspace。

Dockerfile:hermetic 多阶段 deps 预装

共享 Dockerfile 支持 prod/dev,预装第三方 deps,避免源代码污染缓存。

关键片段(完整见参考):

ARG INCLUDE_DEPENDENCIES=dev
ARG PYTHON_VERSION=3.12.10
FROM python:${PYTHON_VERSION}-slim AS base
COPY --from=ghcr.io/astral-sh/uv:0.5+ /uv /bin/uv
ENV UV_PROJECT_ENVIRONMENT=/usr/local/ UV_PYTHON=/usr/local/bin/python UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_FROZEN=1

FROM base AS deps-${INCLUDE_DEPENDENCIES}
WORKDIR /src
COPY pyproject.toml uv.lock ./
ARG PACKAGE
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --no-install-workspace --only-group dev --inexact --package $PACKAGE  # dev/prod 分支

参数清单

  • --no-install-workspace:忽略本地源,仅第三方。
  • --mount=type=cache:BuildKit 持久化 uv cache。
  • target=deps-dev:Dagger 入口点,仅复制 pyproject.toml + uv.lock(<1MB),缓存稳定。

阈值:第三方 deps >1000 时,deps-dev 层 <500MB;监控 Dockerfile 缓存命中率>80%。

Dagger 模块:增量依赖分区与复制

Dagger Python SDK 解析 uv.lock,递归找 transitive deps,仅复制所需源目录。核心函数 get_project_sources_map

伪代码关键(完整 GitHub):

async def get_project_sources_map(self, uv_lock: File, project: str) -> dict[str, str]:
    uv_lock_dict = tomli.loads(await uv_lock.contents())
    members = set(uv_lock_dict["manifest"]["members"])
    local_projects = {project}
    def find_deps(package_name):  # 递归
        for pkg in uv_lock_dict["package"]:
            if pkg["name"] == package_name:
                for dep in pkg.get("dependencies", []):
                    if isinstance(dep, dict) and dep["name"] in members:
                        local_projects.add(dep["name"])
                        find_deps(dep["name"])
    find_deps(project)
    return {pkg["name"]: pkg["source"]["editable"] for pkg in uv_lock_dict["package"] if pkg["name"] in local_projects}

def copy_source_code(self, container: Container, root_dir: RootDir, project_sources_map: dict):
    for proj, path in project_sources_map.items():
        container = container.with_directory(f"/src/{path}", root_dir.directory(path))
    return container

调用

dagger call build-project --project lib-three  # 自动分区,缓存 deps-dev + 源

扩展管道

@function
async def pytest(self, root_dir: RootDir, project: str) -> str:
    container = await self.build_project(root_dir, project)
    return await container.with_exec(["pytest"]).stdout()

本地 / CI 一致,Dagger Cloud 启用远程:dagger cloud call

规模参数

  • 递归深度阈值:>50 层警报循环依赖。
  • 并行:Dagger/BuildKit 自动,监控 concurrency=CPU*2。
  • 远程缓存:Dagger Cloud 默认 TTL=7d,命中率目标 > 70%。
  • TB 级优化:Depot 集成加速(零配置),分区 >1000 项目用多 uv.lock。

监控与回滚策略

关键指标

指标 阈值 工具
构建时长 <5min / 项目 Dagger logs
缓存命中 >85% BuildKit stats
Lock 解析 <200ms Prometheus
镜像大小 <1GB/dev Docker inspect

回滚:若 uv.lock 变更频繁,fallback Poetry + 手动 graph;极端 TB 用 Bazel(Python rules)。

风险限界

  1. uv 生态年轻,TB 验证少;限 10k LoC 测试。
  2. Dagger 版本锁 v0.16+,Cloud 配额监控。

此方案最小 boilerplate,支持任意结构(如 nested 项目),TB monorepo CI 降至分钟级。立即试:clone 示例 repo,uv sync && dagger call

资料来源: [1] https://gafni.dev/blog/cracking-the-python-monorepo/
[2] https://news.ycombinator.com/item?id=47172608
示例代码:https://github.com/danielgafni/website/tree/master/www/content/blog/cracking-the-python-monorepo/uv-dagger-dream

查看归档