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::HashMap;
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)]
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: HashMap<String, String>,
89 version_info: HashMap<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: HashMap<String, String> = HashMap::new();
145 let mut ver: HashMap<VersionInfo, u64> = HashMap::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 /// ```
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 /// Windows uses `32512` as the default icon ID. See
295 /// [here](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadicona)
296 /// for Windows docs demonstrating this.
297 pub fn set_icon(&mut self, path: &str) -> &mut Self {
298 self.set_icon_with_id(path, "32512")
299 }
300
301 /// Add an icon with the specified name ID.
302 ///
303 /// This icon need to be in `ico` format. The path can be absolute or
304 /// relative to the projects root.
305 ///
306 /// ## Name ID and Icon Loading
307 ///
308 /// The name ID can be (the string representation of) a 16-bit unsigned
309 /// integer, or some other string.
310 ///
311 /// You should not add multiple icons with the same name ID. It will result
312 /// in a build failure.
313 ///
314 /// When the name ID is an integer, the icon can be loaded at runtime with
315 ///
316 /// ```ignore
317 /// LoadIconW(h_instance, MAKEINTRESOURCEW(name_id_as_integer))
318 /// ```
319 ///
320 /// Otherwise, it can be loaded with
321 ///
322 /// ```ignore
323 /// LoadIconW(h_instance, name_id_as_wide_c_str_as_ptr)
324 /// ```
325 ///
326 /// Where `h_instance` is the module handle of the current executable
327 /// ([`GetModuleHandleW`](https://docs.rs/winapi/0.3.8/winapi/um/libloaderapi/fn.GetModuleHandleW.html)`(null())`),
328 /// [`LoadIconW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.LoadIconW.html)
329 /// and
330 /// [`MAKEINTRESOURCEW`](https://docs.rs/winapi/0.3.8/winapi/um/winuser/fn.MAKEINTRESOURCEW.html)
331 /// are defined in winapi.
332 ///
333 /// ## Multiple Icons, Which One is Application Icon?
334 ///
335 /// When you have multiple icons, it's a bit complicated which one will be
336 /// chosen as the application icon:
337 /// <https://docs.microsoft.com/en-us/previous-versions/ms997538(v=msdn.10)?redirectedfrom=MSDN#choosing-an-icon>.
338 ///
339 /// To keep things simple, we recommand you use only 16-bit unsigned integer
340 /// name IDs, and add the application icon first with the lowest id:
341 ///
342 /// ```nocheck
343 /// res.set_icon("icon.ico") // This is application icon.
344 /// .set_icon_with_id("icon2.icon", "2")
345 /// .set_icon_with_id("icon3.icon", "3")
346 /// // ...
347 /// ```
348 pub fn set_icon_with_id<'a>(&mut self, path: &'a str, name_id: &'a str) -> &mut Self {
349 self.icons.push(Icon {
350 path: PathBuf::from(path)
351 .canonicalize()
352 .map(|p| p.to_string_lossy().to_string())
353 .unwrap_or(path.to_string()),
354 name_id: name_id.into(),
355 });
356 self
357 }
358
359 /// Set a version info struct property
360 /// Currently we only support numeric values; you have to look them up.
361 pub fn set_version_info(&mut self, field: VersionInfo, value: u64) -> &mut Self {
362 self.version_info.insert(field, value);
363 self
364 }
365
366 /// Set the embedded manifest file
367 ///
368 /// # Example
369 ///
370 /// The following manifest will brand the exe as requesting administrator privileges.
371 /// Thus, everytime it is executed, a Windows UAC dialog will appear.
372 ///
373 /// ```rust
374 /// let mut res = tauri_winres::WindowsResource::new();
375 /// res.set_manifest(r#"
376 /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
377 /// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
378 /// <security>
379 /// <requestedPrivileges>
380 /// <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
381 /// </requestedPrivileges>
382 /// </security>
383 /// </trustInfo>
384 /// </assembly>
385 /// "#);
386 /// ```
387 pub fn set_manifest(&mut self, manifest: &str) -> &mut Self {
388 self.manifest_file = None;
389 self.manifest = Some(manifest.to_string());
390 self
391 }
392
393 /// Some as [`set_manifest()`] but a filename can be provided and
394 /// file is included by the resource compieler itself.
395 /// This method works the same way as [`set_icon()`]
396 ///
397 /// [`set_manifest()`]: #method.set_manifest
398 /// [`set_icon()`]: #method.set_icon
399 pub fn set_manifest_file(&mut self, file: &str) -> &mut Self {
400 self.manifest_file = Some(file.to_string());
401 self.manifest = None;
402 self
403 }
404
405 /// Write a resource file with the set values
406 pub fn write_resource_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
407 let mut f = fs::File::create(path)?;
408
409 // use UTF8 as an encoding
410 // this makes it easier since in rust all string are UTF8
411 writeln!(f, "#pragma code_page(65001)")?;
412 writeln!(f, "1 VERSIONINFO")?;
413 for (k, v) in self.version_info.iter() {
414 match *k {
415 VersionInfo::FILEVERSION | VersionInfo::PRODUCTVERSION => writeln!(
416 f,
417 "{:?} {}, {}, {}, {}",
418 k,
419 (*v >> 48) as u16,
420 (*v >> 32) as u16,
421 (*v >> 16) as u16,
422 *v as u16
423 )?,
424 _ => writeln!(f, "{:?} {:#x}", k, v)?,
425 };
426 }
427 writeln!(f, "{{\nBLOCK \"StringFileInfo\"")?;
428 writeln!(f, "{{\nBLOCK \"{:04x}04b0\"\n{{", self.language)?;
429 for (k, v) in self.properties.iter() {
430 if !v.is_empty() {
431 writeln!(
432 f,
433 "VALUE \"{}\", \"{}\"",
434 escape_string(k),
435 escape_string(v)
436 )?;
437 }
438 }
439 writeln!(f, "}}\n}}")?;
440
441 writeln!(f, "BLOCK \"VarFileInfo\" {{")?;
442 writeln!(f, "VALUE \"Translation\", {:#x}, 0x04b0", self.language)?;
443 writeln!(f, "}}\n}}")?;
444 for icon in &self.icons {
445 writeln!(
446 f,
447 "{} ICON \"{}\"",
448 escape_string(&icon.name_id),
449 escape_string(&icon.path)
450 )?;
451 }
452 if let Some(e) = self.version_info.get(&VersionInfo::FILETYPE) {
453 if let Some(manf) = self.manifest.as_ref() {
454 writeln!(f, "{} 24", e)?;
455 writeln!(f, "{{")?;
456 for line in manf.lines() {
457 writeln!(f, "\" {} \"", escape_string(line.trim()))?;
458 }
459 writeln!(f, "}}")?;
460 } else if let Some(manf) = self.manifest_file.as_ref() {
461 writeln!(f, "{} 24 \"{}\"", e, escape_string(manf))?;
462 }
463 }
464 writeln!(f, "{}", self.append_rc_content)?;
465 Ok(())
466 }
467
468 /// Set a path to an already existing resource file.
469 ///
470 /// We will neither modify this file nor parse its contents. This function
471 /// simply replaces the internaly generated resource file that is passed to
472 /// the compiler. You can use this function to write a resource file yourself.
473 pub fn set_resource_file(&mut self, path: &str) -> &mut Self {
474 self.rc_file = Some(path.to_string());
475 self
476 }
477
478 /// Append an additional snippet to the generated rc file.
479 ///
480 /// # Example
481 ///
482 /// Define a menu resource:
483 ///
484 /// ```rust
485 /// # if cfg!(target_os = "windows") {
486 /// let mut res = tauri_winres::WindowsResource::new();
487 /// res.append_rc_content(r##"sample MENU
488 /// {
489 /// MENUITEM "&Soup", 100
490 /// MENUITEM "S&alad", 101
491 /// POPUP "&Entree"
492 /// {
493 /// MENUITEM "&Fish", 200
494 /// MENUITEM "&Chicken", 201, CHECKED
495 /// POPUP "&Beef"
496 /// {
497 /// MENUITEM "&Steak", 301
498 /// MENUITEM "&Prime Rib", 302
499 /// }
500 /// }
501 /// MENUITEM "&Dessert", 103
502 /// }"##);
503 /// # res.compile()?;
504 /// # }
505 /// # Ok::<_, std::io::Error>(())
506 /// ```
507 pub fn append_rc_content(&mut self, content: &str) -> &mut Self {
508 if !(self.append_rc_content.ends_with('\n') || self.append_rc_content.is_empty()) {
509 self.append_rc_content.push('\n');
510 }
511 self.append_rc_content.push_str(content);
512 self
513 }
514
515 /// Run the resource compiler
516 ///
517 /// This function generates a resource file from the settings or
518 /// uses an existing resource file and passes it to the resource compiler
519 /// of your toolkit.
520 ///
521 /// Further more we will print the correct statements for
522 /// `cargo:rustc-link-lib=` and `cargo:rustc-link-search` on the console,
523 /// so that the cargo build script can link the compiled resource file.
524 pub fn compile(&self) -> io::Result<()> {
525 let output = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string()));
526 let rc = output.join("resource.rc");
527
528 if let Some(s) = self.rc_file.as_ref() {
529 fs::write(&rc, s)?;
530 } else {
531 self.write_resource_file(&rc)?;
532 }
533
534 // This matches v2 behavior
535 embed_resource::compile(rc, embed_resource::NONE)
536 .manifest_required()
537 .unwrap();
538
539 Ok(())
540 }
541
542 /// Run the resource compiler
543 ///
544 /// This function generates a resource file from the settings or
545 /// uses an existing resource file and passes it to the resource compiler
546 /// of your toolkit.
547 ///
548 /// Further more we will print the correct statements for
549 /// `cargo:rustc-link-lib=` and `cargo:rustc-link-search` on the console,
550 /// so that the cargo build script can link the compiled resource file.
551 pub fn compile_for(&self, binaries: &[&str]) -> io::Result<()> {
552 let output = PathBuf::from(env::var("OUT_DIR").unwrap_or_else(|_| ".".to_string()));
553 let rc = output.join("resource.rc");
554
555 if let Some(s) = self.rc_file.as_ref() {
556 fs::write(&output, s)?;
557 } else {
558 self.write_resource_file(rc)?;
559 }
560
561 // This matches v2 behavior
562 embed_resource::compile_for("resource.rc", binaries, embed_resource::NONE)
563 .manifest_required()
564 .unwrap();
565
566 Ok(())
567 }
568}
569
570// Deprecated functions
571impl WindowsResource {
572 #[deprecated(
573 since = "0.1.1",
574 note = "This function is no-op! It is now handled by the embed-resource crate."
575 )]
576 pub fn set_toolkit_path(&mut self, _path: &str) -> &mut Self {
577 self
578 }
579
580 #[deprecated(
581 since = "0.1.1",
582 note = "This function is no-op! It is now handled by the embed-resource crate."
583 )]
584 pub fn set_windres_path(&mut self, _path: &str) -> &mut Self {
585 self
586 }
587
588 #[deprecated(
589 since = "0.1.1",
590 note = "This function is no-op! It is now handled by the embed-resource crate."
591 )]
592 pub fn set_ar_path(&mut self, _path: &str) -> &mut Self {
593 self
594 }
595
596 #[deprecated(
597 since = "0.1.1",
598 note = "This function is no-op! It is now handled by the embed-resource crate."
599 )]
600 pub fn add_toolkit_include(&mut self, _add: bool) -> &mut Self {
601 self
602 }
603
604 #[deprecated(
605 since = "0.1.1",
606 note = "This function is no-op! It is now handled by the embed-resource crate."
607 )]
608 pub fn set_output_directory(&mut self, _path: &str) -> &mut Self {
609 self
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::helpers::escape_string;
616
617 #[test]
618 fn string_escaping() {
619 assert_eq!(&escape_string(""), "");
620 assert_eq!(&escape_string("foo"), "foo");
621 assert_eq!(&escape_string(r#""Hello""#), r#"""Hello"""#);
622 assert_eq!(
623 &escape_string(r"C:\Program Files\Foobar"),
624 r"C:\\Program Files\\Foobar"
625 );
626 }
627}