tudragon154203 Claude Happy commited on
Commit
172e5d3
·
1 Parent(s): 79aa9eb

build(docker): fix Rust extension build in container

Browse files

Adds Rust toolchain, libssl-dev, and pkg-config to builder stage.
Updates Dockerfile to build extension via script and copy binary
to runtime image, ensuring 'headroom._core' availability.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

Files changed (2) hide show
  1. Dockerfile +19 -3
  2. scripts/build_rust_extension.sh +27 -24
Dockerfile CHANGED
@@ -11,14 +11,22 @@ ARG PYTHON_SITE_PACKAGES=/usr/local/lib/python${PYTHON_VERSION}/site-packages
11
  FROM python:${PYTHON_VERSION}-slim@${PYTHON_DIGEST} AS builder
12
 
13
  ARG UV_VERSION
 
14
 
15
  RUN apt-get update && \
16
  apt-get install -y --no-install-recommends \
17
  build-essential \
18
  g++ \
 
 
 
 
19
  && rm -rf /var/lib/apt/lists/*
20
 
21
- RUN python -m pip install --no-cache-dir uv==${UV_VERSION}
 
 
 
22
 
23
  WORKDIR /build
24
 
@@ -30,10 +38,16 @@ ARG HEADROOM_EXTRAS=proxy,code,ml
30
  RUN --mount=type=cache,target=/root/.cache/uv \
31
  uv pip install --system ".[${HEADROOM_EXTRAS}]"
32
 
33
- # Layer 2: copy real source, reinstall only headroom-ai (no deps)
 
34
  COPY headroom/ headroom/
 
 
 
 
35
  RUN --mount=type=cache,target=/root/.cache/uv \
36
- uv pip install --system --no-deps --reinstall-package headroom-ai .
 
37
 
38
  # ---- Runtime stage (python-slim): supports root/nonroot via build arg ----
39
  FROM python:${PYTHON_VERSION}-slim@${PYTHON_DIGEST} AS runtime-slim-base
@@ -46,6 +60,8 @@ RUN apt-get update && \
46
  rm -rf /var/lib/apt/lists/*
47
 
48
  COPY --from=builder ${PYTHON_SITE_PACKAGES} ${PYTHON_SITE_PACKAGES}
 
 
49
  COPY --from=builder /usr/local/bin/headroom /usr/local/bin/headroom
50
 
51
  RUN mkdir -p /home/nonroot /data && \
 
11
  FROM python:${PYTHON_VERSION}-slim@${PYTHON_DIGEST} AS builder
12
 
13
  ARG UV_VERSION
14
+ ARG PYTHON_SITE_PACKAGES
15
 
16
  RUN apt-get update && \
17
  apt-get install -y --no-install-recommends \
18
  build-essential \
19
  g++ \
20
+ curl \
21
+ libssl-dev \
22
+ pkg-config \
23
+ unzip \
24
  && rm -rf /var/lib/apt/lists/*
25
 
26
+ RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain stable
27
+ ENV PATH="/root/.cargo/bin:${PATH}"
28
+
29
+ RUN python -m pip install --no-cache-dir uv==${UV_VERSION} maturin
30
 
31
  WORKDIR /build
32
 
 
38
  RUN --mount=type=cache,target=/root/.cache/uv \
39
  uv pip install --system ".[${HEADROOM_EXTRAS}]"
40
 
41
+ # Layer 2: copy real source, reinstall only headroom-ai (no deps), then build
42
+ # the Rust extension via maturin build (no venv needed — uses system Python).
43
  COPY headroom/ headroom/
44
+ COPY Cargo.toml Cargo.lock ./
45
+ COPY crates/ crates/
46
+ COPY scripts/build_rust_extension.sh scripts/build_rust_extension.sh
47
+ COPY .cargo/ .cargo/
48
  RUN --mount=type=cache,target=/root/.cache/uv \
49
+ uv pip install --system --no-deps --reinstall-package headroom-ai . && \
50
+ bash ./scripts/build_rust_extension.sh
51
 
52
  # ---- Runtime stage (python-slim): supports root/nonroot via build arg ----
53
  FROM python:${PYTHON_VERSION}-slim@${PYTHON_DIGEST} AS runtime-slim-base
 
60
  rm -rf /var/lib/apt/lists/*
61
 
62
  COPY --from=builder ${PYTHON_SITE_PACKAGES} ${PYTHON_SITE_PACKAGES}
63
+ COPY --from=builder /build/headroom/_core*.so /build/headroom/_core*.pyd /build/headroom/_core*.dylib /src/headroom/ 2>/dev/null || true
64
+ COPY --from=builder /build/headroom/_core*.so /build/headroom/_core*.pyd /build/headroom/_core*.dylib /usr/local/lib/python${PYTHON_VERSION}/site-packages/headroom/ 2>/dev/null || true
65
  COPY --from=builder /usr/local/bin/headroom /usr/local/bin/headroom
66
 
67
  RUN mkdir -p /home/nonroot /data && \
scripts/build_rust_extension.sh CHANGED
@@ -1,48 +1,51 @@
1
  #!/usr/bin/env bash
2
- # Build the Rust → Python extension (headroom._core) and link it into the
3
- # in-tree `headroom/` package so `import headroom._core` resolves.
4
  #
5
- # Why a wrapper script: `maturin develop` builds the binary into the venv,
6
- # but the in-tree `headroom/` source directory (loaded via `pip install -e .`)
7
- # shadows that on sys.path. Python finds `headroom/__init__.py` at the project
8
- # root before reaching the installed extension, so `import headroom._core`
9
- # fails. Copying the built binary into `headroom/` fixes the lookup with zero
10
- # symlinks and works on Windows.
11
  #
12
- # Idempotent. Safe to run repeatedly. Requires `maturin` in PATH (i.e.
13
- # inside the project venv).
14
 
15
  set -euo pipefail
16
 
17
  cd "$(dirname "$0")/.."
18
 
19
  if ! command -v maturin >/dev/null 2>&1; then
20
- echo "error: maturin not found. Activate the venv first:" >&2
21
- echo " source .venv/bin/activate (Unix)" >&2
22
- echo " .venv\\Scripts\\activate (Windows)" >&2
23
  exit 1
24
  fi
25
 
26
- # Build the wheel + install into the venv site-packages.
27
- maturin develop -m crates/headroom-py/Cargo.toml
28
 
29
- # Locate the built extension binary. Maturin writes it under
30
- # `crates/headroom-py/python/headroom/_core.{so,dylib,pyd}`.
31
- EXT_FILE=$(find crates/headroom-py/python/headroom -maxdepth 1 \
 
 
 
 
 
 
 
 
 
 
32
  -name "_core.cpython-*.so" -o -name "_core.cpython-*.dylib" -o -name "_core.pyd" \
33
  2>/dev/null | head -1)
34
 
35
  if [[ -z "$EXT_FILE" ]]; then
36
- echo "error: maturin develop succeeded but produced no _core.* binary." >&2
 
37
  exit 1
38
  fi
39
 
40
- # Copy into the in-tree package dir. Uses cp instead of ln -sf because
41
- # Windows (MSYS2/Git Bash) lacks reliable symlink support and requires
42
- # elevated privileges for native symlinks.
43
  DEST="headroom/$(basename "$EXT_FILE")"
44
  cp -f "$EXT_FILE" "$DEST"
45
- echo "installed: $DEST (from $EXT_FILE)"
 
46
 
47
- # Smoke-test the import to fail loudly if anything is misconfigured.
48
  python -c "from headroom._core import DiffCompressor; print('headroom._core OK:', DiffCompressor)"
 
1
  #!/usr/bin/env bash
2
+ # Build Rust → Python extension (headroom._core) and copy it into in-tree
3
+ # `headroom/` so `import headroom._core` resolves.
4
  #
5
+ # `maturin build` works without venv activation, so this script runs inside
6
+ # Docker and on local shells. It builds a wheel, extracts the compiled
7
+ # extension, and copies it into `headroom/` to beat `pip install -e .` path
8
+ # shadowing.
 
 
9
  #
10
+ # Idempotent. Safe to run repeatedly. Requires `maturin` and `python` in PATH.
 
11
 
12
  set -euo pipefail
13
 
14
  cd "$(dirname "$0")/.."
15
 
16
  if ! command -v maturin >/dev/null 2>&1; then
17
+ echo "error: maturin not found. Install it (e.g. pip install maturin)." >&2
 
 
18
  exit 1
19
  fi
20
 
21
+ maturin build --release --interpreter python --out target/wheels \
22
+ --manifest-path crates/headroom-py/Cargo.toml
23
 
24
+ WHEEL=$(find target/wheels -name "*.whl" -type f 2>/dev/null | head -1)
25
+
26
+ if [[ -z "$WHEEL" ]]; then
27
+ echo "error: maturin build succeeded but produced no wheel." >&2
28
+ exit 1
29
+ fi
30
+
31
+ echo "Built wheel: $WHEEL"
32
+
33
+ TMPDIR=$(mktemp -d)
34
+ unzip -qo "$WHEEL" -d "$TMPDIR"
35
+
36
+ EXT_FILE=$(find "$TMPDIR/headroom" -maxdepth 1 \
37
  -name "_core.cpython-*.so" -o -name "_core.cpython-*.dylib" -o -name "_core.pyd" \
38
  2>/dev/null | head -1)
39
 
40
  if [[ -z "$EXT_FILE" ]]; then
41
+ echo "error: wheel did not contain headroom/_core.* binary." >&2
42
+ rm -rf "$TMPDIR"
43
  exit 1
44
  fi
45
 
 
 
 
46
  DEST="headroom/$(basename "$EXT_FILE")"
47
  cp -f "$EXT_FILE" "$DEST"
48
+ rm -rf "$TMPDIR"
49
+ echo "installed: $DEST (from $WHEEL)"
50
 
 
51
  python -c "from headroom._core import DiffCompressor; print('headroom._core OK:', DiffCompressor)"