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
6 changes: 6 additions & 0 deletions ansible/roles/developer-rust/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
29 changes: 25 additions & 4 deletions ansible/roles/developer-rust/files/hyperi-rust-cache-prune
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}")
Expand Down
52 changes: 43 additions & 9 deletions ansible/roles/developer-rust/files/hyperi-rust-setup
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions ansible/roles/developer-rust/tasks/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<string>{{ rust_cache_build_dir_max }}</string>
<string>--max-age-days</string>
<string>{{ rust_cache_max_age_days }}</string>
{% if rust_cache_root %}
<string>--cache-root</string>
<string>{{ rust_cache_root }}</string>
{% endif %}
<string>--if-free-below</string>
<string>{{ rust_cache_prune_free_floor }}</string>
</array>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<string>{{ rust_cache_build_dir_max }}</string>
<string>--max-age-days</string>
<string>{{ rust_cache_max_age_days }}</string>
{% if rust_cache_root %}
<string>--cache-root</string>
<string>{{ rust_cache_root }}</string>
{% endif %}
</array>

<!-- launchd runs a missed calendar job once the machine wakes, which is
Expand Down
Loading