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}