rustc_windows_rc/
lib.rs

1//! A build script dependency to create a Windows resource file for the compiler
2//!
3//! Uses values from the `CFG_VERSION` and `CFG_RELEASE` environment variables
4//! to set the product and file version information in the Windows resource file.
5use std::{env, fs, path, process};
6
7/// The template for the Windows resource file.
8const RESOURCE_TEMPLATE: &str = include_str!("../rustc.rc.in");
9
10/// A subset of the possible values for the `FILETYPE` field in a Windows resource file
11///
12/// See the `dwFileType` member of [VS_FIXEDFILEINFO](https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo#members)
13#[derive(Debug, Clone, Copy)]
14#[repr(u32)]
15pub enum VersionInfoFileType {
16    /// `VFT_APP` - The file is an application.
17    App = 0x00000001,
18    /// `VFT_DLL` - The file is a dynamic link library.
19    Dll = 0x00000002,
20}
21
22/// Create and compile a Windows resource file with the product and file version information for the rust compiler.
23///
24/// Returns the path to the compiled resource file
25///
26/// Does not emit any cargo directives, the caller is responsible for that.
27pub fn compile_windows_resource_file(
28    file_stem: &path::Path,
29    file_description: &str,
30    filetype: VersionInfoFileType,
31) -> path::PathBuf {
32    let mut resources_dir = path::PathBuf::from(env::var_os("OUT_DIR").unwrap());
33    resources_dir.push("resources");
34    fs::create_dir_all(&resources_dir).unwrap();
35
36    let resource_compiler = if let Ok(path) = env::var("RUSTC_WINDOWS_RC") {
37        path.into()
38    } else {
39        find_msvc_tools::find_tool(&env::var("CARGO_CFG_TARGET_ARCH").unwrap(), "rc.exe")
40            .expect("found rc.exe")
41            .path()
42            .to_owned()
43    };
44
45    let rc_path = resources_dir.join(file_stem.with_extension("rc"));
46
47    write_resource_script_file(&rc_path, file_description, filetype);
48
49    let res_path = resources_dir.join(file_stem.with_extension("res"));
50
51    let status = process::Command::new(resource_compiler)
52        .arg("/fo")
53        .arg(&res_path)
54        .arg(&rc_path)
55        .status()
56        .expect("can execute resource compiler");
57    assert!(status.success(), "rc.exe failed with status {}", status);
58    assert!(
59        res_path.try_exists().unwrap_or(false),
60        "resource file {} was not created",
61        res_path.display()
62    );
63    res_path
64}
65
66/// Writes a Windows resource script file for the rust compiler with the product and file version information
67/// into `rc_path`
68fn write_resource_script_file(
69    rc_path: &path::Path,
70    file_description: &str,
71    filetype: VersionInfoFileType,
72) {
73    let mut resource_script = RESOURCE_TEMPLATE.to_string();
74
75    // Set the string product and file version to the same thing as `rustc --version`
76    let descriptive_version = env::var("CFG_VERSION").unwrap_or("unknown".to_string());
77
78    // Set the product name to "Rust Compiler" or "Rust Compiler (nightly)" etc
79    let product_name = product_name(env::var("CFG_RELEASE_CHANNEL").unwrap());
80
81    // For the numeric version we need `major,minor,patch,build`.
82    // Extract them from `CFG_RELEASE` which is "major.minor.patch" and a "-dev", "-nightly" or similar suffix
83    let cfg_release = env::var("CFG_RELEASE").unwrap();
84    // remove the suffix, if present and parse into [`ResourceVersion`]
85    let version = parse_version(cfg_release.split("-").next().unwrap_or("0.0.0"))
86        .expect("valid CFG_RELEASE version");
87
88    resource_script = resource_script
89        .replace("@RUSTC_FILEDESCRIPTION_STR@", file_description)
90        .replace("@RUSTC_FILETYPE@", &format!("{}", filetype as u32))
91        .replace("@RUSTC_FILEVERSION_QUAD@", &version.to_quad_string())
92        .replace("@RUSTC_FILEVERSION_STR@", &descriptive_version)
93        .replace("@RUSTC_PRODUCTNAME_STR@", &product_name)
94        .replace("@RUSTC_PRODUCTVERSION_QUAD@", &version.to_quad_string())
95        .replace("@RUSTC_PRODUCTVERSION_STR@", &descriptive_version);
96
97    fs::write(&rc_path, resource_script)
98        .unwrap_or_else(|_| panic!("failed to write resource file {}", rc_path.display()));
99}
100
101fn product_name(channel: String) -> String {
102    format!(
103        "Rust Compiler{}",
104        if channel == "stable" { "".to_string() } else { format!(" ({})", channel) }
105    )
106}
107
108/// Windows resources store versions as four 16-bit integers.
109struct ResourceVersion {
110    major: u16,
111    minor: u16,
112    patch: u16,
113    build: u16,
114}
115
116impl ResourceVersion {
117    /// Format the version as a comma-separated string of four integers
118    /// as expected by Windows resource scripts for the `FILEVERSION` and `PRODUCTVERSION` fields.
119    fn to_quad_string(&self) -> String {
120        format!("{},{},{},{}", self.major, self.minor, self.patch, self.build)
121    }
122}
123
124/// Parse a string in the format "major.minor.patch" into a [`ResourceVersion`].
125/// The build is set to 0.
126/// Returns `None` if the version string is not in the expected format.
127fn parse_version(version: &str) -> Option<ResourceVersion> {
128    let mut parts = version.split('.');
129    let major = parts.next()?.parse::<u16>().ok()?;
130    let minor = parts.next()?.parse::<u16>().ok()?;
131    let patch = parts.next()?.parse::<u16>().ok()?;
132    if parts.next().is_some() {
133        None
134    } else {
135        Some(ResourceVersion { major, minor, patch, build: 0 })
136    }
137}