Skip to content

Commit 951c890

Browse files
committed
[anneal] Remove v1 Lake cache symlinks
gherrit-pr-id: Gf5g4ujwxwoivi2uce2w3zt2hemew6jfi
1 parent 83dbc57 commit 951c890

4 files changed

Lines changed: 256 additions & 173 deletions

File tree

anneal/src/aeneas.rs

Lines changed: 149 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ use std::{
1818
path::{Path, PathBuf},
1919
process::Stdio,
2020
sync::{Arc, Mutex},
21-
time::SystemTime,
2221
};
2322

24-
use anyhow::{Context, Result, bail};
23+
use anyhow::{Context, Result, bail, ensure};
2524
use indicatif::{ProgressBar, ProgressStyle};
26-
use walkdir::WalkDir;
25+
use serde_json::{Value, json};
2726

2827
use crate::{generate, resolve::LockedRoots, scanner::AnnealArtifact, setup::Tool};
2928

@@ -271,8 +270,10 @@ pub fn run_aeneas(
271270

272271
// 4. Write Lakefile
273272
//
274-
// Aeneas and its Lean dependencies are unpacked from the managed archive.
275-
let path = materialize_aeneas_dependency(&tmp_lean_root, &lean_root, &toolchain)?;
273+
// Aeneas and its Lean dependencies are used directly from the managed
274+
// archive. The generated manifest below keeps Lake on the locked dependency
275+
// loading path, so package config/build caches can stay read-only.
276+
let path = toolchain.aeneas_lean_dir();
276277
let aeneas_dep = format!(
277278
r#"-- Aeneas rev: {}
278279
require aeneas from "{}""#,
@@ -307,21 +308,19 @@ lean_lib «User» where
307308
);
308309
write_if_changed(&tmp_lean_root.join("lakefile.lean"), &lakefile)
309310
.context("Failed to write Lakefile")?;
311+
write_lake_manifest(&tmp_lean_root, &toolchain).context("Failed to write Lake manifest")?;
310312

311313
// ATOMIC SWAP: If we successfully generated everything, we now swap the
312314
// temporary directory with the real one.
313315
let lean_root = roots.lean_root();
314316
if lean_root.exists() {
315-
// Preserve the `.lake` directory and manifest to prevent re-downloading
316-
// dependencies (like Mathlib) on subsequent runs.
317+
// Preserve the `.lake` directory for generated-workspace build/config
318+
// caches. The Lake manifest is regenerated in the temporary directory
319+
// above because it records absolute paths into the installed toolchain.
317320
let old_lake = lean_root.join(".lake");
318-
let old_manifest = lean_root.join("lake-manifest.json");
319321
if old_lake.exists() {
320322
fs::rename(&old_lake, tmp_lean_root.join(".lake"))?;
321323
}
322-
if old_manifest.exists() {
323-
fs::rename(&old_manifest, tmp_lean_root.join("lake-manifest.json"))?;
324-
}
325324

326325
// Remove the existing directory before renaming the temporary directory.
327326
// Note: `fs::rename` on Unix typically requires the target directory to be
@@ -338,169 +337,83 @@ lean_lib «User» where
338337
Ok(())
339338
}
340339

341-
fn materialize_aeneas_dependency(
342-
tmp_lean_root: &Path,
343-
lean_root: &Path,
344-
toolchain: &crate::setup::Toolchain,
345-
) -> Result<PathBuf> {
346-
let vendor_root = tmp_lean_root.join("vendor").join("aeneas");
347-
let lean_dst = vendor_root.join("backends").join("lean");
348-
let packages_dst = vendor_root.join("packages");
349-
350-
println!("Materializing packages by copying from toolchain...");
351-
copy_toolchain_tree_writable(&toolchain.aeneas_lean_dir(), &lean_dst)
352-
.context("Failed to copy Aeneas Lean package from toolchain")?;
353-
copy_toolchain_tree_writable(&toolchain.aeneas_root().join("packages"), &packages_dst)
354-
.context("Failed to copy Aeneas Lean dependencies from toolchain")?;
355-
356-
Ok(lean_root.join("vendor").join("aeneas").join("backends").join("lean"))
340+
fn write_lake_manifest(lean_root: &Path, toolchain: &crate::setup::Toolchain) -> Result<()> {
341+
let manifest = generated_lake_manifest(toolchain)?;
342+
let mut contents =
343+
serde_json::to_string_pretty(&manifest).context("Failed to serialize Lake manifest")?;
344+
contents.push('\n');
345+
write_if_changed(&lean_root.join("lake-manifest.json"), &contents)
357346
}
358347

359-
fn copy_toolchain_tree_writable(src: &Path, dst: &Path) -> Result<()> {
360-
if dst.exists() {
361-
fs::remove_dir_all(dst)
362-
.with_context(|| format!("Failed to remove stale directory {}", dst.display()))?;
363-
}
364-
365-
let mut entries = WalkDir::new(src).into_iter();
366-
while let Some(entry) = entries.next() {
367-
let entry = entry.with_context(|| format!("Failed to walk {}", src.display()))?;
368-
let path = entry.path();
369-
let rel = path.strip_prefix(src).with_context(|| {
370-
format!("Failed to relativize {} against {}", path.display(), src.display())
348+
fn generated_lake_manifest(toolchain: &crate::setup::Toolchain) -> Result<Value> {
349+
let aeneas_lean_dir = fs::canonicalize(toolchain.aeneas_lean_dir()).with_context(|| {
350+
format!(
351+
"Failed to resolve Aeneas Lake package directory {}",
352+
toolchain.aeneas_lean_dir().display()
353+
)
354+
})?;
355+
let aeneas_manifest_path = aeneas_lean_dir.join("lake-manifest.json");
356+
let aeneas_manifest_file = fs::File::open(&aeneas_manifest_path)
357+
.with_context(|| format!("Failed to open {}", aeneas_manifest_path.display()))?;
358+
let aeneas_manifest: Value = serde_json::from_reader(aeneas_manifest_file)
359+
.with_context(|| format!("Failed to parse {}", aeneas_manifest_path.display()))?;
360+
let aeneas_packages =
361+
aeneas_manifest.get("packages").and_then(Value::as_array).with_context(|| {
362+
format!(
363+
"Aeneas Lake manifest {} is missing a packages array",
364+
aeneas_manifest_path.display()
365+
)
371366
})?;
372-
let target = dst.join(rel);
373-
374-
if is_lake_build_dir(rel) {
375-
// The Nix-built archive already contains Lake build caches for the
376-
// immutable Aeneas dependency tree. Link those caches back to the
377-
// read-only toolchain instead of copying several GiB of `.olean`
378-
// files into every generated Lean workspace. The surrounding
379-
// package/config/source files are still copied below so Lake can
380-
// update small mutable metadata such as `.lake/config`.
381-
if let Some(parent) = target.parent() {
382-
fs::create_dir_all(parent).with_context(|| {
383-
format!("Failed to create parent directory {}", parent.display())
384-
})?;
385-
}
386-
create_symlink(path, &target).with_context(|| {
387-
format!(
388-
"Failed to link prebuilt Lake cache {} to {}",
389-
path.display(),
390-
target.display()
391-
)
392-
})?;
393-
entries.skip_current_dir();
394-
} else if entry.file_type().is_dir() {
395-
fs::create_dir_all(&target)
396-
.with_context(|| format!("Failed to create directory {}", target.display()))?;
397-
} else if entry.file_type().is_symlink() {
398-
let link = fs::read_link(path)
399-
.with_context(|| format!("Failed to read symlink {}", path.display()))?;
400-
if let Some(parent) = target.parent() {
401-
fs::create_dir_all(parent).with_context(|| {
402-
format!("Failed to create parent directory {}", parent.display())
403-
})?;
404-
}
405-
create_symlink(&link, &target)
406-
.with_context(|| format!("Failed to copy symlink {}", target.display()))?;
407-
} else if entry.file_type().is_file() {
408-
if let Some(parent) = target.parent() {
409-
fs::create_dir_all(parent).with_context(|| {
410-
format!("Failed to create parent directory {}", parent.display())
411-
})?;
412-
}
413-
fs::copy(path, &target).with_context(|| {
414-
format!("Failed to copy {} to {}", path.display(), target.display())
415-
})?;
416-
make_writable(&target)?;
417-
}
418-
}
419-
420-
for entry in WalkDir::new(dst).contents_first(true) {
421-
let entry = entry.with_context(|| format!("Failed to walk {}", dst.display()))?;
422-
if !entry.file_type().is_symlink() {
423-
make_writable(entry.path())?;
424-
}
425-
}
426-
427-
normalize_lake_input_mtimes(dst)?;
428367

429-
Ok(())
430-
}
431-
432-
fn is_lake_build_dir(rel: &Path) -> bool {
433-
rel.file_name().is_some_and(|name| name == "build")
434-
&& rel.parent().and_then(Path::file_name).is_some_and(|name| name == ".lake")
435-
}
436-
437-
#[cfg(unix)]
438-
fn create_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
439-
std::os::unix::fs::symlink(src, dst)
440-
}
441-
442-
#[cfg(windows)]
443-
fn create_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
444-
if src.is_dir() {
445-
std::os::windows::fs::symlink_dir(src, dst)
446-
} else {
447-
std::os::windows::fs::symlink_file(src, dst)
448-
}
449-
}
450-
451-
fn make_writable(path: &Path) -> Result<()> {
452-
let metadata =
453-
fs::symlink_metadata(path).with_context(|| format!("Failed to stat {}", path.display()))?;
454-
if metadata.file_type().is_symlink() {
455-
return Ok(());
456-
}
457-
let mut perms = metadata.permissions();
458-
if perms.readonly() {
459-
#[allow(clippy::permissions_set_readonly_false)]
460-
perms.set_readonly(false);
461-
fs::set_permissions(path, perms)
462-
.with_context(|| format!("Failed to make {} writable", path.display()))?;
463-
}
464-
Ok(())
465-
}
466-
467-
fn normalize_lake_input_mtimes(root: &Path) -> Result<()> {
468-
// The installed toolchain archive is intentionally read-only, and
469-
// `copy_toolchain_tree_writable` symlinks `.lake/build` directories back to
470-
// that archive so each verification workspace does not copy several GiB of
471-
// prebuilt `.olean` files. That optimization only works if Lake accepts the
472-
// linked cache artifacts as newer than the copied source/config files. A
473-
// normal file copy gives the copied inputs fresh mtimes, so Lake `--old`
474-
// can decide that archive outputs are stale and then fail while trying to
475-
// remove read-only files through the `.lake/build` symlinks. Mirror the
476-
// archive builder's convention: copied Lake inputs are older than the
477-
// archive cache artifacts, while generated project files keep their normal
478-
// mtimes and remain mutable.
479-
for entry in WalkDir::new(root) {
480-
let entry = entry.with_context(|| format!("Failed to walk {}", root.display()))?;
481-
if !entry.file_type().is_file() || !is_lake_input_file(entry.path()) {
482-
continue;
483-
}
484-
485-
let file = fs::File::options().write(true).open(entry.path()).with_context(|| {
486-
format!("Failed to open {} to normalize mtime", entry.path().display())
368+
let mut packages = Vec::with_capacity(aeneas_packages.len() + 1);
369+
packages.push(json!({
370+
"type": "path",
371+
"name": "aeneas",
372+
"dir": path_to_manifest_string(&aeneas_lean_dir),
373+
"inherited": false,
374+
}));
375+
376+
for entry in aeneas_packages {
377+
let mut entry = entry
378+
.as_object()
379+
.cloned()
380+
.context("Aeneas Lake manifest package entry is not an object")?;
381+
let package_name = entry.get("name").and_then(Value::as_str).unwrap_or("<unknown>");
382+
let package_type = entry.get("type").and_then(Value::as_str).with_context(|| {
383+
format!("Aeneas Lake manifest package entry {package_name} is missing type")
487384
})?;
488-
file.set_times(fs::FileTimes::new().set_modified(SystemTime::UNIX_EPOCH))
489-
.with_context(|| format!("Failed to normalize mtime for {}", entry.path().display()))?;
385+
ensure!(
386+
package_type == "path",
387+
"Aeneas Lake manifest package entry {package_name} is {package_type:?}, not a path dependency"
388+
);
389+
let package_dir = entry.get("dir").and_then(Value::as_str).with_context(|| {
390+
format!("Aeneas Lake manifest package entry {package_name} is missing dir")
391+
})?;
392+
let package_dir = Path::new(package_dir);
393+
let package_dir = if package_dir.is_absolute() {
394+
package_dir.to_path_buf()
395+
} else {
396+
aeneas_lean_dir.join(package_dir)
397+
};
398+
let package_dir = fs::canonicalize(&package_dir)
399+
.with_context(|| format!("Failed to resolve Lake package {}", package_dir.display()))?;
400+
entry.insert("dir".to_string(), json!(path_to_manifest_string(&package_dir)));
401+
entry.insert("inherited".to_string(), json!(true));
402+
packages.push(Value::Object(entry));
490403
}
491404

492-
Ok(())
405+
Ok(json!({
406+
"version": "1.2.0",
407+
"packagesDir": ".lake/packages",
408+
"packages": packages,
409+
"name": "anneal_verification",
410+
"lakeDir": ".lake",
411+
"fixedToolchain": false,
412+
}))
493413
}
494414

495-
fn is_lake_input_file(path: &Path) -> bool {
496-
if path.extension().and_then(|ext| ext.to_str()) == Some("lean") {
497-
return true;
498-
}
499-
500-
matches!(
501-
path.file_name().and_then(|name| name.to_str()),
502-
Some("lakefile.toml" | "lake-manifest.json" | "lean-toolchain")
503-
)
415+
fn path_to_manifest_string(path: &Path) -> String {
416+
path.to_string_lossy().into_owned()
504417
}
505418

506419
/// Generates Anneal `Specs.lean` and writes `Generated.lean`, but does not run the `lake build`.
@@ -760,11 +673,10 @@ fn configure_lake_command(
760673
// FIXME: Replace this with a cleaner toolchain/archive contract.
761674
//
762675
// The Nix-built archive contains prebuilt Lake outputs for the vendored
763-
// Aeneas package, and `materialize_aeneas_dependency` links each generated
764-
// verification workspace's `vendor/aeneas/backends/lean/.lake/build` back
765-
// to that read-only archive cache. That is only sound if Lake evaluates the
766-
// vendored Aeneas package with the same build configuration that was used
767-
// when the archive was produced.
676+
// Aeneas package, and generated verification workspaces require that
677+
// package directly from the installed archive. That is only sound if Lake
678+
// evaluates Aeneas with the same build configuration that was used when the
679+
// archive was produced.
768680
//
769681
// Aeneas' Lakefile currently makes one of those build settings depend on
770682
// the ambient `CI` environment variable:
@@ -775,9 +687,8 @@ fn configure_lake_command(
775687
// sets `CI=true` for normal workflow steps. If we let that variable reach
776688
// this child process, Lake observes a different Aeneas package config than
777689
// the one recorded in the archive traces. It then invalidates the prebuilt
778-
// cache and attempts to rebuild/remove files below the symlinked
779-
// `.lake/build`, which fails because those archive files are intentionally
780-
// read-only.
690+
// cache and attempts to rebuild/remove files below the installed archive's
691+
// read-only `.lake/build`.
781692
//
782693
// Scrubbing `CI` here keeps local runs, example CI jobs, and the integration
783694
// test harness aligned with the archive build environment. A cleaner future
@@ -1031,6 +942,71 @@ mod tests {
1031942
}
1032943
}
1033944

945+
#[test]
946+
fn generated_lake_manifest_locks_archive_path_dependencies() {
947+
let temp = tempfile::tempdir().unwrap();
948+
let toolchain_root = temp.path().join("toolchain");
949+
let aeneas_lean = toolchain_root.join("aeneas/backends/lean");
950+
let mathlib = toolchain_root.join("aeneas/packages/mathlib");
951+
let qq = toolchain_root.join("aeneas/packages/Qq");
952+
std::fs::create_dir_all(&aeneas_lean).unwrap();
953+
std::fs::create_dir_all(&mathlib).unwrap();
954+
std::fs::create_dir_all(&qq).unwrap();
955+
std::fs::write(
956+
aeneas_lean.join("lake-manifest.json"),
957+
serde_json::to_string(&json!({
958+
"version": "1.2.0",
959+
"packagesDir": ".lake/packages",
960+
"packages": [
961+
{
962+
"type": "path",
963+
"name": "mathlib",
964+
"dir": "../../packages/mathlib",
965+
"inherited": false,
966+
},
967+
{
968+
"type": "path",
969+
"name": "Qq",
970+
"dir": "../../packages/Qq",
971+
"inherited": true,
972+
"scope": "",
973+
},
974+
],
975+
"name": "aeneas",
976+
"lakeDir": ".lake",
977+
"fixedToolchain": false,
978+
}))
979+
.unwrap(),
980+
)
981+
.unwrap();
982+
983+
let toolchain = crate::setup::Toolchain { root: toolchain_root };
984+
let manifest = generated_lake_manifest(&toolchain).unwrap();
985+
let packages = manifest.get("packages").unwrap().as_array().unwrap();
986+
987+
assert_eq!(packages.len(), 3);
988+
assert_eq!(packages[0]["name"], "aeneas");
989+
assert_eq!(
990+
packages[0]["dir"],
991+
path_to_manifest_string(&std::fs::canonicalize(&aeneas_lean).unwrap())
992+
);
993+
assert_eq!(packages[0]["inherited"], false);
994+
995+
assert_eq!(packages[1]["name"], "mathlib");
996+
assert_eq!(
997+
packages[1]["dir"],
998+
path_to_manifest_string(&std::fs::canonicalize(&mathlib).unwrap())
999+
);
1000+
assert_eq!(packages[1]["inherited"], true);
1001+
1002+
assert_eq!(packages[2]["name"], "Qq");
1003+
assert_eq!(
1004+
packages[2]["dir"],
1005+
path_to_manifest_string(&std::fs::canonicalize(&qq).unwrap())
1006+
);
1007+
assert_eq!(packages[2]["inherited"], true);
1008+
}
1009+
10341010
fn mk_mapping(
10351011
lean_start: usize,
10361012
lean_end: usize,

0 commit comments

Comments
 (0)