Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/backend/core/build_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
Build Cache Environment
=======================

Compiler-cache environment variables for compiled-language projects.

Fresh worktrees always cold-build, which makes every coder -> QA -> fixer
iteration expensive for C/C++ and Rust projects. ccache and sccache keep
their caches outside the worktree, so pointing the build at them lets a
brand-new worktree reuse object files from previous builds.

Only opt-in mechanisms are used:
- CMake honors CMAKE_<LANG>_COMPILER_LAUNCHER environment variables
- Cargo honors RUSTC_WRAPPER

Plain Make projects are deliberately left alone: overriding CC/CXX can
break builds that expect a bare compiler path.
"""

import os
import shutil
from pathlib import Path


def _is_cmake_project(project_dir: Path) -> bool:
"""Check if the project builds with CMake."""
return (project_dir / "CMakeLists.txt").exists()


def _is_rust_project(project_dir: Path) -> bool:
"""Check if the project builds with Cargo."""
return (project_dir / "Cargo.toml").exists()


def get_build_cache_env(
project_dir: Path, existing_env: dict[str, str] | None = None
) -> dict[str, str]:
"""
Get compiler-cache env vars for the agent session.

Args:
project_dir: Root directory of the project being built
existing_env: Env vars already collected for the SDK subprocess;
variables present there (or in os.environ) are never overridden

Returns:
Dict of env vars to add (empty when no cache tool applies)
"""
project_dir = Path(project_dir)
existing = {**os.environ, **(existing_env or {})}
env: dict[str, str] = {}

if _is_cmake_project(project_dir) and shutil.which("ccache"):
for var in ("CMAKE_C_COMPILER_LAUNCHER", "CMAKE_CXX_COMPILER_LAUNCHER"):
if var not in existing:
env[var] = "ccache"

if _is_rust_project(project_dir) and shutil.which("sccache"):
if "RUSTC_WRAPPER" not in existing:
env["RUSTC_WRAPPER"] = "sccache"

return env


__all__ = ["get_build_cache_env"]
8 changes: 8 additions & 0 deletions apps/backend/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def invalidate_project_cache(project_dir: Path | None = None) -> None:
require_auth_token,
validate_token_not_encrypted,
)
from core.build_cache import get_build_cache_env
from core.providers.config import get_provider_config
from enterprise.data_residency import get_data_residency_config
from linear_updater import is_linear_enabled
Expand Down Expand Up @@ -942,6 +943,13 @@ def create_client(
# Collect env vars to pass to SDK (ANTHROPIC_BASE_URL, etc.)
sdk_env = get_sdk_env_vars()

# Reuse compiler caches across fresh worktrees for compiled stacks
# (ccache for CMake C/C++ builds, sccache for Cargo builds)
build_cache_env = get_build_cache_env(project_dir, sdk_env)
if build_cache_env:
sdk_env.update(build_cache_env)
logger.info(f"Build cache enabled: {', '.join(sorted(build_cache_env))}")

# Configure data residency (regional API endpoints)
data_residency_config = get_data_residency_config()
regional_endpoint = data_residency_config.get_endpoint()
Expand Down
1 change: 1 addition & 0 deletions apps/backend/project/command_registry/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"rustup",
"rustfmt",
"rust-analyzer",
"sccache", # Compiler cache (RUSTC_WRAPPER)
# Cargo subcommand binaries
"cargo-clippy",
"cargo-fmt",
Expand Down
127 changes: 127 additions & 0 deletions tests/test_build_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
Tests for the build_cache module.

Tests cover:
- ccache env injection for CMake projects
- sccache env injection for Cargo projects
- Respecting user-set variables
- No-op behavior when tools are missing or project is not compiled
"""

# Add auto-claude to path for imports
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))

from core.build_cache import get_build_cache_env


@pytest.fixture
def temp_dir(tmp_path):
"""Alias fixture for a temporary project directory."""
return tmp_path


def _which_factory(available: set[str]):
"""Return a shutil.which stand-in knowing only the given tools."""

def _which(name: str):
return f"/usr/bin/{name}" if name in available else None

return _which


class TestBuildCacheEnv:
"""Tests for get_build_cache_env."""

def test_cmake_project_with_ccache(self, temp_dir):
"""CMake project gets compiler launcher vars when ccache exists."""
(temp_dir / "CMakeLists.txt").write_text("project(demo)")

with patch("core.build_cache.shutil.which", _which_factory({"ccache"})):
env = get_build_cache_env(temp_dir)

assert env["CMAKE_C_COMPILER_LAUNCHER"] == "ccache"
assert env["CMAKE_CXX_COMPILER_LAUNCHER"] == "ccache"

def test_cmake_project_without_ccache(self, temp_dir):
"""No launcher vars when ccache is not installed."""
(temp_dir / "CMakeLists.txt").write_text("project(demo)")

with patch("core.build_cache.shutil.which", _which_factory(set())):
env = get_build_cache_env(temp_dir)

assert env == {}

def test_rust_project_with_sccache(self, temp_dir):
"""Cargo project gets RUSTC_WRAPPER when sccache exists."""
(temp_dir / "Cargo.toml").write_text('[package]\nname = "demo"')

with patch("core.build_cache.shutil.which", _which_factory({"sccache"})):
env = get_build_cache_env(temp_dir)

assert env["RUSTC_WRAPPER"] == "sccache"

def test_user_set_vars_not_overridden(self, temp_dir):
"""Variables already present in the session env are respected."""
(temp_dir / "CMakeLists.txt").write_text("project(demo)")
(temp_dir / "Cargo.toml").write_text('[package]\nname = "demo"')

existing = {
"CMAKE_C_COMPILER_LAUNCHER": "distcc",
"RUSTC_WRAPPER": "my-wrapper",
}
with patch(
"core.build_cache.shutil.which",
_which_factory({"ccache", "sccache"}),
):
env = get_build_cache_env(temp_dir, existing)

assert "CMAKE_C_COMPILER_LAUNCHER" not in env
assert "RUSTC_WRAPPER" not in env
# CXX launcher was not pre-set, so it is still added
assert env["CMAKE_CXX_COMPILER_LAUNCHER"] == "ccache"

def test_os_environ_vars_not_overridden(self, temp_dir):
"""Variables set in os.environ are respected."""
(temp_dir / "CMakeLists.txt").write_text("project(demo)")

with (
patch("core.build_cache.shutil.which", _which_factory({"ccache"})),
patch.dict(
"core.build_cache.os.environ",
{"CMAKE_C_COMPILER_LAUNCHER": "distcc"},
),
):
env = get_build_cache_env(temp_dir)

assert "CMAKE_C_COMPILER_LAUNCHER" not in env
assert env["CMAKE_CXX_COMPILER_LAUNCHER"] == "ccache"

def test_non_compiled_project_is_noop(self, temp_dir):
"""Python/JS projects get no build-cache vars."""
(temp_dir / "package.json").write_text("{}")
(temp_dir / "requirements.txt").write_text("flask\n")

with patch(
"core.build_cache.shutil.which",
_which_factory({"ccache", "sccache"}),
):
env = get_build_cache_env(temp_dir)

assert env == {}

def test_make_only_project_is_noop(self, temp_dir):
"""Plain Make projects are left alone (no CC/CXX override)."""
(temp_dir / "Makefile").write_text("all:\n\tgcc main.c\n")
(temp_dir / "main.c").write_text("int main(void) { return 0; }")

with patch("core.build_cache.shutil.which", _which_factory({"ccache"})):
env = get_build_cache_env(temp_dir)

assert env == {}
Loading