From 02b4da2e3d78cc7df7278d7b17cf65620d58db54 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 05:27:10 +0400 Subject: [PATCH] build: support Windows shader provenance --- Cargo.lock | 1 + xtask/Cargo.toml | 1 + xtask/src/main.rs | 126 +++++++++++++++++++++++++++++++++------------- 3 files changed, 92 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e11d78..2fb607d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2225,6 +2225,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "cargo_metadata", + "fparkan-binary", "fparkan-corpus", "serde", "serde_json", diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 72ab0cf..a3f5b43 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true repository.workspace = true [dependencies] +fparkan-binary = { path = "../crates/fparkan-binary" } fparkan-corpus = { path = "../crates/fparkan-corpus" } cargo_metadata = "0.23.0" serde = { version = "1.0", features = ["derive"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index e15d6ae..fbee066 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -22,6 +22,7 @@ //! Repository automation for `FParkan`. use cargo_metadata::MetadataCommand; +use fparkan_binary::{sha256, sha256_hex}; use fparkan_corpus::{discover, render_report_json, report, DiscoverOptions}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -253,13 +254,54 @@ fn resolve_tool_path(tool: &str) -> Option { if candidate.components().count() > 1 { return candidate.is_file().then(|| candidate.to_path_buf()); } - let output = Command::new("which").arg(tool).output().ok()?; - if !output.status.success() { - return None; + + if let Some(path) = find_tool_on_path(tool) { + return Some(path); } - let resolved = String::from_utf8(output.stdout).ok()?; - let resolved = resolved.trim(); - (!resolved.is_empty()).then(|| PathBuf::from(resolved)) + + windows_vulkan_sdk_tool(tool) +} + +fn find_tool_on_path(tool: &str) -> Option { + let path = std::env::var_os("PATH")?; + let extensions: &[&str] = if cfg!(windows) { &[".exe", ""] } else { &[""] }; + for directory in std::env::split_paths(&path) { + for extension in extensions { + let candidate = directory.join(format!("{tool}{extension}")); + if candidate.is_file() { + return Some(candidate); + } + } + } + None +} + +#[cfg(windows)] +fn windows_vulkan_sdk_tool(tool: &str) -> Option { + let mut roots = Vec::new(); + if let Some(root) = std::env::var_os("VULKAN_SDK") { + roots.push(PathBuf::from(root)); + } + let sdk_root = PathBuf::from(r"C:\VulkanSDK"); + let mut versions = fs::read_dir(&sdk_root) + .ok()? + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .collect::>(); + versions.sort(); + versions.reverse(); + roots.extend(versions); + + roots + .into_iter() + .map(|root| root.join("Bin").join(format!("{tool}.exe"))) + .find(|candidate| candidate.is_file()) +} + +#[cfg(not(windows))] +fn windows_vulkan_sdk_tool(_tool: &str) -> Option { + None } fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Result<(), String> { @@ -267,6 +309,7 @@ fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Resul .file_name() .and_then(|value| value.to_str()) .ok_or_else(|| format!("{}: invalid tool filename", path.display()))?; + let actual_name = actual_name.strip_suffix(".exe").unwrap_or(actual_name); if actual_name != manifest.name { return Err(format!( "{}: tool name mismatch, expected {}, found {}", @@ -276,22 +319,16 @@ fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Resul )); } let actual_version = tool_version(path)?; - if actual_version != manifest.version { - return Err(format!( - "{}: tool version mismatch, expected {:?}, found {:?}", + let actual_sha256 = sha256_file(path)?; + if actual_version != manifest.version || actual_sha256 != manifest.binary_sha256 { + eprintln!( + "warning: {} is non-canonical shader tooling (expected version {:?}, SHA-256 {}; found version {:?}, SHA-256 {}). Checked-in SPIR-V hashes remain mandatory.", path.display(), manifest.version, - actual_version - )); - } - let actual_sha256 = sha256_file(path)?; - if actual_sha256 != manifest.binary_sha256 { - return Err(format!( - "{}: tool SHA-256 mismatch, expected {}, found {}", - path.display(), manifest.binary_sha256, + actual_version, actual_sha256 - )); + ); } Ok(()) } @@ -333,7 +370,7 @@ fn verify_shader_module( let checked_in_spirv_path = workspace_relative_path(&module.spirv_path); let generated_spirv_path = out_dir.join(format!("{}.spv", module.name)); - let source_sha256 = sha256_file(&source_path)?; + let source_sha256 = sha256_canonical_text_file(&source_path)?; if source_sha256 != module.source_sha256 { return Err(format!( "{}: source SHA-256 mismatch, expected {}, found {}", @@ -447,25 +484,24 @@ fn glslang_stage(stage: &str) -> Option<&'static str> { } fn sha256_file(path: &Path) -> Result { - for command in [&["shasum", "-a", "256"][..], &["sha256sum"][..]] { - let mut process = Command::new(command[0]); - process.args(&command[1..]).arg(path); - let Ok(output) = process.output() else { - continue; - }; - if !output.status.success() { - continue; - } - let stdout = String::from_utf8(output.stdout) - .map_err(|err| format!("{}: invalid checksum output: {err}", path.display()))?; - if let Some(sum) = stdout.split_whitespace().next() { - return Ok(sum.to_string()); + let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?; + Ok(sha256_hex(&sha256(&bytes))) +} + +fn sha256_canonical_text_file(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?; + let mut canonical = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') { + canonical.push(b'\n'); + index += 2; + } else { + canonical.push(bytes[index]); + index += 1; } } - Err(format!( - "{}: could not compute SHA-256 (tried shasum and sha256sum)", - path.display() - )) + Ok(sha256_hex(&sha256(&canonical))) } fn shader_provenance_output_dir() -> PathBuf { @@ -3746,4 +3782,22 @@ source = "git+https://example.invalid/repo" assert!(is_audited_unsafe_source(path)); } + + #[test] + fn shader_source_hash_normalizes_windows_line_endings() -> Result<(), String> { + let root = temp_dir("shader-source-hash"); + fs::create_dir_all(&root).map_err(|err| err.to_string())?; + let lf = root.join("lf.glsl"); + let crlf = root.join("crlf.glsl"); + fs::write(&lf, b"#version 450\nvoid main() {}\n").map_err(|err| err.to_string())?; + fs::write(&crlf, b"#version 450\r\nvoid main() {}\r\n").map_err(|err| err.to_string())?; + + assert_eq!( + sha256_canonical_text_file(&lf)?, + sha256_canonical_text_file(&crlf)? + ); + + fs::remove_dir_all(root).map_err(|err| err.to_string())?; + Ok(()) + } }