@@ -12,7 +12,7 @@ use std::{
1212use assert_cmd:: assert:: Assert ;
1313use fs2:: FileExt as _;
1414use serde:: Deserialize ;
15- use serde_json:: json;
15+ use serde_json:: { Value , json} ;
1616use walkdir:: WalkDir ;
1717
1818fn new_sorted_walkdir ( path : impl AsRef < Path > ) -> WalkDir {
@@ -294,6 +294,210 @@ fn get_toolchain_bin_dir() -> PathBuf {
294294 get_toolchain_install_dir ( ) . join ( "aeneas" ) . join ( "bin" )
295295}
296296
297+ fn run_archive_lake_cache_reuse_test ( test_name : & str ) -> datatest_stable:: Result < ( ) > {
298+ let _permit =
299+ profile_step ( test_name, None , "wait_toolchain_run_slot" , || acquire_toolchain_run_slot ( ) ) ;
300+ let temp = tempfile:: Builder :: new ( )
301+ . prefix ( "anneal-archive-cache-reuse-" )
302+ . tempdir_in ( get_target_dir ( ) ) ?;
303+ assert_archive_lake_cache_reuse ( test_name, & get_toolchain_install_dir ( ) , temp. path ( ) )
304+ }
305+
306+ fn assert_archive_lake_cache_reuse (
307+ test_name : & str ,
308+ toolchain_root : & Path ,
309+ temp_root : & Path ,
310+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
311+ let aeneas_root = toolchain_root. join ( "aeneas" ) ;
312+ let aeneas_lean = aeneas_root. join ( "backends/lean" ) ;
313+ let lean_root = toolchain_root. join ( "lean" ) ;
314+ let workspace = temp_root. join ( "generated-workspace" ) ;
315+
316+ assert_no_write_bits ( & aeneas_root) ?;
317+
318+ fs:: create_dir_all ( workspace. join ( "generated" ) ) ?;
319+ fs:: copy ( aeneas_lean. join ( "lean-toolchain" ) , workspace. join ( "lean-toolchain" ) ) ?;
320+ fs:: write ( workspace. join ( "generated/Generated.lean" ) , "import Aeneas\n " ) ?;
321+ fs:: write (
322+ workspace. join ( "lakefile.lean" ) ,
323+ format ! (
324+ r#"import Lake
325+ open Lake DSL
326+
327+ require aeneas from "{}"
328+
329+ package anneal_verification
330+
331+ @[default_target]
332+ lean_lib Generated where
333+ srcDir := "generated"
334+ roots := #[`Generated]
335+ "# ,
336+ lake_string( & aeneas_lean)
337+ ) ,
338+ ) ?;
339+ write_relative_archive_manifest ( & workspace, & aeneas_lean) ?;
340+
341+ // This mirrors v1's generated workspace contract with the Nix-built
342+ // archive: dependency paths are locked relative to the workspace, package
343+ // caches are read-only, and `--old` must reuse the prebuilt Lake outputs.
344+ run_lake_archive_command (
345+ test_name,
346+ & workspace,
347+ & lean_root,
348+ & [ "--keep-toolchain" , "--old" , "build" , "Generated" ] ,
349+ ) ?;
350+ run_lake_archive_command (
351+ test_name,
352+ & workspace,
353+ & lean_root,
354+ & [ "--keep-toolchain" , "env" , "lean" , "--json" , "generated/Generated.lean" ] ,
355+ ) ?;
356+
357+ Ok ( ( ) )
358+ }
359+
360+ fn assert_no_write_bits ( root : & Path ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
361+ for entry in new_sorted_walkdir ( root) {
362+ let entry = entry?;
363+ let metadata = fs:: symlink_metadata ( entry. path ( ) ) ?;
364+ if metadata. file_type ( ) . is_symlink ( ) {
365+ continue ;
366+ }
367+ if metadata. permissions ( ) . mode ( ) & 0o222 != 0 {
368+ panic ! ( "archive path should be read-only: {}" , entry. path( ) . display( ) ) ;
369+ }
370+ }
371+ Ok ( ( ) )
372+ }
373+
374+ fn write_relative_archive_manifest (
375+ workspace : & Path ,
376+ aeneas_lean : & Path ,
377+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
378+ let aeneas_lean = fs:: canonicalize ( aeneas_lean) ?;
379+ let workspace = fs:: canonicalize ( workspace) ?;
380+ let manifest_path = aeneas_lean. join ( "lake-manifest.json" ) ;
381+ let manifest: Value = serde_json:: from_reader ( fs:: File :: open ( & manifest_path) ?) ?;
382+ let aeneas_packages = manifest. get ( "packages" ) . and_then ( Value :: as_array) . ok_or_else ( || {
383+ invalid_data ( format ! (
384+ "Aeneas Lake manifest {} is missing packages" ,
385+ manifest_path. display( )
386+ ) )
387+ } ) ?;
388+
389+ let aeneas_dir = relative_manifest_string ( & aeneas_lean, & workspace) ?;
390+ let mut packages = vec ! [ json!( {
391+ "type" : "path" ,
392+ "name" : "aeneas" ,
393+ "dir" : aeneas_dir,
394+ "inherited" : false ,
395+ } ) ] ;
396+
397+ for entry in aeneas_packages {
398+ let mut entry = entry
399+ . as_object ( )
400+ . cloned ( )
401+ . ok_or_else ( || invalid_data ( "Aeneas Lake manifest package entry is not an object" ) ) ?;
402+ let package_type = entry
403+ . get ( "type" )
404+ . and_then ( Value :: as_str)
405+ . ok_or_else ( || invalid_data ( "Aeneas Lake manifest package entry is missing type" ) ) ?;
406+ if package_type != "path" {
407+ return Err ( invalid_data ( format ! (
408+ "Aeneas Lake manifest package entry is {package_type:?}, not a path dependency"
409+ ) )
410+ . into ( ) ) ;
411+ }
412+ let package_dir = entry
413+ . get ( "dir" )
414+ . and_then ( Value :: as_str)
415+ . ok_or_else ( || invalid_data ( "Aeneas Lake manifest package entry is missing dir" ) ) ?;
416+ let package_dir = Path :: new ( package_dir) ;
417+ let package_dir = if package_dir. is_absolute ( ) {
418+ package_dir. to_path_buf ( )
419+ } else {
420+ aeneas_lean. join ( package_dir)
421+ } ;
422+ let package_dir = fs:: canonicalize ( package_dir) ?;
423+ entry. insert ( "dir" . to_string ( ) , json ! ( relative_manifest_string( & package_dir, & workspace) ?) ) ;
424+ entry. insert ( "inherited" . to_string ( ) , json ! ( true ) ) ;
425+ packages. push ( Value :: Object ( entry) ) ;
426+ }
427+
428+ let manifest = json ! ( {
429+ "version" : "1.2.0" ,
430+ "packagesDir" : ".lake/packages" ,
431+ "packages" : packages,
432+ "name" : "anneal_verification" ,
433+ "lakeDir" : ".lake" ,
434+ "fixedToolchain" : false ,
435+ } ) ;
436+ fs:: write (
437+ workspace. join ( "lake-manifest.json" ) ,
438+ format ! ( "{}\n " , serde_json:: to_string_pretty( & manifest) ?) ,
439+ ) ?;
440+ Ok ( ( ) )
441+ }
442+
443+ fn relative_manifest_string (
444+ path : & Path ,
445+ base : & Path ,
446+ ) -> Result < String , Box < dyn std:: error:: Error > > {
447+ let path = pathdiff:: diff_paths ( path, base) . ok_or_else ( || {
448+ invalid_data ( format ! (
449+ "failed to compute relative path from {} to {}" ,
450+ base. display( ) ,
451+ path. display( )
452+ ) )
453+ } ) ?;
454+ Ok ( path. to_string_lossy ( ) . into_owned ( ) )
455+ }
456+
457+ fn lake_string ( path : & Path ) -> String {
458+ path. to_string_lossy ( ) . replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
459+ }
460+
461+ fn run_lake_archive_command (
462+ test_name : & str ,
463+ workspace : & Path ,
464+ lean_root : & Path ,
465+ args : & [ & str ] ,
466+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
467+ let lean_bin = lean_root. join ( "bin" ) ;
468+ let mut cmd = process:: Command :: new ( lean_bin. join ( "lake" ) ) ;
469+ cmd. args ( args)
470+ . current_dir ( workspace)
471+ . env_remove ( "CI" )
472+ . env ( "LEAN_SYSROOT" , lean_root)
473+ . env ( "MATHLIB_NO_CACHE_ON_UPDATE" , "1" )
474+ . env ( "PATH" , prepend_env_paths ( "PATH" , & [ lean_bin] ) ?) ;
475+
476+ let lib_var = if cfg ! ( target_os = "macos" ) { "DYLD_LIBRARY_PATH" } else { "LD_LIBRARY_PATH" } ;
477+ cmd. env (
478+ lib_var,
479+ prepend_env_paths ( lib_var, & [ lean_root. join ( "lib" ) , lean_root. join ( "lib/lean" ) ] ) ?,
480+ ) ;
481+
482+ run_command_with_profile ( test_name, Some ( "archive_lake_cache_reuse" ) , cmd) ?. assert . success ( ) ;
483+ Ok ( ( ) )
484+ }
485+
486+ fn prepend_env_paths (
487+ var_name : & str ,
488+ new_paths : & [ PathBuf ] ,
489+ ) -> Result < std:: ffi:: OsString , Box < dyn std:: error:: Error > > {
490+ let mut paths = new_paths. to_vec ( ) ;
491+ if let Some ( existing) = std:: env:: var_os ( var_name) {
492+ paths. extend ( std:: env:: split_paths ( & existing) ) ;
493+ }
494+ Ok ( std:: env:: join_paths ( paths) ?)
495+ }
496+
497+ fn invalid_data ( message : impl Into < String > ) -> io:: Error {
498+ io:: Error :: new ( io:: ErrorKind :: InvalidData , message. into ( ) )
499+ }
500+
297501fn toolchain_run_jobs ( ) -> usize {
298502 * TOOLCHAIN_RUN_JOBS . get_or_init ( || {
299503 std:: env:: var ( "ANNEAL_INTEGRATION_REAL_JOBS" )
@@ -949,6 +1153,11 @@ fn run_integration_test(path: &Path) -> datatest_stable::Result<()> {
9491153 run_dirty_sandbox_test ( path)
9501154 } ) ;
9511155 }
1156+ if path_str. contains ( "archive_lake_cache_reuse/anneal.toml" ) {
1157+ return profile_step ( & test_name, None , "archive_lake_cache_reuse_test" , || {
1158+ run_archive_lake_cache_reuse_test ( & test_name)
1159+ } ) ;
1160+ }
9521161 // Load the test configuration from the associated 'anneal.toml' manifest.
9531162 let config = anneal_toml. test . unwrap_or_default ( ) ;
9541163
0 commit comments