Skip to content
Draft
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
28 changes: 16 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,23 +200,27 @@ openshell sandbox create -- codex

Mise preserves an existing `SCCACHE_DIR` so each environment can choose where
to store compiler cache entries. When `SCCACHE_DIR` is unset, OpenShell uses
the worktree-local `.cache/sccache` directory. To make cache entries available
to multiple worktrees on a workstation, set the variable to a user-level
directory before activating mise. For example:
the user-level `openshell/sccache` directory under the platform cache directory.
All worktrees therefore share compiler outputs while retaining separate Cargo
artifacts in each worktree's `target/` directory.

The sccache daemon retains the environment with which it started. After changing
`SCCACHE_DIR` or other sccache settings, inspect the requested and active
configuration:

```shell
mise run rust:cache:status
```

If the requested and active settings differ, refresh the daemon when no other
builds are running:

```shell
export SCCACHE_DIR="$HOME/.cache/openshell/sccache"
mise run rust:cache:refresh
```

CI can select a different directory or configure a remote sccache backend
without changing the workstation setting. Cargo output remains in each
worktree's `target/` directory.

OpenShell does not set `SCCACHE_BASEDIRS`. Sccache loads base directories when
its machine-local daemon starts, but the correct workspace root differs for
each worktree. Cache reuse therefore depends on the compiler inputs: outputs
that embed absolute paths, including Rust dependencies in some builds, can
still miss across worktrees.
without changing the workstation defaults.

## Main Tasks

Expand Down
14 changes: 9 additions & 5 deletions crates/openshell-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
use std::env;
use std::path::{Path, PathBuf};

#[path = "build_support/git.rs"]
mod git;

const PROTO_REL: &str = "../../proto";

fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);

// --- Git-derived version ---
// Compute a version from `git describe` for local builds. In Docker/CI
// builds where .git is absent, this silently does nothing and the binary
// falls back to CARGO_PKG_VERSION (which is already sed-patched by the
// build pipeline).
println!("cargo:rerun-if-changed=../../.git/HEAD");
println!("cargo:rerun-if-changed=../../.git/refs/tags");
git::emit_rerun_if_changed(&manifest_dir);

if let Some(version) = git_version() {
if let Some(version) = git_version(&manifest_dir) {
println!("cargo:rustc-env=OPENSHELL_GIT_VERSION={version}");
}

Expand All @@ -33,7 +37,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("PROTOC", protobuf_src::protoc());
}

let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let proto_root = manifest_dir.join(PROTO_REL);

let mut proto_files = Vec::new();
Expand Down Expand Up @@ -83,7 +86,7 @@ fn collect_proto_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()
/// 3 commits past v0.0.3 → "0.0.4-dev.3+g2bf9969"
///
/// Returns `None` when git is unavailable or the repo has no matching tags.
fn git_version() -> Option<String> {
fn git_version(manifest_dir: &Path) -> Option<String> {
// Match numeric release tags only (e.g. `v0.0.29`). The bare glob `v*`
// also matches non-release tags like `vm-dev` or `vm-prod`; when one of
// those lands on the same commit as a release tag, `git describe` picks
Expand All @@ -92,6 +95,7 @@ fn git_version() -> Option<String> {
// those development tags without losing any release tag.
let output = std::process::Command::new("git")
.args(["describe", "--tags", "--long", "--match", "v[0-9]*"])
.current_dir(manifest_dir)
.output()
.ok()?;

Expand Down
69 changes: 69 additions & 0 deletions crates/openshell-core/build_support/git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;

const WATCH_PATHS: [&str; 3] = ["HEAD", "refs/tags", "packed-refs"];

#[cfg(not(test))]
pub fn emit_rerun_if_changed(manifest_dir: &Path) {
for path in watch_paths(manifest_dir) {
println!("cargo:rerun-if-changed={}", path.display());
}
}

pub fn watch_paths(manifest_dir: &Path) -> Vec<PathBuf> {
let mut git_paths = WATCH_PATHS
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>();

if let Some(head_ref) = symbolic_head_ref(manifest_dir) {
git_paths.push(head_ref);
}

git_paths
.iter()
.filter_map(|path| resolve_existing_git_path(manifest_dir, path))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}

fn symbolic_head_ref(manifest_dir: &Path) -> Option<String> {
let output = Command::new("git")
.args(["symbolic-ref", "--quiet", "HEAD"])
.current_dir(manifest_dir)
.output()
.ok()?;

if !output.status.success() {
return None;
}

let head_ref = String::from_utf8(output.stdout).ok()?.trim().to_owned();
(!head_ref.is_empty()).then_some(head_ref)
}

fn resolve_existing_git_path(manifest_dir: &Path, path: &str) -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--git-path", path])
.current_dir(manifest_dir)
.output()
.ok()?;

if !output.status.success() {
return None;
}

let path = PathBuf::from(String::from_utf8(output.stdout).ok()?.trim());
let path = if path.is_absolute() {
path
} else {
manifest_dir.join(path)
};

path.exists().then_some(path)
}
117 changes: 117 additions & 0 deletions crates/openshell-core/tests/git_watch_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#[path = "../build_support/git.rs"]
mod git;

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn run_git(repo: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.expect("run git");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.expect("git output is UTF-8")
.trim()
.to_owned()
}

fn init_repository() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().expect("create temp directory");
let repo = temp.path().join("repo");
fs::create_dir(&repo).expect("create repository directory");

run_git(&repo, &["init", "--quiet"]);
run_git(&repo, &["config", "user.name", "OpenShell Test"]);
run_git(
&repo,
&["config", "user.email", "openshell@example.invalid"],
);
fs::write(repo.join("README.md"), "test\n").expect("write tracked file");
run_git(&repo, &["add", "README.md"]);
run_git(&repo, &["commit", "--quiet", "-m", "initial"]);
run_git(&repo, &["tag", "v0.0.1"]);

(temp, repo)
}

fn git_path(repo: &Path, path: &str) -> PathBuf {
let path = PathBuf::from(run_git(repo, &["rev-parse", "--git-path", path]));
if path.is_absolute() {
path
} else {
repo.join(path)
}
}

#[test]
fn resolves_metadata_paths_in_normal_checkout() {
let (_temp, repo) = init_repository();

let paths = git::watch_paths(&repo);
let head_ref = run_git(&repo, &["symbolic-ref", "HEAD"]);

assert!(paths.contains(&git_path(&repo, "HEAD")));
assert!(paths.contains(&git_path(&repo, &head_ref)));
assert!(paths.contains(&git_path(&repo, "refs/tags")));
assert!(paths.iter().all(|path| path.exists()));
}

#[test]
fn resolves_metadata_paths_in_linked_worktree() {
let (temp, repo) = init_repository();
let worktree = temp.path().join("linked");
let worktree_arg = worktree.to_str().expect("worktree path is UTF-8");
run_git(
&repo,
&["worktree", "add", "--quiet", "-b", "linked", worktree_arg],
);

let paths = git::watch_paths(&worktree);
let head = git_path(&worktree, "HEAD");
let head_ref = run_git(&worktree, &["symbolic-ref", "HEAD"]);

assert!(paths.contains(&head));
assert_ne!(head, worktree.join(".git/HEAD"));
assert!(paths.contains(&git_path(&worktree, &head_ref)));
assert!(paths.contains(&git_path(&worktree, "refs/tags")));
assert!(paths.iter().all(|path| path.exists()));
}

#[test]
fn watches_packed_refs() {
let (_temp, repo) = init_repository();
run_git(&repo, &["pack-refs", "--all"]);

let paths = git::watch_paths(&repo);

assert!(paths.contains(&git_path(&repo, "packed-refs")));
}

#[test]
fn detached_head_does_not_require_a_symbolic_ref() {
let (_temp, repo) = init_repository();
run_git(&repo, &["checkout", "--quiet", "--detach"]);

let paths = git::watch_paths(&repo);

assert!(paths.contains(&git_path(&repo, "HEAD")));
assert!(paths.iter().all(|path| path.exists()));
}

#[test]
fn ignores_source_tree_without_git_metadata() {
let temp = tempfile::tempdir().expect("create temp directory");

assert!(git::watch_paths(temp.path()).is_empty());
}
7 changes: 4 additions & 3 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ _.file = [".env"]
KUBECONFIG = "{{config_root}}/kubeconfig"
UV_CACHE_DIR = "{{config_root}}/.cache/uv"

# Enable sccache for faster Rust builds. Preserve an SCCACHE_DIR selected by the
# caller so workstations and CI can configure their cache locations separately.
# Enable sccache for faster Rust builds. Preserve caller overrides, use one
# user-level cache for local worktrees, and retain the repository-local path
# expected by CI cache steps.
RUSTC_WRAPPER = "sccache"
SCCACHE_DIR = "{{ get_env(name='SCCACHE_DIR', default=config_root ~ '/.cache/sccache') }}"
SCCACHE_DIR = "{{ get_env(name='SCCACHE_DIR', default=(config_root ~ '/.cache/sccache') if get_env(name='CI', default='') else (xdg_cache_home ~ '/openshell/sccache')) }}"

# Shared build constants (overridable via environment).
# DOCKER_BUILDKIT enables BuildKit for Docker; ignored by podman.
Expand Down
17 changes: 17 additions & 0 deletions tasks/rust.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@

# Rust check, lint, and format tasks

["rust:cache:status"]
description = "Show requested and active Rust compiler cache settings"
run = '''
echo "Requested cache directory: ${SCCACHE_DIR:-<unset>}"
echo "Requested base directories: ${SCCACHE_BASEDIRS:-<unset>}"
echo
sccache --show-stats
'''

["rust:cache:refresh"]
description = "Restart sccache with the current worktree cache settings"
run = '''
sccache --stop-server || true
sccache --start-server
sccache --show-stats
'''

["rust:check"]
description = "Check all Rust crates for errors"
run = "cargo check --workspace"
Expand Down
Loading