From 6d337e0efaf178d6263ec800d3d74e39c84bf3c5 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 15 Sep 2025 11:19:24 +0200 Subject: [PATCH 1/4] Keep track of compiler info during build --- rewatch/src/build.rs | 33 ++++++- rewatch/src/build/build_types.rs | 14 ++- rewatch/src/build/clean.rs | 11 ++- rewatch/src/build/compile.rs | 45 ++++++--- rewatch/src/build/compiler_info.rs | 144 +++++++++++++++++++++++++++++ rewatch/src/build/packages.rs | 4 + rewatch/src/build/parse.rs | 4 +- 7 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 rewatch/src/build/compiler_info.rs diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 1a93b2bf364..43fcdcdf23e 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -1,6 +1,7 @@ pub mod build_types; pub mod clean; pub mod compile; +pub mod compiler_info; pub mod deps; pub mod logs; pub mod namespaces; @@ -10,6 +11,7 @@ pub mod read_compile_state; use self::parse::parser_args; use crate::build::compile::{mark_modules_with_deleted_deps_dirty, mark_modules_with_expired_deps_dirty}; +use crate::build::compiler_info::{CompilerCheckResult, verify_compiler_info, write_compiler_info}; use crate::helpers::emojis::*; use crate::helpers::{self}; use crate::project_context::ProjectContext; @@ -109,6 +111,20 @@ pub fn get_compiler_args(rescript_file_path: &Path) -> Result { Ok(result) } +pub fn get_compiler_info(project_context: &ProjectContext) -> Result { + let bsc_path = helpers::get_bsc(); + let bsc_hash = helpers::compute_file_hash(&bsc_path).ok_or(anyhow!( + "Failed to compute bsc hash for {}", + bsc_path.to_string_lossy() + ))?; + let runtime_path = compile::get_runtime_path(&project_context.current_config, project_context)?; + Ok(CompilerInfo { + bsc_path, + bsc_hash, + runtime_path, + }) +} + pub fn initialize_build( default_timing: Option, filter: &Option, @@ -117,8 +133,8 @@ pub fn initialize_build( build_dev_deps: bool, snapshot_output: bool, ) -> Result { - let bsc_path = helpers::get_bsc(); let project_context = ProjectContext::new(path)?; + let compiler = get_compiler_info(&project_context)?; if !snapshot_output && show_progress { print!("{} {}Building package tree...", style("[1/7]").bold().dim(), TREE); @@ -129,6 +145,8 @@ pub fn initialize_build( let packages = packages::make(filter, &project_context, show_progress, build_dev_deps)?; let timing_package_tree_elapsed = timing_package_tree.elapsed(); + let compiler_check = verify_compiler_info(&packages, &compiler); + if !snapshot_output && show_progress { println!( "{}{} {}Built package tree in {:.2}s", @@ -139,6 +157,14 @@ pub fn initialize_build( .unwrap_or(timing_package_tree_elapsed) .as_secs_f64() ); + if let CompilerCheckResult::CleanedPackagesDueToCompiler = compiler_check { + println!( + "{}{} {}Cleaned previous build due to compiler update", + LINE_CLEAR, + style("[1/7]").bold().dim(), + SWEEP + ); + } } if !packages::validate_packages_dependencies(&packages) { @@ -156,7 +182,7 @@ pub fn initialize_build( let _ = stdout().flush(); } - let mut build_state = BuildState::new(project_context, packages, bsc_path); + let mut build_state = BuildState::new(project_context, packages, compiler); packages::parse_packages(&mut build_state); let timing_source_files_elapsed = timing_source_files.elapsed(); @@ -448,6 +474,9 @@ pub fn incremental_build( log_deprecations(build_state); } + // Write per-package compiler metadata to `lib/bs/compiler-info.json` (idempotent) + write_compiler_info(build_state); + Ok(()) } } diff --git a/rewatch/src/build/build_types.rs b/rewatch/src/build/build_types.rs index 76035ab27fa..64a8895da41 100644 --- a/rewatch/src/build/build_types.rs +++ b/rewatch/src/build/build_types.rs @@ -2,6 +2,7 @@ use crate::build::packages::{Namespace, Package}; use crate::config::Config; use crate::project_context::ProjectContext; use ahash::{AHashMap, AHashSet}; +use blake3::Hash; use std::{fmt::Display, path::PathBuf, time::SystemTime}; #[derive(Debug, Clone, PartialEq)] @@ -96,10 +97,17 @@ pub struct BuildState { pub packages: AHashMap, pub module_names: AHashSet, pub deleted_modules: AHashSet, - pub bsc_path: PathBuf, + pub compiler_info: CompilerInfo, pub deps_initialized: bool, } +#[derive(Debug, Clone)] +pub struct CompilerInfo { + pub bsc_path: PathBuf, + pub bsc_hash: Hash, + pub runtime_path: PathBuf, +} + impl BuildState { pub fn get_package(&self, package_name: &str) -> Option<&Package> { self.packages.get(package_name) @@ -111,7 +119,7 @@ impl BuildState { pub fn new( project_context: ProjectContext, packages: AHashMap, - bsc_path: PathBuf, + compiler: CompilerInfo, ) -> Self { Self { project_context, @@ -119,7 +127,7 @@ impl BuildState { modules: AHashMap::new(), packages, deleted_modules: AHashSet::new(), - bsc_path, + compiler_info: compiler, deps_initialized: false, } } diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index b5f4e79a559..34a698fcb4d 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -1,5 +1,6 @@ use super::build_types::*; use super::packages; +use crate::build; use crate::build::packages::Package; use crate::config::Config; use crate::helpers; @@ -332,9 +333,8 @@ pub fn cleanup_after_build(build_state: &BuildState) { pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, clean_dev_deps: bool) -> Result<()> { let project_context = ProjectContext::new(path)?; - + let compiler_info = build::get_compiler_info(&project_context)?; let packages = packages::make(&None, &project_context, show_progress, clean_dev_deps)?; - let bsc_path = helpers::get_bsc(); let timing_clean_compiler_assets = Instant::now(); if !snapshot_output && show_progress { @@ -364,7 +364,7 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, clean_dev_ } let timing_clean_mjs = Instant::now(); - let mut build_state = BuildState::new(project_context, packages, bsc_path); + let mut build_state = BuildState::new(project_context, packages, compiler_info); packages::parse_packages(&mut build_state); let root_config = build_state.get_root_config(); let suffix_for_print = if snapshot_output || !show_progress { @@ -418,7 +418,7 @@ pub fn clean(path: &Path, show_progress: bool, snapshot_output: bool, clean_dev_ Ok(()) } -fn clean_package(show_progress: bool, snapshot_output: bool, package: &Package) { +pub fn clean_package(show_progress: bool, snapshot_output: bool, package: &Package) { if show_progress { if snapshot_output { println!("Cleaning {}", package.name) @@ -441,4 +441,7 @@ fn clean_package(show_progress: bool, snapshot_output: bool, package: &Package) let path_str = package.get_ocaml_build_path(); let path = std::path::Path::new(&path_str); let _ = std::fs::remove_dir_all(path); + + // remove the per-package compiler metadata file so that a subsequent build writes fresh metadata + let _ = std::fs::remove_file(package.get_compiler_info_path()); } diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 67074c9ddd1..f47c08cd9db 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -16,7 +16,9 @@ use console::style; use log::{debug, trace}; use rayon::prelude::*; use std::path::Path; +use std::path::PathBuf; use std::process::Command; +use std::sync::OnceLock; use std::time::SystemTime; pub fn compile( @@ -336,22 +338,36 @@ pub fn compile( Ok((compile_errors, compile_warnings, num_compiled_modules)) } -pub fn get_runtime_path_args( - package_config: &Config, - project_context: &ProjectContext, -) -> Result> { - match std::env::var("RESCRIPT_RUNTIME") { - Ok(runtime_path) => Ok(vec!["-runtime-path".to_string(), runtime_path]), +static RUNTIME_PATH_MEMO: OnceLock = OnceLock::new(); + +pub fn get_runtime_path(package_config: &Config, project_context: &ProjectContext) -> Result { + if let Some(p) = RUNTIME_PATH_MEMO.get() { + return Ok(p.clone()); + } + + let resolved = match std::env::var("RESCRIPT_RUNTIME") { + Ok(runtime_path) => Ok(PathBuf::from(runtime_path)), Err(_) => match helpers::try_package_path(package_config, project_context, "@rescript/runtime") { - Ok(runtime_path) => Ok(vec![ - "-runtime-path".to_string(), - runtime_path.to_string_lossy().to_string(), - ]), + Ok(runtime_path) => Ok(runtime_path), Err(err) => Err(anyhow!( "The rescript runtime package could not be found.\nPlease set RESCRIPT_RUNTIME environment variable or make sure the runtime package is installed.\nError: {err}" )), }, - } + }?; + + let _ = RUNTIME_PATH_MEMO.set(resolved.clone()); + Ok(resolved) +} + +pub fn get_runtime_path_args( + package_config: &Config, + project_context: &ProjectContext, +) -> Result> { + let runtime_path = get_runtime_path(package_config, project_context)?; + Ok(vec![ + "-runtime-path".to_string(), + runtime_path.to_string_lossy().to_string(), + ]) } pub fn compiler_args( @@ -581,7 +597,7 @@ fn compile_file( let BuildState { packages, project_context, - bsc_path, + compiler_info, .. } = build_state; let root_config = build_state.get_root_config(); @@ -612,7 +628,7 @@ fn compile_file( package.is_local_dep, )?; - let to_mjs = Command::new(bsc_path) + let to_mjs = Command::new(&compiler_info.bsc_path) .current_dir( build_path_abs .canonicalize() @@ -748,6 +764,9 @@ fn compile_file( } }); + // TODO: Optionally record per-module successful compile timestamps if needed + // for future metadata-based invalidation. Current metadata is per-package. + if helpers::contains_ascii_characters(&err) { if package.is_local_dep { // suppress warnings of external deps diff --git a/rewatch/src/build/compiler_info.rs b/rewatch/src/build/compiler_info.rs new file mode 100644 index 00000000000..88cbdada1f4 --- /dev/null +++ b/rewatch/src/build/compiler_info.rs @@ -0,0 +1,144 @@ +use super::build_types::{BuildState, CompilerInfo}; +use super::clean; +use super::packages; +use ahash::AHashMap; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Write; + +// In order to have a loose coupling with the compiler, we don't want to have a hard dependency on the compiler's structs +// We can use this struct to parse the compiler-info.json file +// If something is not there, that is fine, we will treat it as a mismatch +#[derive(Serialize, Deserialize)] +struct CompilerInfoFile { + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + bsc_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + bsc_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + runtime_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + generated_at: Option, +} + +pub enum CompilerCheckResult { + SameCompilerAsLastRun, + CleanedPackagesDueToCompiler, +} + +pub fn verify_compiler_info( + packages: &AHashMap, + compiler: &CompilerInfo, +) -> CompilerCheckResult { + let mismatched_packages = packages + .values() + .filter(|package| { + let info_path = package.get_compiler_info_path(); + let Ok(contents) = std::fs::read_to_string(&info_path) else { + // Can't read file → treat as mismatch so we clean and rewrite + return true; + }; + + let parsed: Result = serde_json::from_str(&contents); + let parsed = match parsed { + Ok(p) => p, + Err(_) => return true, // unknown or invalid format -> treat as mismatch + }; + + let current_bsc_path_str = compiler.bsc_path.to_string_lossy(); + let current_bsc_hash_hex = compiler.bsc_hash.to_hex().to_string(); + let current_runtime_path_str = compiler.runtime_path.to_string_lossy(); + + let mut mismatch = false; + if parsed.bsc_path.as_deref() != Some(¤t_bsc_path_str) { + log::debug!( + "compiler-info mismatch for {}: bsc_path changed (stored='{}', current='{}')", + package.name, + parsed.bsc_path.as_deref().unwrap_or(""), + current_bsc_path_str + ); + mismatch = true; + } + if parsed.bsc_hash.as_deref() != Some(¤t_bsc_hash_hex) { + log::debug!( + "compiler-info mismatch for {}: bsc_hash changed (stored='{}', current='{}')", + package.name, + parsed.bsc_hash.as_deref().unwrap_or(""), + current_bsc_hash_hex + ); + mismatch = true; + } + if parsed.runtime_path.as_deref() != Some(¤t_runtime_path_str) { + log::debug!( + "compiler-info mismatch for {}: runtime_path changed (stored='{}', current='{}')", + package.name, + parsed.runtime_path.as_deref().unwrap_or(""), + current_runtime_path_str + ); + mismatch = true; + } + + mismatch + }) + .collect::>(); + + let cleaned_count = mismatched_packages.len(); + mismatched_packages.par_iter().for_each(|package| { + // suppress progress printing during init to avoid breaking step output + clean::clean_package(false, true, package); + }); + if cleaned_count == 0 { + CompilerCheckResult::SameCompilerAsLastRun + } else { + CompilerCheckResult::CleanedPackagesDueToCompiler + } +} + +pub fn write_compiler_info(build_state: &BuildState) { + let bsc_path_str = build_state.compiler_info.bsc_path.to_string_lossy().to_string(); + let bsc_hash_hex = build_state.compiler_info.bsc_hash.to_hex().to_string(); + let runtime_path_str = build_state + .compiler_info + .runtime_path + .to_string_lossy() + .to_string(); + + // derive version from the crate version + let version = env!("CARGO_PKG_VERSION").to_string(); + let generated_at = crate::helpers::get_system_time().to_string(); + + let out = CompilerInfoFile { + version: Some(version), + bsc_path: Some(bsc_path_str), + bsc_hash: Some(bsc_hash_hex), + runtime_path: Some(runtime_path_str), + generated_at: Some(generated_at), + }; + let contents = serde_json::to_string_pretty(&out).unwrap_or_else(|_| String::new()); + + build_state.packages.values().par_bridge().for_each(|package| { + let info_path = package.get_compiler_info_path(); + let should_write = match std::fs::read_to_string(&info_path) { + Ok(existing) => existing != contents, + Err(_) => true, + }; + + if should_write { + if let Some(parent) = info_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + // We write atomically to avoid leaving a partially written JSON file + // (e.g. process interruption) that would be read on the next init as an + // invalid/mismatched compiler-info, causing unnecessary cleans. The + // rename within the same directory is atomic on common platforms. + let tmp = info_path.with_extension("json.tmp"); + if let Ok(mut f) = File::create(&tmp) { + let _ = f.write_all(contents.as_bytes()); + let _ = std::fs::rename(&tmp, &info_path); + } + } + }); +} diff --git a/rewatch/src/build/packages.rs b/rewatch/src/build/packages.rs index 8aabaa8936c..cc0436650dd 100644 --- a/rewatch/src/build/packages.rs +++ b/rewatch/src/build/packages.rs @@ -91,6 +91,10 @@ impl Package { get_build_path(&self.path) } + pub fn get_compiler_info_path(&self) -> PathBuf { + self.get_build_path().join("compiler-info.json") + } + pub fn get_js_path(&self) -> PathBuf { get_js_path(&self.path) } diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index f1a8583ffe2..ff6dad09580 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -196,7 +196,7 @@ pub fn generate_asts( &build_state.project_context, package, module_name, - &build_state.bsc_path, + &build_state.compiler_info.bsc_path, ) { has_failure = true; stderr.push_str(&format!("{}\n", err)); @@ -310,7 +310,7 @@ fn generate_ast( /* Create .ast */ let result = match Some( - Command::new(&build_state.bsc_path) + Command::new(&build_state.compiler_info.bsc_path) .current_dir(&build_path_abs) .args(parser_args) .output() From bbaf9e147e42655de1c12fc97d1da84a44abcd29 Mon Sep 17 00:00:00 2001 From: nojaf Date: Mon, 15 Sep 2025 13:36:56 +0200 Subject: [PATCH 2/4] Take rescript.json hash into account --- rewatch/src/build/compiler_info.rs | 117 ++++++++++++++++++----------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/rewatch/src/build/compiler_info.rs b/rewatch/src/build/compiler_info.rs index 88cbdada1f4..43320405485 100644 --- a/rewatch/src/build/compiler_info.rs +++ b/rewatch/src/build/compiler_info.rs @@ -1,3 +1,5 @@ +use crate::helpers; + use super::build_types::{BuildState, CompilerInfo}; use super::clean; use super::packages; @@ -12,16 +14,12 @@ use std::io::Write; // If something is not there, that is fine, we will treat it as a mismatch #[derive(Serialize, Deserialize)] struct CompilerInfoFile { - #[serde(skip_serializing_if = "Option::is_none")] - version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - bsc_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - bsc_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - runtime_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - generated_at: Option, + version: String, + bsc_path: String, + bsc_hash: String, + rescript_config_hash: String, + runtime_path: String, + generated_at: String, } pub enum CompilerCheckResult { @@ -29,6 +27,10 @@ pub enum CompilerCheckResult { CleanedPackagesDueToCompiler, } +fn get_rescript_config_hash(package: &packages::Package) -> Option { + helpers::compute_file_hash(&package.config.path).map(|hash| hash.to_hex().to_string()) +} + pub fn verify_compiler_info( packages: &AHashMap, compiler: &CompilerInfo, @@ -51,35 +53,48 @@ pub fn verify_compiler_info( let current_bsc_path_str = compiler.bsc_path.to_string_lossy(); let current_bsc_hash_hex = compiler.bsc_hash.to_hex().to_string(); let current_runtime_path_str = compiler.runtime_path.to_string_lossy(); + let current_rescript_config_hash = match get_rescript_config_hash(package) { + Some(hash) => hash, + None => return true, // can't compute hash -> treat as mismatch + }; let mut mismatch = false; - if parsed.bsc_path.as_deref() != Some(¤t_bsc_path_str) { + if parsed.bsc_path != current_bsc_path_str { log::debug!( "compiler-info mismatch for {}: bsc_path changed (stored='{}', current='{}')", package.name, - parsed.bsc_path.as_deref().unwrap_or(""), + parsed.bsc_path, current_bsc_path_str ); mismatch = true; } - if parsed.bsc_hash.as_deref() != Some(¤t_bsc_hash_hex) { + if parsed.bsc_hash != current_bsc_hash_hex { log::debug!( "compiler-info mismatch for {}: bsc_hash changed (stored='{}', current='{}')", package.name, - parsed.bsc_hash.as_deref().unwrap_or(""), + parsed.bsc_hash, current_bsc_hash_hex ); mismatch = true; } - if parsed.runtime_path.as_deref() != Some(¤t_runtime_path_str) { + if parsed.runtime_path != current_runtime_path_str { log::debug!( "compiler-info mismatch for {}: runtime_path changed (stored='{}', current='{}')", package.name, - parsed.runtime_path.as_deref().unwrap_or(""), + parsed.runtime_path, current_runtime_path_str ); mismatch = true; } + if parsed.rescript_config_hash != current_rescript_config_hash { + log::debug!( + "compiler-info mismatch for {}: rescript_config_hash changed (stored='{}', current='{}')", + package.name, + parsed.rescript_config_hash, + current_rescript_config_hash + ); + mismatch = true; + } mismatch }) @@ -98,46 +113,58 @@ pub fn verify_compiler_info( } pub fn write_compiler_info(build_state: &BuildState) { - let bsc_path_str = build_state.compiler_info.bsc_path.to_string_lossy().to_string(); - let bsc_hash_hex = build_state.compiler_info.bsc_hash.to_hex().to_string(); - let runtime_path_str = build_state + let bsc_path = build_state.compiler_info.bsc_path.to_string_lossy().to_string(); + let bsc_hash = build_state.compiler_info.bsc_hash.to_hex().to_string(); + let runtime_path = build_state .compiler_info .runtime_path .to_string_lossy() .to_string(); - // derive version from the crate version let version = env!("CARGO_PKG_VERSION").to_string(); let generated_at = crate::helpers::get_system_time().to_string(); - let out = CompilerInfoFile { - version: Some(version), - bsc_path: Some(bsc_path_str), - bsc_hash: Some(bsc_hash_hex), - runtime_path: Some(runtime_path_str), - generated_at: Some(generated_at), - }; - let contents = serde_json::to_string_pretty(&out).unwrap_or_else(|_| String::new()); + // Borrowing serializer to avoid cloning the constant fields for every package + #[derive(Serialize)] + struct CompilerInfoFileRef<'a> { + version: &'a str, + bsc_path: &'a str, + bsc_hash: &'a str, + rescript_config_hash: String, + runtime_path: &'a str, + generated_at: &'a str, + } build_state.packages.values().par_bridge().for_each(|package| { - let info_path = package.get_compiler_info_path(); - let should_write = match std::fs::read_to_string(&info_path) { - Ok(existing) => existing != contents, - Err(_) => true, - }; + if let Some(rescript_config_hash) = helpers::compute_file_hash(&package.config.path) { + let out = CompilerInfoFileRef { + version: &version, + bsc_path: &bsc_path, + bsc_hash: &bsc_hash, + rescript_config_hash: rescript_config_hash.to_hex().to_string(), + runtime_path: &runtime_path, + generated_at: &generated_at, + }; + let contents = serde_json::to_string_pretty(&out).unwrap_or_else(|_| String::new()); + let info_path = package.get_compiler_info_path(); + let should_write = match std::fs::read_to_string(&info_path) { + Ok(existing) => existing != contents, + Err(_) => true, + }; - if should_write { - if let Some(parent) = info_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - // We write atomically to avoid leaving a partially written JSON file - // (e.g. process interruption) that would be read on the next init as an - // invalid/mismatched compiler-info, causing unnecessary cleans. The - // rename within the same directory is atomic on common platforms. - let tmp = info_path.with_extension("json.tmp"); - if let Ok(mut f) = File::create(&tmp) { - let _ = f.write_all(contents.as_bytes()); - let _ = std::fs::rename(&tmp, &info_path); + if should_write { + if let Some(parent) = info_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + // We write atomically to avoid leaving a partially written JSON file + // (e.g. process interruption) that would be read on the next init as an + // invalid/mismatched compiler-info, causing unnecessary cleans. The + // rename within the same directory is atomic on common platforms. + let tmp = info_path.with_extension("json.tmp"); + if let Ok(mut f) = File::create(&tmp) { + let _ = f.write_all(contents.as_bytes()); + let _ = std::fs::rename(&tmp, &info_path); + } } } }); From d97bca9c52d445199fd340428eb0c138ceb1f54b Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 15:12:24 +0200 Subject: [PATCH 3/4] Apply code review suggestions --- rewatch/src/build/compiler_info.rs | 41 +++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/rewatch/src/build/compiler_info.rs b/rewatch/src/build/compiler_info.rs index 43320405485..d3576d531df 100644 --- a/rewatch/src/build/compiler_info.rs +++ b/rewatch/src/build/compiler_info.rs @@ -145,7 +145,17 @@ pub fn write_compiler_info(build_state: &BuildState) { runtime_path: &runtime_path, generated_at: &generated_at, }; - let contents = serde_json::to_string_pretty(&out).unwrap_or_else(|_| String::new()); + let contents = match serde_json::to_string_pretty(&out) { + Ok(s) => s, + Err(err) => { + log::error!( + "Failed to serialize compiler-info for package {}: {}. Skipping write.", + package.name, + err + ); + return; + } + }; let info_path = package.get_compiler_info_path(); let should_write = match std::fs::read_to_string(&info_path) { Ok(existing) => existing != contents, @@ -162,8 +172,33 @@ pub fn write_compiler_info(build_state: &BuildState) { // rename within the same directory is atomic on common platforms. let tmp = info_path.with_extension("json.tmp"); if let Ok(mut f) = File::create(&tmp) { - let _ = f.write_all(contents.as_bytes()); - let _ = std::fs::rename(&tmp, &info_path); + if let Err(err) = f.write_all(contents.as_bytes()) { + log::error!( + "Failed to write compiler-info for package {} to temporary file {}: {}. Skipping rename.", + package.name, + tmp.display(), + err + ); + let _ = std::fs::remove_file(&tmp); + return; + } + if let Err(err) = f.sync_all() { + log::error!( + "Failed to flush compiler-info for package {}: {}. Skipping rename.", + package.name, + err + ); + let _ = std::fs::remove_file(&tmp); + return; + } + if let Err(err) = std::fs::rename(&tmp, &info_path) { + log::error!( + "Failed to atomically replace compiler-info for package {}: {}.", + package.name, + err + ); + let _ = std::fs::remove_file(&tmp); + } } } } From bef0e4058723efe1bdfe562f87527be4d1151566 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 15:13:37 +0200 Subject: [PATCH 4/4] Remove comment --- rewatch/src/build/compile.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index f47c08cd9db..7db67b653d8 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -764,9 +764,6 @@ fn compile_file( } }); - // TODO: Optionally record per-module successful compile timestamps if needed - // for future metadata-based invalidation. Current metadata is per-package. - if helpers::contains_ascii_characters(&err) { if package.is_local_dep { // suppress warnings of external deps