diff --git a/ansible/roles/developer-rust/defaults/main.yml b/ansible/roles/developer-rust/defaults/main.yml
index 2772c0f..0178593 100644
--- a/ansible/roles/developer-rust/defaults/main.yml
+++ b/ansible/roles/developer-rust/defaults/main.yml
@@ -23,6 +23,12 @@ rust_cache_manage: true
# leaves the per-project layout alone, so this stays safe to leave on.
rust_cache_central_build_dir: true
+# Where the pooled build cache lives. Empty means the platform cache directory
+# (~/.cache, or ~/Library/Caches on macOS). Set it to a path on a box with a
+# dedicated cache volume, e.g. /cache -- both hyperi-rust-setup and the prune
+# are given the same value, so the prune still refuses a pool outside it.
+rust_cache_root: ""
+
rust_cache_sccache_max: "20G"
rust_cache_ccache_max: "10G"
diff --git a/ansible/roles/developer-rust/files/hyperi-rust-cache-prune b/ansible/roles/developer-rust/files/hyperi-rust-cache-prune
index 89dc1ae..1469fb8 100644
--- a/ansible/roles/developer-rust/files/hyperi-rust-cache-prune
+++ b/ansible/roles/developer-rust/files/hyperi-rust-cache-prune
@@ -213,8 +213,19 @@ def human(size: int) -> str:
return f"{value:.1f}T"
-def cache_root() -> Path:
- """The platform's cache directory -- must match hyperi-rust-setup exactly."""
+def cache_root(override: Path | None = None) -> Path:
+ """Where caches live -- must match what hyperi-rust-setup was given.
+
+ A box with a dedicated cache volume puts the pool on that disk, outside home,
+ which would otherwise be refused. The override names the same directory
+ `hyperi-rust-setup --cache-root` was given.
+
+ It is a flag rather than an environment read because a systemd unit does not
+ load the user's profile, so an XDG_CACHE_HOME set there is absent when the
+ timer fires.
+ """
+ if override is not None:
+ return override
if sys.platform == "darwin":
return Path.home() / "Library" / "Caches"
xdg = os.environ.get("XDG_CACHE_HOME")
@@ -520,6 +531,16 @@ def main() -> int:
"free (e.g. 20%% or 80G) -- how the hourly guard invokes it"
),
)
+ parser.add_argument(
+ "--cache-root",
+ type=Path,
+ default=None,
+ metavar="DIR",
+ help=(
+ "the directory the pool is expected to live under, for a box whose cache "
+ "is on its own volume -- the same value hyperi-rust-setup was given"
+ ),
+ )
parser.add_argument(
"--pool",
type=Path,
@@ -549,10 +570,10 @@ def main() -> int:
# and keeps working.
if args.pool is None:
pool = pool.resolve()
- root = cache_root().resolve()
+ root = cache_root(args.cache_root).resolve()
if pool == root or not pool.is_relative_to(root):
print(f"refusing to prune {pool}: outside the cache root {root}.")
- print("Point build.build-dir inside the cache root, or pass --pool to override.")
+ print("Pass --cache-root for a cache on its own volume, or --pool to name it.")
return 1
print(f"hyperi-rust-cache-prune: {pool}")
diff --git a/ansible/roles/developer-rust/files/hyperi-rust-setup b/ansible/roles/developer-rust/files/hyperi-rust-setup
index e8af4cf..a7c2124 100644
--- a/ansible/roles/developer-rust/files/hyperi-rust-setup
+++ b/ansible/roles/developer-rust/files/hyperi-rust-setup
@@ -273,17 +273,25 @@ def cargo_version() -> tuple[int, int] | None:
return (int(match.group(1)), int(match.group(2))) if match else None
-def cache_root(family: str) -> Path:
- """The platform's cache directory -- the one place a user expects to be evictable."""
+def cache_root(family: str, override: Path | None = None) -> Path:
+ """Where caches live -- the platform default, or a directory named by the caller.
+
+ The override exists for a box with a dedicated cache volume, where the pool
+ belongs on that disk rather than on the one holding home. Whatever is chosen
+ has to be given to hyperi-rust-cache-prune as well, since it will not evict
+ a pool outside the root it was told about.
+ """
+ if override is not None:
+ return override
if family == "macos":
return Path.home() / "Library" / "Caches"
xdg = os.environ.get("XDG_CACHE_HOME")
return Path(xdg) if xdg else Path.home() / ".cache"
-def build_dir_path(family: str) -> Path:
+def build_dir_path(family: str, override: Path | None = None) -> Path:
"""Where intermediate build artefacts are pooled, and what the prune tool bounds."""
- return cache_root(family) / "hyperi-rust-build"
+ return cache_root(family, override) / "hyperi-rust-build"
def shell_env_file(family: str) -> Path:
@@ -512,7 +520,9 @@ def install_cargo_sweep(dry_run: bool, rep: Reporter) -> None:
# ---------------------------------------------------------------------------
-def resolve_build_dir(family: str, wanted: bool, rep: Reporter) -> Path | None:
+def resolve_build_dir(
+ family: str, wanted: bool, rep: Reporter, *, cache_root_override: Path | None = None
+) -> Path | None:
"""Decide whether the central build directory can be configured on this box."""
if not wanted:
return None
@@ -525,7 +535,7 @@ def resolve_build_dir(family: str, wanted: bool, rep: Reporter) -> Path | None:
have = ".".join(str(part) for part in version)
rep.info(f"cargo {have} predates build-dir (needs {want}) -- keeping per-project target/")
return None
- return build_dir_path(family)
+ return build_dir_path(family, cache_root_override)
def build_config(
@@ -597,14 +607,21 @@ def build_config(
def write_cargo_config(
- family: str, dry_run: bool, rep: Reporter, *, central_build_dir: bool
+ family: str,
+ dry_run: bool,
+ rep: Reporter,
+ *,
+ central_build_dir: bool,
+ cache_root_override: Path | None = None,
) -> None:
home = cargo_home()
config = home / "config.toml"
existing = config.read_text(encoding="utf-8") if config.is_file() else ""
carried = carry_over_sections(existing, rep) if existing else Carried("", "")
- build_dir = resolve_build_dir(family, central_build_dir, rep)
+ build_dir = resolve_build_dir(
+ family, central_build_dir, rep, cache_root_override=cache_root_override
+ )
content, managed = build_config(family, rep, build_dir=build_dir, carried=carried)
if managed == 0:
@@ -981,6 +998,17 @@ def main() -> int:
"so the same source built under a different root still hits"
),
)
+ parser.add_argument(
+ "--cache-root",
+ type=Path,
+ default=None,
+ metavar="DIR",
+ help=(
+ "put the pooled build cache under this directory instead of the platform "
+ "cache dir -- for a box with a dedicated cache volume. Give the same "
+ "value to hyperi-rust-cache-prune"
+ ),
+ )
parser.add_argument(
"--no-build-dir",
dest="central_build_dir",
@@ -1028,7 +1056,13 @@ def main() -> int:
install_cargo_sweep(args.check, rep)
rep.step("Global Cargo config")
- write_cargo_config(family, args.check, rep, central_build_dir=args.central_build_dir)
+ write_cargo_config(
+ family,
+ args.check,
+ rep,
+ central_build_dir=args.central_build_dir,
+ cache_root_override=args.cache_root,
+ )
rep.step("Cache caps")
write_env_caps(
diff --git a/ansible/roles/developer-rust/tasks/rust.yml b/ansible/roles/developer-rust/tasks/rust.yml
index 5d25c9b..e2534aa 100644
--- a/ansible/roles/developer-rust/tasks/rust.yml
+++ b/ansible/roles/developer-rust/tasks/rust.yml
@@ -515,6 +515,7 @@
--sccache-size {{ rust_cache_sccache_max }}
--ccache-size {{ rust_cache_ccache_max }}
--sccache-basedirs {{ rust_cache_sccache_basedirs | quote }}
+ {{ ('--cache-root ' ~ (rust_cache_root | quote)) if rust_cache_root else '' }}
{{ '' if rust_cache_central_build_dir else '--no-build-dir' }}
become: "{{ ansible_facts['distribution'] != 'MacOSX' }}"
become_user: "{{ actual_user if ansible_facts['distribution'] != 'MacOSX' else omit }}"
diff --git a/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune-guard.service.j2 b/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune-guard.service.j2
index 67a662e..d179598 100644
--- a/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune-guard.service.j2
+++ b/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune-guard.service.j2
@@ -6,7 +6,7 @@ Type=oneshot
User={{ actual_user }}
# Same prune with the same ceiling, gated on free space. Above the floor it
# exits after one statvfs without walking the pool.
-ExecStart=/usr/local/bin/hyperi-rust-cache-prune --yes --max-size {{ rust_cache_build_dir_max }} --max-age-days {{ rust_cache_max_age_days }} --if-free-below {{ rust_cache_prune_free_floor }}
+ExecStart=/usr/local/bin/hyperi-rust-cache-prune --yes --max-size {{ rust_cache_build_dir_max }} --max-age-days {{ rust_cache_max_age_days }}{{ ' --cache-root ' ~ rust_cache_root if rust_cache_root else '' }} --if-free-below {{ rust_cache_prune_free_floor }}
# Walking a large pool is IO-bound; it must never compete with an active build.
Nice=19
IOSchedulingClass=idle
diff --git a/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune.service.j2 b/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune.service.j2
index 46710e0..60e0c4b 100644
--- a/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune.service.j2
+++ b/ansible/roles/developer-rust/templates/hyperi-rust-cache-prune.service.j2
@@ -5,7 +5,7 @@ Description=Bound the pooled Rust build cache
Type=oneshot
User={{ actual_user }}
# Runs entirely inside the user's own cache directory, so it needs no sudo.
-ExecStart=/usr/local/bin/hyperi-rust-cache-prune --yes --max-size {{ rust_cache_build_dir_max }} --max-age-days {{ rust_cache_max_age_days }}
+ExecStart=/usr/local/bin/hyperi-rust-cache-prune --yes --max-size {{ rust_cache_build_dir_max }} --max-age-days {{ rust_cache_max_age_days }}{{ ' --cache-root ' ~ rust_cache_root if rust_cache_root else '' }}
# Walking a large pool is IO-bound; it must never compete with an active build.
Nice=19
IOSchedulingClass=idle
diff --git a/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune-guard.plist.j2 b/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune-guard.plist.j2
index e985b41..a02341e 100644
--- a/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune-guard.plist.j2
+++ b/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune-guard.plist.j2
@@ -13,6 +13,10 @@
{{ rust_cache_build_dir_max }}
--max-age-days
{{ rust_cache_max_age_days }}
+{% if rust_cache_root %}
+ --cache-root
+ {{ rust_cache_root }}
+{% endif %}
--if-free-below
{{ rust_cache_prune_free_floor }}
diff --git a/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune.plist.j2 b/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune.plist.j2
index 70f8f9b..4390e0f 100644
--- a/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune.plist.j2
+++ b/ansible/roles/developer-rust/templates/io.hyperi.rust-cache-prune.plist.j2
@@ -14,6 +14,10 @@
{{ rust_cache_build_dir_max }}
--max-age-days
{{ rust_cache_max_age_days }}
+{% if rust_cache_root %}
+ --cache-root
+ {{ rust_cache_root }}
+{% endif %}