aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 04:27:10 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 04:27:10 +0300
commit02b4da2e3d78cc7df7278d7b17cf65620d58db54 (patch)
treec133c799b0f02275ba1cce82193bec32ac9be51a
parent9f7a94b0d90756a171b77c50d070a3e24cf1cb64 (diff)
downloadfparkan-02b4da2e3d78cc7df7278d7b17cf65620d58db54.tar.xz
fparkan-02b4da2e3d78cc7df7278d7b17cf65620d58db54.zip
build: support Windows shader provenance
-rw-r--r--Cargo.lock1
-rw-r--r--xtask/Cargo.toml1
-rw-r--r--xtask/src/main.rs126
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<PathBuf> {
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);
+ }
+
+ windows_vulkan_sdk_tool(tool)
+}
+
+fn find_tool_on_path(tool: &str) -> Option<PathBuf> {
+ 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<PathBuf> {
+ let mut roots = Vec::new();
+ if let Some(root) = std::env::var_os("VULKAN_SDK") {
+ roots.push(PathBuf::from(root));
}
- let resolved = String::from_utf8(output.stdout).ok()?;
- let resolved = resolved.trim();
- (!resolved.is_empty()).then(|| PathBuf::from(resolved))
+ 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::<Vec<_>>();
+ 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<PathBuf> {
+ 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 {:?}",
- 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 {}",
+ 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,
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<String, String> {
- 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<String, String> {
+ 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(())
+ }
}