CLI: pdfcraft-cli.exe carries a Windows version resource (#598)

pdfcraft-cli.exe had no VERSIONINFO, so Explorer's Details tab,
(Get-Item pdfcraft-cli.exe).VersionInfo and inventory tools showed no
version or publisher for it. A build.rs like the app's embeds one with
winresource (already a build dependency of pdfcraft): the same product
name and copyright, its own description and file names, and the package
version. PDFCRAFT_REQUIRE_WINRES=1, which the Windows packaging already
sets, makes a missing resource compiler an error; otherwise it's a
warning, and other targets are unaffected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
jcl-bit
2026-10-09 19:09:23 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 74e30e5803
commit 994b42c15c
4 changed files with 87 additions and 0 deletions
Generated
+1
View File
@@ -3606,6 +3606,7 @@ dependencies = [
"pdfcraft-render",
"rustix 1.1.5",
"serde_json",
"winresource",
]
[[package]]
+4
View File
@@ -25,6 +25,10 @@ base64 = "0.22"
# Already in Cargo.lock through the GUI crates.
rustix = { version = "1", features = ["process"] }
[build-dependencies]
# VERSIONINFO for pdfcraft-cli.exe; build.rs only uses it when targeting Windows.
winresource = "0.1"
[features]
default = ["mcp"]
# The opt-in MCP server (`pdfcraft-cli mcp`). Build with --no-default-features to leave it out.
+28
View File
@@ -0,0 +1,28 @@
//! Windows only: embed version info (VERSIONINFO) into `pdfcraft-cli.exe`, so Explorer's
//! Details tab, `(Get-Item pdfcraft-cli.exe).VersionInfo` and inventory tools can read the version
//! and publisher, as they can for `pdfcraft.exe` (`apps/pdfcraft/build.rs`).
//!
//! On every other target this does nothing. A missing resource compiler is a warning, so a
//! cross-compile from macOS or Linux still links, unless `PDFCRAFT_REQUIRE_WINRES=1` turns it
//! into an error (for release builds).
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=PDFCRAFT_REQUIRE_WINRES");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let mut res = winresource::WindowsResource::new();
res.set("ProductName", "PdfCraft")
.set("FileDescription", "PdfCraft command-line tool")
.set("LegalCopyright", "Copyright (c) the PdfCraft contributors. MIT OR Apache-2.0.")
.set("OriginalFilename", "pdfcraft-cli.exe")
.set("InternalName", "pdfcraft-cli");
if let Err(e) = res.compile() {
if std::env::var_os("PDFCRAFT_REQUIRE_WINRES").is_some() {
println!("cargo::error=embedding Windows resources failed: {e}");
return;
}
println!("cargo:warning=pdfcraft-cli.exe built without version resources: {e}");
}
}
@@ -0,0 +1,54 @@
//! `pdfcraft-cli.exe` carries a Windows version resource (`build.rs`), so Explorer, PowerShell's
//! `VersionInfo` and inventory tools see its version and publisher, as they do for `pdfcraft.exe`.
#![cfg(windows)]
/// `s` as UTF-16LE, the encoding of every string in a version resource.
fn utf16(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
/// A little-endian 32-bit DWORD at `at`.
fn dword(bytes: &[u8], at: usize) -> u32 {
let b: [u8; 4] = bytes[at..at + 4].try_into().unwrap();
u32::from_le_bytes(b)
}
/// The value of string-table entry `key`: the key and its terminator, zero padding to the next
/// 32-bit boundary, then the value and its terminator.
fn string_value(resource: &[u8], key: &str) -> Option<String> {
let after_key = find(resource, &utf16(&format!("{key}\0")))? + utf16(key).len() + 2;
let start = after_key + resource[after_key..].iter().take_while(|&&b| b == 0).count();
let units: Vec<u16> = resource[start..].as_chunks::<2>().0.iter().map(|&c| u16::from_le_bytes(c)).take_while(|&u| u != 0).collect();
String::from_utf16(&units).ok()
}
#[test]
fn the_exe_has_version_info_with_the_package_version() {
let exe = std::fs::read(env!("CARGO_BIN_EXE_pdfcraft-cli")).unwrap();
let start = find(&exe, &utf16("VS_VERSION_INFO")).expect("no version resource");
let resource = &exe[start..];
for (key, value) in [
("ProductName", "PdfCraft"),
("FileDescription", "PdfCraft command-line tool"),
("LegalCopyright", "Copyright (c) the PdfCraft contributors. MIT OR Apache-2.0."),
("OriginalFilename", "pdfcraft-cli.exe"),
("InternalName", "pdfcraft-cli"),
("FileVersion", env!("CARGO_PKG_VERSION")),
("ProductVersion", env!("CARGO_PKG_VERSION")),
] {
assert_eq!(string_value(resource, key).as_deref(), Some(value), "{key}");
}
// VS_FIXEDFILEINFO: signature, struct version, then file and product versions as
// major << 16 | minor and patch << 16 | build.
let fixed = start + find(resource, &0xFEEF_04BDu32.to_le_bytes()).expect("no VS_FIXEDFILEINFO");
let number = |s: &str| -> u32 { s.parse().unwrap() };
let ms = (number(env!("CARGO_PKG_VERSION_MAJOR")) << 16) | number(env!("CARGO_PKG_VERSION_MINOR"));
let ls = number(env!("CARGO_PKG_VERSION_PATCH")) << 16;
assert_eq!([dword(&exe, fixed + 8), dword(&exe, fixed + 12)], [ms, ls], "file version");
assert_eq!([dword(&exe, fixed + 16), dword(&exe, fixed + 20)], [ms, ls], "product version");
}