Skip to main content

tauri_winres/
lib.rs

1//! Rust Windows resource helper
2//!
3//! This crate implements a simple generator for Windows resource (.rc) files
4//! for use with either Microsoft `rc.exe` resource compiler or with GNU `windres.exe`
5//!
6//! The [`WindowsResorce::compile()`] method is intended to be used from a build script and
7//! needs environment variables from cargo to be set. It not only compiles the resource
8//! but directs cargo to link the resource compiler's output.
9//!
10//! # Example
11//!
12//! ```rust
13//! # use std::io;
14//! # fn test_main() -> io::Result<()> {
15//! if cfg!(target_os = "windows") {
16//!     let mut res = tauri_winres::WindowsResource::new();
17//!     res.set_icon("test.ico")
18//! #      .set_output_directory(".")
19//!        .set("InternalName", "TEST.EXE")
20//!        // manually set version 1.0.0.0
21//!        .set_version_info(tauri_winres::VersionInfo::PRODUCTVERSION, 0x0001000000000000);
22//!     res.compile()?;
23//! }
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! # Defaults
29//!
30//! We try to guess some sensible default values from Cargo's build time environement variables
31//! This is described in [`WindowsResource::new()`]. Furthermore we have to know where to find the
32//! resource compiler for the MSVC Toolkit. This can be done by looking up a registry key but
33//! for MinGW this has to be done manually.
34//!
35//! The following paths are the hardcoded defaults:
36//! MSVC the last registry key at
37//! `HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots`, for MinGW we try our luck by simply
38//! using the `%PATH%` environment variable.
39//!
40//! Note that the toolkit bitness as to match the one from the current Rust compiler. If you are
41//! using Rust GNU 64-bit you have to use MinGW64. For MSVC this is simpler as (recent) Windows
42//! SDK always installs both versions on a 64-bit system.
43//!
44//! [`WindowsResorce::compile()`]: struct.WindowsResource.html#method.compile
45//! [`WindowsResource::new()`]: struct.WindowsResource.html#method.new
46
47mod helpers;
48
49use std::collections::BTreeMap;
50use std::env;
51use std::fs;
52use std::io;
53use std::io::prelude::*;
54use std::path::{Path, PathBuf};
55
56use helpers::{escape_string, parse_cargo_toml};
57
58/// Version info field names
59#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
60pub enum VersionInfo {
61    /// The version value consists of four 16 bit words, e.g.,
62    /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE`
63    FILEVERSION,
64    /// The version value consists of four 16 bit words, e.g.,
65    /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE`
66    PRODUCTVERSION,
67    /// Should be Windows NT Win32, with value `0x40004`
68    FILEOS,
69    /// The value (for a rust compiler output) should be
70    /// 1 for a EXE and 2 for a DLL
71    FILETYPE,
72    /// Only for Windows drivers
73    FILESUBTYPE,
74    /// Bit mask for FILEFLAGS
75    FILEFLAGSMASK,
76    /// Only the bits set in FILEFLAGSMASK are read
77    FILEFLAGS,
78}
79
80#[derive(Debug)]
81struct Icon {
82    path: String,
83    name_id: String,
84}
85
86#[derive(Debug)]
87pub struct WindowsResource {
88    properties: BTreeMap<String, String>,
89    version_info: BTreeMap<VersionInfo, u64>,
90    rc_file: Option<String>,
91    icons: Vec<Icon>,
92    language: u16,
93    manifest: Option<String>,
94    manifest_file: Option<String>,
95    append_rc_content: String,
96}
97
98#[allow(clippy::new_without_default)]
99impl WindowsResource {
100    /// Create a new resource with version info struct
101    ///
102    ///
103    /// We initialize the resource file with values provided by cargo
104    ///
105    /// | Field                | Cargo / Values               |
106    /// |----------------------|------------------------------|
107    /// | `"FileVersion"`      | `package.version`            |
108    /// | `"ProductVersion"`   | `package.version`            |
109    /// | `"ProductName"`      | `package.name`               |
110    /// | `"FileDescription"`  | `package.description`        |
111    ///
112    /// Furthermore if a section `package.metadata.tauri-winres` exists
113    /// in `Cargo.toml` it will be parsed. Values in this section take precedence
114    /// over the values provided natively by cargo. Only the string table
115    /// of the version struct can be set this way.
116    /// Additionally, the language field is set to neutral (i.e. `0`)
117    /// and no icon is set. These settings have to be done programmatically.
118    ///
119    /// `Cargo.toml` files have to be written in UTF-8, so we support all valid UTF-8 strings
120    /// provided.
121    ///
122    /// ```,toml
123    /// #Cargo.toml
124    /// [package.metadata.tauri-winres]
125    /// OriginalFilename = "testing.exe"
126    /// FileDescription = "⛄❤☕"
127    /// LegalCopyright = "Copyright © 2016"
128    /// ```
129    ///
130    /// The version info struct is set to some values
131    /// sensible for creating an executable file.
132    ///
133    /// | Property             | Cargo / Values               |
134    /// |----------------------|------------------------------|
135    /// | `FILEVERSION`        | `package.version`            |
136    /// | `PRODUCTVERSION`     | `package.version`            |
137    /// | `FILEOS`             | `VOS_NT_WINDOWS32 (0x40004)` |
138    /// | `FILETYPE`           | `VFT_APP (0x1)`              |
139    /// | `FILESUBTYPE`        | `VFT2_UNKNOWN (0x0)`         |
140    /// | `FILEFLAGSMASK`      | `VS_FFI_FILEFLAGSMASK (0x3F)`|
141    /// | `FILEFLAGS`          | `0x0`                        |
142    ///
143    pub fn new() -> Self {
144        let mut props: BTreeMap<String, String> = BTreeMap::new();
145        let mut ver: BTreeMap<VersionInfo, u64> = BTreeMap::new();
146
147        props.insert(
148            "FileVersion".to_string(),
149            env::var("CARGO_PKG_VERSION").unwrap(),
150        );
151        props.insert(
152            "ProductVersion".to_string(),
153            env::var("CARGO_PKG_VERSION").unwrap(),
154        );
155        props.insert(
156            "ProductName".to_string(),
157            env::var("CARGO_PKG_NAME").unwrap(),
158        );
159        // If there is no description, fallback to name
160        let description = if let Ok(description) = env::var("CARGO_PKG_DESCRIPTION") {
161            if !description.is_empty() {
162                description
163            } else {
164                env::var("CARGO_PKG_NAME").unwrap()
165            }
166        } else {
167            env::var("CARGO_PKG_NAME").unwrap()
168        };
169        props.insert("FileDescription".to_string(), description);
170
171        parse_cargo_toml(&mut props).unwrap();
172
173        let mut version = 0_u64;
174        version |= env::var("CARGO_PKG_VERSION_MAJOR")
175            .unwrap()
176            .parse()
177            .unwrap_or(0)
178            << 48;
179        version |= env::var("CARGO_PKG_VERSION_MINOR")
180            .unwrap()
181            .parse()
182            .unwrap_or(0)
183            << 32;
184        version |= env::var("CARGO_PKG_VERSION_PATCH")
185            .unwrap()
186            .parse()
187            .unwrap_or(0)
188            << 16;
189        // version |= env::var("CARGO_PKG_VERSION_PRE").unwrap().parse().unwrap_or(0);
190        ver.insert(VersionInfo::FILEVERSION, version);
191        ver.insert(VersionInfo::PRODUCTVERSION, version);
192        ver.insert(VersionInfo::FILEOS, 0x00040004);
193        ver.insert(VersionInfo::FILETYPE, 1);
194        ver.insert(VersionInfo::FILESUBTYPE, 0);
195        ver.insert(VersionInfo::FILEFLAGSMASK, 0x3F);
196        ver.insert(VersionInfo::FILEFLAGS, 0);
197
198        WindowsResource {
199            properties: props,
200            version_info: ver,
201            rc_file: None,
202            icons: Vec::new(),
203            language: 0,
204            manifest: None,
205            manifest_file: None,
206            append_rc_content: String::new(),
207        }
208    }
209
210    /// Set string properties of the version info struct.
211    ///
212    /// Possible field names are:
213    ///
214    ///  - `"FileVersion"`
215    ///  - `"FileDescription"`
216    ///  - `"ProductVersion"`
217    ///  - `"ProductName"`
218    ///  - `"OriginalFilename"`
219    ///  - `"LegalCopyright"`
220    ///  - `"LegalTrademark"`
221    ///  - `"CompanyName"`
222    ///  - `"Comments"`
223    ///  - `"InternalName"`
224    ///
225    /// Additionally there exists
226    /// `"PrivateBuild"`, `"SpecialBuild"`
227    /// which should only be set, when the `FILEFLAGS` property is set to
228    /// `VS_FF_PRIVATEBUILD(0x08)` or `VS_FF_SPECIALBUILD(0x20)`
229    ///
230    /// It is possible to use arbirtrary field names but Windows Explorer and other
231    /// tools might not show them.
232    pub fn set<'a>(&mut self, name: &'a str, value: &'a str) -> &mut Self {
233        self.properties.insert(name.to_string(), value.to_string());
234        self
235    }
236
237    /// Set the user interface language of the file
238    ///
239    /// # Example
240    ///
241    /// ```rust,compile_fail
242    /// # use std::io;
243    /// fn main() {
244    ///   if cfg!(target_os = "windows") {
245    ///     let mut res = tauri_winres::WindowsResource::new();
246    /// #   res.set_output_directory(".");
247    ///     res.set_language(winapi::um::winnt::MAKELANGID(
248    ///         winapi::um::winnt::LANG_ENGLISH,
249    ///         winapi::um::winnt::SUBLANG_ENGLISH_US
250    ///     ));
251    ///     res.compile().unwrap();
252    ///   }
253    /// }
254    /// ```
255    /// For possible values look at the `winapi::um::winnt` constants, specifically those
256    /// starting with `LANG_` and `SUBLANG_`.
257    ///
258    /// [`MAKELANGID`]: https://docs.rs/winapi/0.3/x86_64-pc-windows-msvc/winapi/um/winnt/fn.MAKELANGID.html
259    /// [`winapi::um::winnt`]: https://docs.rs/winapi/0.3/x86_64-pc-windows-msvc/winapi/um/winnt/index.html#constants
260    ///
261    /// # Table
262    /// Sometimes it is just simpler to specify the numeric constant directly
263    /// (That is what most `.rc` files do).
264    /// For possible values take a look at the MSDN page for resource files;
265    /// we only listed some values here.
266    ///
267    /// | Language            | Value    |
268    /// |---------------------|----------|
269    /// | Neutral             | `0x0000` |
270    /// | English             | `0x0009` |
271    /// | English (US)        | `0x0409` |
272    /// | English (GB)        | `0x0809` |
273    /// | German              | `0x0407` |
274    /// | German (AT)         | `0x0c07` |
275    /// | French              | `0x000c` |
276    /// | French (FR)         | `0x040c` |
277    /// | Catalan             | `0x0003` |
278    /// | Basque              | `0x042d` |
279    /// | Breton              | `0x007e` |
280    /// | Scottish Gaelic     | `0x0091` |
281    /// | Romansch            | `0x0017` |
282    pub fn set_language(&mut self, language: u16) -> &mut Self {
283        self.language = language;
284        self
285    }
286
287    /// Add an icon with nameID `32512`.
288    ///
289    /// This icon needs to be in `ico` format. The filename can be absolute
290    /// or relative to the projects root.
291    ///
292    /// Equivalent to `set_icon_with_id(path, "32512")`.
293    ///
294    /// The reason for `32512` is that we misunderstood `IDI_APPLICATION` (`MAKEINTRESOURCE(32512)`)
295    /// should be used for the application icon, which is not true.
296    /// See <https://github.com/mxre/winres/issues/28>
297    ///
298    /// Quote from [Old New Things](https://devblogs.microsoft.com/oldnewthing/20250423-00/?p=111106)
299    ///
300    /// > Recall the algorithm by which Explorer finds the “first” icon in a file.
301    /// >
302    /// > - Choose the alphabetically first named group icon, if available.
303    /// > - Else, choose the group icon with the numerically lowest identifier.
304    pub fn set_icon(&mut self, path: &str) -> &mut Self {
305        self.set_icon_with_id(path, "32512")
306    }
307
308    /// Add an icon with the specified name ID.
309    ///
310    /// This icon need to be in `ico` format. The path can be absolute or
311    /// relative to the projects root.
312    ///
313    /// ## Name ID and Icon Loading
314    ///
315    /// The name ID can be (the string representation of) a 16-bit unsigned
316    /// integer, or some other string.
317    ///
318    /// You should not add multiple icons with the same name ID. It will result
319    /// in a build failure.
320    ///
321    /// When the name ID is an integer, the icon can be loaded at runtime with
322    ///
323    /// ```ignore
324    /// LoadIconW(h_instance, MAKEINTRESOURCEW(name_id_as_integer))
325    /// ```
326    ///
327    /// Otherwise, it can be loaded with
328    ///
329    /// ```ignore
330    /// LoadIconW(h_instance, name_id_as_wide_c_str_as_ptr)
331    /// ```
332    ///
333    /// Where `h_instance` is the module handle of the current executable
334    /// ([`GetModuleHandleW`](https://docs.rs/winapi/0.3.8/winapi/um/libloaderapi/fn.GetModuleHandleW.html)`(null())`),
335    /// [`LoadIconW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.LoadIconW.html)
336    /// and
337    /// [`MAKEINTRESOURCEW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.MAKEINTRESOURCEW.html)
338    /// are defined in winapi.
339    ///
340    /// ## Multiple Icons, Which One is Application Icon?
341    ///
342    /// When you have multiple icons, it's a bit complicated which one will be
343    /// chosen as the application icon:
344    /// <https://docs.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)?redirectedfrom=MSDN#choosing-an-icon>.
345    ///
346    /// To keep things simple, we recommand you use only 16-bit unsigned integer
347    /// name IDs, and add the application icon first with the lowest id:
348    ///
349    /// ```nocheck
350    /// res.set_icon("icon.ico") // This is application icon.
351    ///    .set_icon_with_id("icon2.icon", "2")
352    ///    .set_icon_with_id("icon3.icon", "3")
353    ///    // ...
354    /// ```
355    pub fn set_icon_with_id<'a>(&mut self, path: &'a str, name_id: &'a str) -> &mut Self {
356        self.icons.push(Icon {
357            path: dunce::canonicalize(path)
358                .map(|p| p.to_string_lossy().to_string())
359                .unwrap_or(path.to_string()),
360            name_id: name_id.into(),
361        });
362        self
363    }
364
365    /// Set a version info struct property
366    /// Currently we only support numeric values; you have to look them up.
367    pub fn set_version_info(&mut self, field: VersionInfo, value: u64) -> &mut Self {
368        self.version_info.insert(field, value);
369        self
370    }
371
372    /// Set the embedded manifest file
373    ///
374    /// # Example
375    ///
376    /// The following manifest will brand the exe as requesting administrator privileges.
377    /// Thus, everytime it is executed, a Windows UAC dialog will appear.
378    ///
379    /// ```rust
380    /// let mut res = tauri_winres::WindowsResource::new();
381    /// res.set_manifest(r#"
382    /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
383    /// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
384    ///     <security>
385    ///         <requestedPrivileges>
386    ///             <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
387    ///         </requestedPrivileges>
388    ///     </security>
389    /// </trustInfo>
390    /// </assembly>
391    /// "#);
392    /// ```
393    pub fn set_manifest(&mut self, manifest: &str) -> &mut Self {
394        self.manifest_file = None;
395        self.manifest = Some(manifest.to_string());
396        self
397    }
398
399    /// Some as [`set_manifest()`] but a filename can be provided and
400    /// file is included by the resource compieler itself.
401    /// This method works the same way as [`set_icon()`]
402    ///
403    /// [`set_manifest()`]: #method.set_manifest
404    /// [`set_icon()`]: #method.set_icon
405    pub fn set_manifest_file(&mut self, file: &str) -> &mut Self {
406        self.manifest_file = Some(file.to_string());
407        self.manifest = None;
408        self
409    }
410
411    /// Write a resource file with the set values
412    pub fn write_resource_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
413        let mut f = fs::File::create(path)?;
414
415        // use UTF8 as an encoding
416        // this makes it easier since in rust all string are UTF8
417        writeln!(f, "#pragma code_page(65001)")?;
418        writeln!(f, "1 VERSIONINFO")?;
419        for (k, v) in self.version_info.iter() {
420            match *k {
421                VersionInfo::FILEVERSION | VersionInfo::PRODUCTVERSION => writeln!(
422                    f,
423                    "{:?} {}, {}, {}, {}",
424                    k,
425                    (*v >> 48) as u16,
426                    (*v >> 32) as u16,
427                    (*v >> 16) as u16,
428                    *v as u16
429                )?,
430                _ => writeln!(f, "{:?} {:#x}", k, v)?,
431            };
432        }
433        writeln!(f, "{{\nBLOCK \"StringFileInfo\"")?;
434        writeln!(f, "{{\nBLOCK \"{:04x}04b0\"\n{{", self.language)?;
435        for (k, v) in self.properties.iter() {
436            if !v.is_empty() {
437                writeln!(
438                    f,
439                    "VALUE \"{}\", \"{}\"",
440                    escape_string(k),
441                    escape_string(v)
442                )?;
443            }
444        }
445        writeln!(f, "}}\n}}")?;
446
447        writeln!(f, "BLOCK \"VarFileInfo\" {{")?;
448        writeln!(f, "VALUE \"Translation\", {:#x}, 0x04b0", self.language)?;
449        writeln!(f, "}}\n}}")?;
450        for icon in &self.icons {
451            writeln!(
452                f,
453                "{} ICON \"{}\"",
454                escape_string(&icon.name_id),
455                escape_string(&icon.path)
456            )?;
457        }
458        if let Some(e) = self.version_info.get(&VersionInfo::FILETYPE) {
459            if let Some(manf) = self.manifest.as_ref() {
460                writeln!(f, "{} 24", e)?;
461                writeln!(f, "{{")?;
462                for line in manf.lines() {
463                    writeln!(f, "\" {} \"", escape_string(line.trim()))?;
464                }
465                writeln!(f, "}}")?;
466            } else if let Some(manf) = self.manifest_file.as_ref() {
467                writeln!(f, "{} 24 \"{}\"", e, escape_string(manf))?;
468            }
469        }
470        writeln!(f, "{}", self.append_rc_content)?;
471        Ok(())
472    }
473
474    /// Set a path to an already existing resource file.
475    ///
476    /// We will neither modify this file nor parse its contents. This function
477    /// simply replaces the internaly generated resource file that is passed to
478    /// the compiler. You can use this function to write a resource file yourself.
479    pub fn set_resource_file(&mut self, path: &str) -> &mut Self {
480        self.rc_file = Some(path.to_string());
481        self
482    }
483
484    /// Append an additional snippet to the generated rc file.
485    ///
486    /// # Example
487    ///
488    /// Define a menu resource:
489    ///
490    /// ```rust,no_run
491    /// # if cfg!(target_os = "windows") {
492    ///     let mut res = tauri_winres::WindowsResource::new();
493    ///     res.append_rc_content(r##"sample MENU
494    /// {
495    ///     MENUITEM "&Soup", 100
496    ///     MENUITEM "S&alad", 101
497    ///     POPUP "&Entree"
498    ///     {
499    ///          MENUITEM "&Fish", 200
500    ///          MENUITEM "&Chicken", 201, CHECKED
501    ///          POPUP "&Beef"
502    ///          {
503    ///               MENUITEM "&Steak", 301
504    ///               MENUITEM "&Prime Rib", 302
505    ///          }
506    ///     }
507    ///     MENUITEM "&Dessert", 103
508    /// }"##);
509    /// #    res.compile()?;
510    /// # }
511    /// # Ok::<_, std::io::Error>(())
512    /// ```
513    pub fn append_rc_content(&mut self, content: &str) -> &mut Self {
514        if !(self.append_rc_content.ends_with('\n') || self.append_rc_content.is_empty()) {
515            self.append_rc_content.push('\n');
516        }
517        self.append_rc_content.push_str(content);
518        self
519    }
520
521    /// Run the resource compiler
522    ///
523    /// This function generates a resource file from the settings or
524    /// uses an existing resource file and passes it to the resource compiler
525    /// of your toolkit.
526    ///
527    /// Further more we will print the correct statements for
528    /// `cargo:rustc-link-lib=` and `cargo:rustc-link-search` on the console,
529    /// so that the cargo build script can link the compiled resource file.
530    pub fn compile(&self) -> io::Result<()> {
531        let output = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string()));
532        let rc = output.join("resource.rc");
533
534        if let Some(s) = self.rc_file.as_ref() {
535            fs::write(&rc, s)?;
536        } else {
537            self.write_resource_file(&rc)?;
538        }
539
540        // This matches v2 behavior
541        embed_resource::compile(rc, embed_resource::NONE)
542            .manifest_required()
543            .unwrap();
544
545        Ok(())
546    }
547
548    /// Run the resource compiler
549    ///
550    /// This function generates a resource file from the settings or
551    /// uses an existing resource file and passes it to the resource compiler
552    /// of your toolkit.
553    ///
554    /// Further more we will print the correct statements for
555    /// `cargo:rustc-link-lib=` and `cargo:rustc-link-search` on the console,
556    /// so that the cargo build script can link the compiled resource file.
557    pub fn compile_for(&self, binaries: &[&str]) -> io::Result<()> {
558        let output = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string()));
559        let rc = output.join("resource.rc");
560
561        if let Some(s) = self.rc_file.as_ref() {
562            fs::write(&rc, s)?;
563        } else {
564            self.write_resource_file(&rc)?;
565        }
566
567        // This matches v2 behavior
568        embed_resource::compile_for(rc, binaries, embed_resource::NONE)
569            .manifest_required()
570            .unwrap();
571
572        Ok(())
573    }
574}
575
576// Deprecated functions
577impl WindowsResource {
578    #[deprecated(
579        since = "0.1.1",
580        note = "This function is no-op! It is now handled by the embed-resource crate."
581    )]
582    pub fn set_toolkit_path(&mut self, _path: &str) -> &mut Self {
583        self
584    }
585
586    #[deprecated(
587        since = "0.1.1",
588        note = "This function is no-op! It is now handled by the embed-resource crate."
589    )]
590    pub fn set_windres_path(&mut self, _path: &str) -> &mut Self {
591        self
592    }
593
594    #[deprecated(
595        since = "0.1.1",
596        note = "This function is no-op! It is now handled by the embed-resource crate."
597    )]
598    pub fn set_ar_path(&mut self, _path: &str) -> &mut Self {
599        self
600    }
601
602    #[deprecated(
603        since = "0.1.1",
604        note = "This function is no-op! It is now handled by the embed-resource crate."
605    )]
606    pub fn add_toolkit_include(&mut self, _add: bool) -> &mut Self {
607        self
608    }
609
610    #[deprecated(
611        since = "0.1.1",
612        note = "This function is no-op! It is now handled by the embed-resource crate."
613    )]
614    pub fn set_output_directory(&mut self, _path: &str) -> &mut Self {
615        self
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::helpers::escape_string;
622
623    #[test]
624    fn string_escaping() {
625        assert_eq!(&escape_string(""), "");
626        assert_eq!(&escape_string("foo"), "foo");
627        assert_eq!(&escape_string(r#""Hello""#), r#"""Hello"""#);
628        assert_eq!(
629            &escape_string(r"C:\Program Files\Foobar"),
630            r"C:\\Program Files\\Foobar"
631        );
632    }
633}