proto-build: portable protoc dependency emission (fixes the Windows release build)

emit_rerun_if_changed piped protoc output through /dev/stdout and
/dev/null, which do not exist on Windows — the release build's Windows
job failed there even with protoc on PATH. Both protoc outputs now go
to real files under the build script's OUT_DIR (deterministic names, so
reruns overwrite), the dependency list is read from disk, and the
make-style target prefix is matched with normalized path separators
since protoc may spell the target with forward slashes on Windows.
Verified locally by forcing a fresh build-script run: the .d file lands
in OUT_DIR and kigi-tools-api builds cleanly.
This commit is contained in:
2026-07-18 05:14:42 -04:00
parent 9bb4087b28
commit 641c73899c
+34 -10
View File
@@ -3,7 +3,7 @@ pub mod find_protoc;
use anyhow::Context; use anyhow::Context;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{fs, iter}; use std::{env, fs, iter};
/// Find the protoc well-known types include directory. /// Find the protoc well-known types include directory.
/// ///
@@ -113,11 +113,27 @@ impl XaiProtoBuilder {
} }
// Can only process one input file when using --dependency_out=FILE. // Can only process one input file when using --dependency_out=FILE.
// Both protoc outputs go to real files: /dev/stdout and /dev/null do
// not exist on Windows (the release build failed on exactly this).
// OUT_DIR is always set for build scripts; deterministic names make
// reruns overwrite instead of accumulate.
let scratch_dir = env::var_os("OUT_DIR")
.map(PathBuf::from)
.unwrap_or_else(env::temp_dir);
for proto in protos { for proto in protos {
let stem = proto
.file_stem()
.and_then(|s| s.to_str())
.context("proto file name not UTF-8")?;
let dep_file = scratch_dir.join(format!("{stem}.protoc-deps.d"));
let descriptor_file = scratch_dir.join(format!("{stem}.protoc-desc.bin"));
let mut command = Command::new(protoc.unwrap_or(Path::new("protoc"))); let mut command = Command::new(protoc.unwrap_or(Path::new("protoc")));
command command
.arg("--dependency_out=/dev/stdout") .arg(format!("--dependency_out={}", dep_file.display()))
.arg("--descriptor_set_out=/dev/null"); .arg(format!(
"--descriptor_set_out={}",
descriptor_file.display()
));
// Add protoc's well-known types include directory first (if found). // Add protoc's well-known types include directory first (if found).
// This is needed for Bazel sandboxed builds where protoc and its // This is needed for Bazel sandboxed builds where protoc and its
@@ -143,15 +159,23 @@ impl XaiProtoBuilder {
return Err(anyhow::anyhow!("protoc command failed")); return Err(anyhow::anyhow!("protoc command failed"));
} }
let output = let output = fs::read_to_string(&dep_file)
String::from_utf8(output.stdout).context("protoc command output not UTF-8")?; .with_context(|| format!("read protoc dependency file {}", dep_file.display()))?;
// Make-style `.d` format: `<descriptor path>: dep1 dep2 …`.
// Compare with normalized separators — protoc may spell the
// target path with forward slashes even on Windows.
let mut lines = output.lines(); let mut lines = output.lines();
let first_line = lines.next().context("protoc command output is empty")?; let first_line = lines.next().context("protoc dependency output is empty")?;
let prefix = "/dev/null:"; let normalized_first = first_line.replace('\\', "/");
let rem = first_line.strip_prefix(prefix).with_context(|| { let prefix = format!("{}:", descriptor_file.display()).replace('\\', "/");
format!("protoc command output must start with /dev/null: {output:?}") let rem_len = normalized_first
})?; .strip_prefix(&prefix)
.with_context(|| {
format!("protoc dependency output must start with {prefix:?}: {output:?}")
})?
.len();
let rem = &first_line[first_line.len() - rem_len..];
for line in iter::once(rem).chain(lines) { for line in iter::once(rem).chain(lines) {
let line = line.trim(); let line = line.trim();
let line = line.strip_suffix("\\").unwrap_or(line); let line = line.strip_suffix("\\").unwrap_or(line);