use std::path::{Path, PathBuf};
use crate::error::BridgeError;
const INIT_FAILED: &str = "HostInitFailed";
pub fn ensure(backend_csproj: &Path, assembly_name: &str, tfm: &str) -> Result<PathBuf, BridgeError> {
let backend_dir = backend_csproj.parent().ok_or_else(|| {
BridgeError::new(
INIT_FAILED,
format!("The backend project path {} has no parent directory.", backend_csproj.display()),
)
})?;
let wrapper_dir = backend_dir.join("obj").join("tauri-dotnet-watch-runner");
std::fs::create_dir_all(&wrapper_dir).map_err(|e| {
BridgeError::new(
INIT_FAILED,
format!("Failed to create the dotnet-watch runner folder at {}: {e}", wrapper_dir.display()),
)
})?;
let csproj_path = wrapper_dir.join("TauriDotNetWatchRunner.csproj");
let program_path = wrapper_dir.join("Program.cs");
let csproj_contents = format!(
r#"<Project Sdk="Microsoft.NET.Sdk">
<!-- Generated by tauri-plugin-dotnet on every sidecar start; safe to delete, regenerated on demand. -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>{tfm}</TargetFramework>
<!-- Program.cs is one fully-qualified line with no nullable-sensitive code, so neither
Nullable nor ImplicitUsings buys anything here - deliberately left off.
This project exists only so `dotnet watch` sees the backend in its project graph; it is
never packaged or published on its own. -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="{backend_csproj}" />
</ItemGroup>
</Project>
"#,
tfm = xml_escape(tfm),
backend_csproj = xml_escape(&backend_csproj.display().to_string()),
);
let program_contents = format!(
"// Generated by tauri-plugin-dotnet; do not edit - regenerated on every sidecar start.\n\
return await Tauri.Plugin.DotNet.Hosting.Sidecar.SidecarRunner.RunAsync(args, {assembly_name:?});\n"
);
write_if_changed(&csproj_path, &csproj_contents)?;
write_if_changed(&program_path, &program_contents)?;
Ok(csproj_path)
}
fn write_if_changed(path: &Path, contents: &str) -> Result<(), BridgeError> {
if let Ok(existing) = std::fs::read_to_string(path) {
if existing == contents {
return Ok(());
}
}
std::fs::write(path, contents)
.map_err(|e| BridgeError::new(INIT_FAILED, format!("Failed to write {}: {e}", path.display())))
}
fn xml_escape(value: &str) -> String {
value.replace('&', "&").replace('<', "<").replace('>', ">")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_a_project_referencing_the_backend() {
let dir = std::env::temp_dir().join(format!("tdn-wrapper-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let backend_csproj = dir.join("MyApp.Backend.csproj");
std::fs::write(&backend_csproj, "<Project />").unwrap();
let wrapper_csproj = ensure(&backend_csproj, "MyApp.Backend", "net8.0").unwrap();
assert!(wrapper_csproj.exists());
let csproj_text = std::fs::read_to_string(&wrapper_csproj).unwrap();
assert!(csproj_text.contains(&backend_csproj.display().to_string()));
assert!(csproj_text.contains("net8.0"));
let program_text = std::fs::read_to_string(wrapper_csproj.with_file_name("Program.cs")).unwrap();
assert!(program_text.contains("SidecarRunner.RunAsync"));
assert!(program_text.contains("\"MyApp.Backend\""));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn does_not_rewrite_unchanged_files() {
let dir = std::env::temp_dir().join(format!("tdn-wrapper-test-{}", std::process::id() as u64 + 1));
std::fs::create_dir_all(&dir).unwrap();
let backend_csproj = dir.join("MyApp.Backend.csproj");
std::fs::write(&backend_csproj, "<Project />").unwrap();
let wrapper_csproj = ensure(&backend_csproj, "MyApp.Backend", "net8.0").unwrap();
let before = std::fs::metadata(&wrapper_csproj).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
ensure(&backend_csproj, "MyApp.Backend", "net8.0").unwrap();
let after = std::fs::metadata(&wrapper_csproj).unwrap().modified().unwrap();
assert_eq!(before, after, "unchanged content must not touch the file's mtime");
std::fs::remove_dir_all(&dir).ok();
}
}