aboutsummaryrefslogtreecommitdiff
path: root/xtask/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'xtask/src/main.rs')
-rw-r--r--xtask/src/main.rs311
1 files changed, 310 insertions, 1 deletions
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index e1e5970..735ec94 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -29,6 +29,7 @@ use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
+use std::time::{SystemTime, UNIX_EPOCH};
const CORPORA_MANIFEST_ENV: &str = "FPARKAN_CORPORA_MANIFEST";
const PART1_ROOT_ENV: &str = "FPARKAN_CORPUS_PART1_ROOT";
@@ -36,6 +37,7 @@ const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT";
const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_roadmap.md";
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-audit.json";
+const SHADER_MANIFEST_REPORT: &str = "adapters/fparkan-render-vulkan/shaders/manifest.json";
const STAGE_PACKAGE_MANIFEST: &str = "fixtures/acceptance/stage_packages.toml";
const SUPPLY_CHAIN_POLICY_CONFIG: &str = "deny.toml";
const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["macos"];
@@ -73,6 +75,7 @@ fn run(args: &[String]) -> Result<(), String> {
[cmd] if cmd == "ci" => {
run_cargo_fmt_check()?;
run_policy(Path::new("."))?;
+ run_shader_provenance_verification()?;
cargo(&["test", "--workspace", "--all-targets", "--all-features", "--locked"])?;
cargo(&[
"clippy",
@@ -95,6 +98,7 @@ fn run(args: &[String]) -> Result<(), String> {
Ok(())
}
[cmd] if cmd == "policy" => run_policy(Path::new(".")),
+ [cmd] if cmd == "shader-provenance" => run_shader_provenance_verification(),
[cmd, subcmd, rest @ ..] if cmd == "acceptance" && subcmd == "report" => {
let options = parse_acceptance_options(rest)?;
run_acceptance_report(&options)
@@ -129,7 +133,7 @@ fn run(args: &[String]) -> Result<(), String> {
Ok(())
}
_ => Err(
- "usage: cargo xtask ci | policy | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | native-smoke audit --dir <path> | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path>"
+ "usage: cargo xtask ci | policy | shader-provenance | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | native-smoke audit --dir <path> | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path>"
.to_string(),
),
}
@@ -191,6 +195,285 @@ fn run_cargo_fmt_check() -> Result<(), String> {
}
}
+fn run_shader_provenance_verification() -> Result<(), String> {
+ let manifest_path = workspace_relative_path(SHADER_MANIFEST_REPORT);
+ let manifest = load_shader_manifest(&manifest_path)?;
+ let compiler_path = resolve_required_tool("FPARKAN_GLSLANG_VALIDATOR", &manifest.compiler)?;
+ let validator_path = resolve_required_tool("FPARKAN_SPIRV_VAL", &manifest.validator)?;
+
+ verify_tool_metadata(&compiler_path, &manifest.compiler)?;
+ verify_tool_metadata(&validator_path, &manifest.validator)?;
+
+ let out_dir = shader_provenance_output_dir();
+ if out_dir.exists() {
+ fs::remove_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?;
+ }
+ fs::create_dir_all(&out_dir).map_err(|err| format!("{}: {err}", out_dir.display()))?;
+
+ for module in &manifest.modules {
+ verify_shader_module(&manifest, module, &compiler_path, &validator_path, &out_dir)?;
+ }
+ Ok(())
+}
+
+fn load_shader_manifest(path: &Path) -> Result<ShaderManifestJson, String> {
+ let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
+ let manifest = serde_json::from_str::<ShaderManifestJson>(&text)
+ .map_err(|err| format!("{}: invalid shader manifest JSON: {err}", path.display()))?;
+ if manifest.modules.is_empty() {
+ return Err(format!(
+ "{}: shader manifest must describe at least one module",
+ path.display()
+ ));
+ }
+ Ok(manifest)
+}
+
+fn resolve_required_tool(
+ env_var: &str,
+ manifest: &ShaderToolManifestJson,
+) -> Result<PathBuf, String> {
+ let requested = std::env::var(env_var).unwrap_or_else(|_| manifest.name.clone());
+ resolve_tool_path(&requested).ok_or_else(|| {
+ format!(
+ "required shader tool {} is unavailable (set {env_var} to override path)",
+ manifest.name
+ )
+ })
+}
+
+fn resolve_tool_path(tool: &str) -> Option<PathBuf> {
+ let candidate = Path::new(tool);
+ 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;
+ }
+ let resolved = String::from_utf8(output.stdout).ok()?;
+ let resolved = resolved.trim();
+ (!resolved.is_empty()).then(|| PathBuf::from(resolved))
+}
+
+fn verify_tool_metadata(path: &Path, manifest: &ShaderToolManifestJson) -> Result<(), String> {
+ let actual_name = path
+ .file_name()
+ .and_then(|value| value.to_str())
+ .ok_or_else(|| format!("{}: invalid tool filename", path.display()))?;
+ if actual_name != manifest.name {
+ return Err(format!(
+ "{}: tool name mismatch, expected {}, found {}",
+ path.display(),
+ manifest.name,
+ actual_name
+ ));
+ }
+ 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 {}",
+ path.display(),
+ manifest.binary_sha256,
+ actual_sha256
+ ));
+ }
+ Ok(())
+}
+
+fn tool_version(path: &Path) -> Result<String, String> {
+ let output = Command::new(path)
+ .arg("--version")
+ .output()
+ .map_err(|err| format!("{} --version: {err}", path.display()))?;
+ if !output.status.success() {
+ return Err(format!(
+ "{} --version exited with {}",
+ path.display(),
+ output.status
+ ));
+ }
+ let stdout = String::from_utf8(output.stdout)
+ .map_err(|err| format!("{} --version: invalid UTF-8: {err}", path.display()))?;
+ stdout
+ .lines()
+ .find(|line| !line.trim().is_empty())
+ .map(str::trim)
+ .map(|line| {
+ line.strip_prefix("Glslang Version: ")
+ .unwrap_or(line)
+ .to_string()
+ })
+ .ok_or_else(|| format!("{} --version returned no version line", path.display()))
+}
+
+fn verify_shader_module(
+ manifest: &ShaderManifestJson,
+ module: &ShaderModuleManifestJson,
+ compiler_path: &Path,
+ validator_path: &Path,
+ out_dir: &Path,
+) -> Result<(), String> {
+ let source_path = workspace_relative_path(&module.source_path);
+ 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)?;
+ if source_sha256 != module.source_sha256 {
+ return Err(format!(
+ "{}: source SHA-256 mismatch, expected {}, found {}",
+ source_path.display(),
+ module.source_sha256,
+ source_sha256
+ ));
+ }
+
+ let checked_in_spirv_sha256 = sha256_file(&checked_in_spirv_path)?;
+ if checked_in_spirv_sha256 != module.sha256 {
+ return Err(format!(
+ "{}: checked-in SPIR-V SHA-256 mismatch, expected {}, found {}",
+ checked_in_spirv_path.display(),
+ module.sha256,
+ checked_in_spirv_sha256
+ ));
+ }
+
+ compile_shader_module(
+ compiler_path,
+ &manifest.target_env,
+ module,
+ &source_path,
+ &generated_spirv_path,
+ )?;
+ validate_shader_module(validator_path, &manifest.target_env, &generated_spirv_path)?;
+
+ let generated_spirv_sha256 = sha256_file(&generated_spirv_path)?;
+ if generated_spirv_sha256 != module.sha256 {
+ return Err(format!(
+ "{}: generated SPIR-V SHA-256 mismatch, expected {}, found {}",
+ generated_spirv_path.display(),
+ module.sha256,
+ generated_spirv_sha256
+ ));
+ }
+ Ok(())
+}
+
+fn compile_shader_module(
+ compiler_path: &Path,
+ target_env: &str,
+ module: &ShaderModuleManifestJson,
+ source_path: &Path,
+ output_path: &Path,
+) -> Result<(), String> {
+ let stage = glslang_stage(&module.stage).ok_or_else(|| {
+ format!(
+ "{}: unsupported shader stage {:?}",
+ source_path.display(),
+ module.stage
+ )
+ })?;
+ let output = Command::new(compiler_path)
+ .args(["-V", "--target-env", target_env, "-S", stage, "-e"])
+ .arg(&module.entry_point)
+ .arg(source_path)
+ .arg("-o")
+ .arg(output_path)
+ .output()
+ .map_err(|err| {
+ format!(
+ "{}: shader compile failed to start: {err}",
+ source_path.display()
+ )
+ })?;
+ if !output.status.success() {
+ return Err(format!(
+ "{}: shader compile failed:\n{}{}",
+ source_path.display(),
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(())
+}
+
+fn validate_shader_module(
+ validator_path: &Path,
+ target_env: &str,
+ module_path: &Path,
+) -> Result<(), String> {
+ let output = Command::new(validator_path)
+ .args(["--target-env", target_env])
+ .arg(module_path)
+ .output()
+ .map_err(|err| {
+ format!(
+ "{}: shader validation failed to start: {err}",
+ module_path.display()
+ )
+ })?;
+ if !output.status.success() {
+ return Err(format!(
+ "{}: shader validation failed:\n{}{}",
+ module_path.display(),
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(())
+}
+
+fn glslang_stage(stage: &str) -> Option<&'static str> {
+ match stage {
+ "vertex" => Some("vert"),
+ "fragment" => Some("frag"),
+ _ => None,
+ }
+}
+
+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());
+ }
+ }
+ Err(format!(
+ "{}: could not compute SHA-256 (tried shasum and sha256sum)",
+ path.display()
+ ))
+}
+
+fn shader_provenance_output_dir() -> PathBuf {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_nanos();
+ workspace_root_path()
+ .join("target")
+ .join("fparkan")
+ .join("shader-provenance")
+ .join(format!("{}-{}", std::process::id(), nonce))
+}
+
fn run_cargo_deny() -> Result<(), String> {
validate_supply_chain_policy_config(&workspace_relative_path(SUPPLY_CHAIN_POLICY_CONFIG))?;
let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into());
@@ -1374,6 +1657,32 @@ struct NativeSmokeAuditOptions {
dir: PathBuf,
}
+#[derive(Debug, Deserialize)]
+struct ShaderManifestJson {
+ target_env: String,
+ compiler: ShaderToolManifestJson,
+ validator: ShaderToolManifestJson,
+ modules: Vec<ShaderModuleManifestJson>,
+}
+
+#[derive(Debug, Deserialize)]
+struct ShaderToolManifestJson {
+ name: String,
+ version: String,
+ binary_sha256: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct ShaderModuleManifestJson {
+ name: String,
+ stage: String,
+ entry_point: String,
+ source_path: String,
+ source_sha256: String,
+ spirv_path: String,
+ sha256: String,
+}
+
fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOptions, String> {
let mut options = TestOptions {
stage: Stage::All,