Repository navigation
chore(build): share build outputs through local and remote Lake caches #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3430361
6797c5d
4a54915
b010959
6f4b9c6
598a448
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Build caches | ||
|
|
||
| Complexitylib's build has two caches. Both are optional: `lake build` without them compiles | ||
| everything it needs. | ||
|
|
||
| **Local artifact cache.** `lakefile.toml` enables Lake's content-addressed artifact cache, so a | ||
| module built in one checkout (a worktree, an agent sandbox, a second clone) is reused by every | ||
| other checkout on the same machine with the same inputs. Outputs are hard-linked back into | ||
| `.lake/build`. `lake cache clean` empties it; it lives in the Lean toolchain's directory under | ||
| `~/.elan` unless `LAKE_CACHE_DIR` says otherwise. | ||
|
|
||
| **Remote cache.** CI uploads the build outputs of every commit on `main`; `scripts/cache/get.sh` | ||
| downloads them for the nearest cached ancestor of your `HEAD`: | ||
|
|
||
| ```bash | ||
| scripts/cache/get.sh # Mathlib's cache, then Complexitylib's outputs | ||
| lake build # compiles only what you changed | ||
| ``` | ||
|
|
||
| A complete set of library and test outputs is about 70 MB compressed. Outputs are platform | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| independent (`platformIndependent = true`), so Linux CI builds serve macOS checkouts. | ||
| `COMPLEXITYLIB_CACHE_URL` overrides the location; set it to an empty string to skip the | ||
| Complexitylib download. | ||
|
|
||
| ## Trust and privacy | ||
|
|
||
| Downloaded `.olean` files are trusted by Lean: imported modules are not re-checked by the | ||
| kernel, and their compiled code (tactics, macros, initializers) runs during your build. Whoever | ||
| can write to the cache could therefore make a false theorem appear proved or run code on your | ||
| machine. The cache is set up so that this is no more trust than building `main` already | ||
| requires: | ||
|
|
||
| - Only the `main` push job of this repository can write. It authenticates with GitHub's OIDC | ||
| token through Workload Identity Federation, restricted to this repository's numeric id, the | ||
| `main` ref and the `push` event; no credentials are stored in GitHub. Pull requests, forks and | ||
| other workflows cannot obtain write access. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the attribute condition pins repository id,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - The writer can create objects but not delete them, so a published artifact can never be | ||
| replaced. Lake checks every downloaded artifact against the hash recorded in the mapping. | ||
| - Merge-gating CI always builds from source; the cache only saves local build time. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| The cache contains only build outputs of commits on `main` (sources that are public in this | ||
| repository); build logs and machine paths are not uploaded. Downloads are anonymous HTTPS | ||
| requests to Google Cloud Storage; the bucket keeps no access logs. | ||
|
|
||
| ## Maintainer runbook | ||
|
|
||
| The cache runs in a dedicated Google Cloud project maintained by Samuel Schlesinger. | ||
|
|
||
| **Setup.** `PROJECT=<id> BILLING_ACCOUNT=<id> ALERT_EMAIL=<address> scripts/cache/setup-gcs.sh` | ||
| creates, in order (each step can be rerun alone, e.g. `setup-gcs.sh alert`): | ||
|
|
||
| | Step | Creates | | ||
| | --- | --- | | ||
| | `project` | the project, billing link and APIs | | ||
| | `bucket` | `gs://complexitylib-lake-cache` (`us-east1`), public read by object name without listing, no soft delete or versioning, revision mappings expire after 180 days | | ||
| | `writer` | the GitHub identity pool and provider, and the `lake-cache-writer` service account (create + read on the bucket only) | | ||
| | `budget` | a monthly budget (default 10 USD) with alerts at 50/90/100 % and a Pub/Sub topic | | ||
| | `killswitch` | a Cloud Run function that removes public read when the budget is reached or the egress alert fires | | ||
| | `alert` | an alert on more than 20 GB sent per hour, to the kill switch and `ALERT_EMAIL` | | ||
| | `variables` | prints the `gh variable set` commands for the three repository variables | | ||
|
|
||
| CI skips the upload until the repository variable `LAKE_CACHE_BUCKET` is set. | ||
|
|
||
| **Costs.** Storage and egress are small at this size: each `main` push uploads only modules whose | ||
| outputs changed, and Cloud Storage's free tier covers 5 GB of storage and 100 GB of egress per | ||
| month in `us-east1`. The budget alerts and the kill switch bound abuse of the public bucket. | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| **After the kill switch fires.** Downloads fail and builds fall back to compiling. Check the egress | ||
| graph and the function's log, then restore public read: | ||
|
|
||
| ```bash | ||
| gcloud storage buckets add-iam-policy-binding gs://complexitylib-lake-cache \ | ||
| --member=allUsers --role=roles/storage.legacyObjectReader | ||
| ``` | ||
|
|
||
| **Pruning.** Artifacts are shared between revisions, so they are not expired by age. When storage | ||
| grows, delete the artifacts that no revision mapping still in the bucket refers to. | ||
|
|
||
| **Layout.** `a0/cslib-community/complexitylib/<hash>.art` holds module archives and | ||
| `r0/cslib-community/complexitylib/tc/<toolchain>/<commit>.jsonl` the input-to-output mappings of | ||
| each commit, as written by `scripts/cache/upload.sh` and read by `lake cache get`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env bash | ||
| # Download prebuilt build outputs, so that `lake build` only compiles what you changed. | ||
| # | ||
| # Usage: scripts/cache/get.sh (from the repository root or any worktree) | ||
| # | ||
| # Fetches Mathlib's cache, then Complexitylib's outputs for the nearest ancestor of HEAD that | ||
| # `main` CI has uploaded (Lake looks back up to 100 commits). Missing outputs are not an error: | ||
| # `lake build` compiles whatever the cache did not provide. See scripts/cache/README.md. | ||
| set -u | ||
| cd "$(dirname "$0")/../.." || exit 1 | ||
|
|
||
| url="${COMPLEXITYLIB_CACHE_URL-https://storage.googleapis.com/complexitylib-lake-cache}" | ||
|
|
||
| lake exe cache get || echo "warning: Mathlib cache download failed; lake build will compile Mathlib" >&2 | ||
| if [ -z "$url" ]; then | ||
| echo "COMPLEXITYLIB_CACHE_URL is empty; skipping the Complexitylib cache." >&2 | ||
| exit 0 | ||
| fi | ||
| if ! LAKE_CACHE_ARTIFACT_ENDPOINT="$url/a0" LAKE_CACHE_REVISION_ENDPOINT="$url/r0" \ | ||
| lake cache get --repo=cslib-community/complexitylib; then | ||
| echo "note: no cached Complexitylib outputs found for this history; lake build will compile them" >&2 | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """Stop public downloads from the remote cache when spending or egress crosses its limit. | ||
|
|
||
| Deployed as a Pub/Sub-triggered Cloud Run function by scripts/cache/setup-gcs.sh. It receives | ||
| two kinds of message on the same topic: | ||
|
|
||
| * Cloud Billing budget notifications (`costAmount`, `budgetAmount`): acts once the cost reaches | ||
| the budget; | ||
| * Cloud Monitoring alert notifications (`incident.state`): acts when the egress alert opens. | ||
|
|
||
| Acting means removing `allUsers` from the bucket's IAM policy. Objects are kept, so the | ||
| maintainer restores service by re-granting public read (see scripts/cache/README.md). | ||
| """ | ||
|
|
||
| import base64 | ||
| import json | ||
| import os | ||
|
|
||
| import functions_framework | ||
| from google.cloud import storage | ||
|
|
||
| BUCKET = os.environ["BUCKET"] | ||
|
|
||
|
|
||
| def should_stop(message: dict) -> bool: | ||
| if "costAmount" in message and "budgetAmount" in message: | ||
| return float(message["costAmount"]) >= float(message["budgetAmount"]) | ||
| return message.get("incident", {}).get("state") == "open" | ||
|
|
||
|
|
||
| @functions_framework.cloud_event | ||
| def handle(cloud_event) -> None: | ||
| message = json.loads(base64.b64decode(cloud_event.data["message"]["data"])) | ||
| if not should_stop(message): | ||
| return | ||
| bucket = storage.Client().bucket(BUCKET) | ||
| policy = bucket.get_iam_policy(requested_policy_version=3) | ||
| changed = False | ||
| for binding in policy.bindings: | ||
| if "allUsers" in binding["members"]: | ||
| binding["members"].discard("allUsers") | ||
| changed = True | ||
| policy.bindings = [b for b in policy.bindings if b["members"]] | ||
| if changed: | ||
| bucket.set_iam_policy(policy) | ||
| print(f"Removed public read from gs://{BUCKET}: {json.dumps(message)[:500]}") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| functions-framework==3.8.2 | ||
| google-cloud-storage==2.19.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
continue-on-error: trueto this step and to the upload step. Otherwise a failed upload failsmain's Lean CI run, and with it the formal-abstracts publication, which waits for that run's success. The README already calls both caches optional; this keeps CI consistent with that.