1use std::{
13 ffi::OsString,
14 io,
15 path::{Path, PathBuf},
16};
17
18use sha2::{Digest, Sha256};
19use waterui_assets_core::{AssetError, download_remote_bytes, write_bytes_atomically};
20
21use crate::{
22 project_model::water_dir::{HomeDirError, water_home_dir_in},
23 toolchain::Host,
24};
25
26#[derive(Debug, Clone)]
28pub struct ManagedTool {
29 pub name: &'static str,
31 pub version: &'static str,
33 pub url: String,
35 pub sha256: &'static str,
37 pub binary: String,
39}
40
41impl ManagedTool {
42 pub fn install_dir(&self, host: &Host) -> Result<PathBuf, HomeDirError> {
47 Ok(water_home_dir_in(host)?
48 .join("tools")
49 .join(self.name)
50 .join(self.version))
51 }
52
53 #[must_use]
55 pub fn binary_path(&self, host: &Host) -> Option<PathBuf> {
56 let path = self.install_dir(host).ok()?.join(&self.binary);
57 path.is_file().then_some(path)
58 }
59
60 #[must_use]
65 pub fn binary_dir(&self, host: &Host) -> Option<PathBuf> {
66 self.binary_path(host)
67 .and_then(|path| path.parent().map(Path::to_path_buf))
68 }
69
70 pub async fn install(&self, host: &Host) -> Result<PathBuf, ManagedToolError> {
81 if let Some(dir) = self.binary_dir(host) {
82 return Ok(dir);
83 }
84
85 let tool_dir = water_home_dir_in(host)?.join("tools").join(self.name);
86 let install_dir = tool_dir.join(self.version);
87 smol::unblock({
88 let tool_dir = tool_dir.clone();
89 move || std::fs::create_dir_all(&tool_dir)
90 })
91 .await?;
92
93 let staging = smol::unblock({
94 move || {
95 tempfile::Builder::new()
96 .prefix(".staging-")
97 .tempdir_in(&tool_dir)
98 }
99 })
100 .await?;
101 let archive_path = staging.path().join("archive.zip");
102 fetch_pinned(&self.url, self.sha256, &archive_path).await?;
103
104 let extract_dir = staging.path().join("extract");
105 smol::unblock({
106 let archive_path = archive_path.clone();
107 let extract_dir = extract_dir.clone();
108 move || -> Result<(), ManagedToolError> {
109 std::fs::create_dir_all(&extract_dir)?;
110 let file = std::fs::File::open(&archive_path)?;
111 zip::ZipArchive::new(file)?.extract(&extract_dir)?;
112 Ok(())
113 }
114 })
115 .await?;
116
117 remove_directory_if_exists(&install_dir).await?;
118 smol::unblock(move || std::fs::rename(&extract_dir, &install_dir)).await?;
119
120 self.binary_dir(host)
121 .ok_or_else(|| ManagedToolError::ArchiveLayout {
122 name: self.name,
123 binary: self.binary.clone(),
124 })
125 }
126}
127
128#[must_use]
131pub fn dxc() -> ManagedTool {
132 let binary = if cfg!(target_arch = "aarch64") {
133 "bin/arm64/dxc.exe"
134 } else if cfg!(target_arch = "x86_64") {
135 "bin/x64/dxc.exe"
136 } else {
137 "bin/x86/dxc.exe"
138 };
139 ManagedTool {
140 name: "dxc",
141 version: "1.8.2505.1",
142 url: "https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.8.2505.1/dxc_2025_07_14.zip"
143 .to_string(),
144 sha256: "9ad895a6b039e3a8f8c22a1009f866800b840a74b50db9218d13319e215ea8a4",
145 binary: binary.to_string(),
146 }
147}
148
149#[must_use]
152pub fn cmake() -> Option<ManagedTool> {
153 let (package, sha256) = if cfg!(target_arch = "x86_64") {
154 (
155 "cmake-4.4.3-windows-x86_64",
156 "4d52ebab7193a698651639ed80d8d04fd903358843572cf44c7fd234cb7c26ab",
157 )
158 } else if cfg!(target_arch = "aarch64") {
159 (
160 "cmake-4.4.3-windows-arm64",
161 "7b410ddd00e24c7250eec7452da2348a4a70437aa87e9cda0a20d6a85662fcff",
162 )
163 } else if cfg!(target_arch = "x86") {
164 (
165 "cmake-4.4.3-windows-i386",
166 "018024d05e2fc77d386046da87f90345f9beea21e35c5c8ab02fd15421b7da18",
167 )
168 } else {
169 return None;
170 };
171 Some(ManagedTool {
172 name: "cmake",
173 version: "4.4.3",
174 url: format!("https://github.com/Kitware/CMake/releases/download/v4.4.3/{package}.zip"),
175 sha256,
176 binary: format!("{package}/bin/cmake.exe"),
177 })
178}
179
180#[must_use]
183pub fn sccache() -> Option<ManagedTool> {
184 let (package, sha256) = if cfg!(target_arch = "x86_64") {
185 (
186 "sccache-v0.18.0-x86_64-pc-windows-msvc",
187 "8965c74d5e8a225244f741e18ad2f3f504f48228dc1bac948fc22761a348363d",
188 )
189 } else if cfg!(target_arch = "aarch64") {
190 (
191 "sccache-v0.18.0-aarch64-pc-windows-msvc",
192 "205d613fa74a9a0525e41a5ace77b1c71907d5bd4a5e668bad79111776829290",
193 )
194 } else {
195 return None;
196 };
197 Some(ManagedTool {
198 name: "sccache",
199 version: "0.18.0",
200 url: format!("https://github.com/mozilla/sccache/releases/download/v0.18.0/{package}.zip"),
201 sha256,
202 binary: format!("{package}/sccache.exe"),
203 })
204}
205
206#[must_use]
209pub fn jdk() -> Option<ManagedTool> {
210 let (package, sha256) = if cfg!(target_arch = "x86_64") {
211 (
212 "OpenJDK21U-jdk_x64_windows_hotspot_21.0.12.1_1",
213 "f9d6e191ab098c0d416e7d588a24420a8621cd2f4720dab2459b8b7b2d2d8b4e",
214 )
215 } else if cfg!(target_arch = "aarch64") {
216 (
217 "OpenJDK21U-jdk_aarch64_windows_hotspot_21.0.12.1_1",
218 "ccf2e51f527d542a70ba5794a600d3aac04b4e967950e227834c7566cb1bec7b",
219 )
220 } else {
221 return None;
222 };
223 Some(ManagedTool {
224 name: "jdk",
225 version: "21.0.12.1+1",
226 url: format!(
227 "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12.1%2B1/{package}.zip"
228 ),
229 sha256,
230 binary: "jdk-21.0.12.1+1/bin/java.exe".to_string(),
231 })
232}
233
234#[must_use]
236pub fn all() -> Vec<ManagedTool> {
237 let mut tools = vec![dxc()];
238 for tool in [cmake(), sccache(), jdk()].into_iter().flatten() {
239 tools.push(tool);
240 }
241 tools
242}
243
244pub const LLVM_ARM64_MSI_URL: &str =
247 "https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.1/LLVM-23.1.1-woa64.msi";
248pub const LLVM_ARM64_MSI_SHA256: &str =
250 "aeb4415a5fcfd488dc0ef69ccb2c85a81163c11960b63b6b295c6c3df5a6317e";
251
252pub async fn fetch_pinned(
259 url: &str,
260 sha256: &str,
261 destination: &Path,
262) -> Result<(), ManagedToolError> {
263 let bytes = download_remote_bytes(url).await?;
264 let actual = hex::encode(Sha256::digest(&bytes));
265 if actual != sha256 {
266 return Err(ManagedToolError::Checksum {
267 expected: sha256.to_string(),
268 actual,
269 });
270 }
271 write_bytes_atomically(destination, &bytes).await?;
272 Ok(())
273}
274
275#[must_use]
281pub fn managed_tools_path_env(host: &Host) -> Option<(String, OsString)> {
282 let mut entries: Vec<PathBuf> = all()
283 .into_iter()
284 .filter_map(|tool| tool.binary_dir(host))
285 .collect();
286 if entries.is_empty() {
287 return None;
288 }
289 entries.extend(host.path_entries());
290 let value = std::env::join_paths(entries).ok()?;
291 Some(("PATH".to_string(), value))
292}
293
294async fn remove_directory_if_exists(path: &Path) -> Result<(), ManagedToolError> {
295 if smol::unblock({
296 let path = path.to_path_buf();
297 move || path.is_dir()
298 })
299 .await
300 {
301 smol::unblock({
302 let path = path.to_path_buf();
303 move || std::fs::remove_dir_all(&path)
304 })
305 .await?;
306 }
307 Ok(())
308}
309
310#[derive(Debug, thiserror::Error)]
312pub enum ManagedToolError {
313 #[error(transparent)]
315 Asset(#[from] AssetError),
316
317 #[error(transparent)]
319 HomeDir(#[from] HomeDirError),
320
321 #[error(transparent)]
323 Io(#[from] io::Error),
324
325 #[error("Could not unpack the release archive: {0}")]
327 Zip(#[from] zip::result::ZipError),
328
329 #[error(
331 "Checksum mismatch for the downloaded archive: expected sha256 {expected}, got {actual}"
332 )]
333 Checksum {
334 expected: String,
336 actual: String,
338 },
339
340 #[error("The unpacked {name} archive does not contain the expected binary `{binary}`")]
342 ArchiveLayout {
343 name: &'static str,
345 binary: String,
347 },
348}