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}