embed_resource/
lib.rs

1//! A [`Cargo` build script](http://doc.crates.io/build-script.html) library to handle compilation and inclusion of Windows
2//! resources in the most resilient fashion imaginable
3//!
4//! # Background
5//!
6//! Including Windows resources seems very easy at first, despite the build scripts' abhorrent documentation:
7//! [compile with `windres`, then make linkable with
8//! `ar`](https://github.com/nabijaczleweli/cargo-update/commit/ef4346c#diff-a7b0a2dee0126cddf994326e705a91ea).
9//!
10//! I was very happy with that solution until it was brought to my attention, that [MSVC uses something
11//! different](https://github.com/nabijaczleweli/cargo-update/commit/f57e9c3#diff-a7b0a2dee0126cddf994326e705a91ea),
12//! and now either `windres`-`ar` combo or `RC.EXE` would be used, which was OK.
13//!
14//! Later it transpired, that [MSVC is even more incompatible with everything
15//! else](https://github.com/nabijaczleweli/cargo-update/commit/39fa758#diff-a7b0a2dee0126cddf994326e705a91ea)
16//! by way of not having `RC.EXE` in `$PATH` (because it would only be reasonable to do so),
17//! so another MSVC artisan made the script [find the most likely places for `RC.EXE` to
18//! be](https://github.com/nabijaczleweli/cargo-update/pull/22), and the script grew yet again,
19//! now standing at 100 lines and 3.2 kB.
20//!
21//! After [copying the build script in its
22//! entirety](https://github.com/thecoshman/http/commit/98205a4#diff-a7b0a2dee0126cddf994326e705a91ea)
23//! and realising how error-prone that was, then being [nudged by
24//! Shepmaster](https://chat.stackoverflow.com/transcript/message/35378953#35378953)
25//! to extract it to a crate, here we are.
26//!
27//! # Usage
28//!
29//! For the purposes of the demonstration we will assume that the resource file's name
30//! is "checksums.rc", but it can be any name relative to the crate root.
31//!
32//! In `Cargo.toml`:
33//!
34//! ```toml
35//! # The general section with crate name, license, etc.
36//! build = "build.rs"
37//!
38//! [build-dependencies]
39//! embed-resource = "3.0"
40//! ```
41//!
42//! In `build.rs`:
43//!
44//! ```rust,no_run
45//! extern crate embed_resource;
46//!
47//! fn main() {
48//!     embed_resource::compile("checksums.rc", embed_resource::NONE).manifest_optional().unwrap();
49//!     // or
50//!     embed_resource::compile("checksums.rc", &["VERSION=000901"]).manifest_required().unwrap();
51//! }
52//! ```
53//!
54//! Use `.manifest_optional().unwrap()` if the manifest is cosmetic (like an icon).<br />
55//! Use `.manifest_required().unwrap()` if the manifest is required (security, entry point, &c.).
56//!
57//! ## Errata
58//!
59//! If no `cargo:rerun-if-changed` annotations are generated, Cargo scans the entire build root by default.
60//! Because the first step in building a manifest is an unspecified C preprocessor step with-out the ability to generate the
61//! equivalent of `cc -MD`, we do *not* output said annotation.
62//!
63//! If scanning is prohibitively expensive, or you have something else that generates the annotations, you may want to spec the
64//! full non-system dependency list for your manifest manually, so:
65//! ```rust,no_run
66//! println!("cargo:rerun-if-changed=app-name-manifest.rc");
67//! embed_resource::compile("app-name-manifest.rc", embed_resource::NONE);
68//! ```
69//! for the above example (cf, [#41](https://github.com/nabijaczleweli/rust-embed-resource/issues/41)).
70//!
71//! # Cross-compilation
72//!
73//! It is possible to embed resources in Windows executables built on non-Windows hosts. There are two ways to do this:
74//!
75//! When targetting `*-pc-windows-gnu`, `*-w64-mingw32-windres` is attempted by default, for `*-pc-windows-msvc` it's `llvm-rc`,
76//! this can be overriden by setting `RC_$TARGET`, `RC_${TARGET//-/_}`, or `RC` environment variables.
77//!
78//! When compiling with LLVM-RC, an external C compiler is used to preprocess the resource,
79//! preloaded with configuration from
80//! [`cc`](https://github.com/alexcrichton/cc-rs#external-configuration-via-environment-variables).
81//!
82//! ## Migration
83//! ### 2.x
84//!
85//! Add `embed_resource::NONE` as the last argument to `embed_resource::compile()` and  `embed_resource::compile_for()`.
86//!
87//! ### 3.x
88//!
89//! Add `.manifest_optional().unwrap()` or `.manifest_required().unwrap()` to all [`compile()`] and `compile_for*()` calls.
90//! `CompilationResult` is `#[must_use]` so should be highlighted automatically.
91//!
92//! Embed-resource <3.x always behaves like `.manifest_optional().unwrap()`.
93//!
94//! # Credit
95//!
96//! In chronological order:
97//!
98//! [@liigo](https://github.com/liigo) -- persistency in pestering me and investigating problems where I have failed
99//!
100//! [@mzji](https://github.com/mzji) -- MSVC lab rat
101//!
102//! [@TheCatPlusPlus](https://github.com/TheCatPlusPlus) -- knowledge and providing first iteration of manifest-embedding code
103//!
104//! [@azyobuzin](https://github.com/azyobuzin) -- providing code for finding places where RC.EXE could hide
105//!
106//! [@retep998](https://github.com/retep998) -- fixing MSVC support
107//!
108//! [@SonnyX](https://github.com/SonnyX) -- Windows cross-compilation support and testing
109//!
110//! [@MSxDOS](https://github.com/MSxDOS) -- finding and supplying RC.EXE its esoteric header include paths
111//!
112//! [@roblabla](https://github.com/roblabla) -- cross-compilation to Windows MSVC via LLVM-RC
113//!
114//! # Special thanks
115//!
116//! To all who support further development on [Patreon](https://patreon.com/nabijaczleweli), in particular:
117//!
118//!   * ThePhD
119//!   * Embark Studios
120//!   * Lars Strojny
121//!   * EvModder
122
123
124#[cfg(any(not(target_os = "windows"), all(target_os = "windows", target_env = "msvc")))]
125extern crate cc;
126#[cfg(not(target_os = "windows"))]
127extern crate memchr;
128#[cfg(all(target_os = "windows", target_env = "msvc"))]
129extern crate vswhom;
130#[cfg(all(target_os = "windows", target_env = "msvc"))]
131extern crate winreg;
132extern crate rustc_version;
133extern crate toml;
134
135#[cfg(not(target_os = "windows"))]
136mod non_windows;
137#[cfg(all(target_os = "windows", target_env = "msvc"))]
138mod windows_msvc;
139#[cfg(all(target_os = "windows", not(target_env = "msvc")))]
140mod windows_not_msvc;
141
142#[cfg(not(target_os = "windows"))]
143use self::non_windows::*;
144#[cfg(all(target_os = "windows", target_env = "msvc"))]
145use self::windows_msvc::*;
146#[cfg(all(target_os = "windows", not(target_env = "msvc")))]
147use self::windows_not_msvc::*;
148
149use std::{env, fs};
150use std::ffi::OsStr;
151use std::borrow::Cow;
152use std::process::Command;
153use toml::Table as TomlTable;
154use std::fmt::{self, Display};
155use std::path::{Path, PathBuf};
156
157
158/// Empty slice, properly-typed for [`compile()`] and `compile_for*()`'s macro list.
159///
160/// Rust helpfully forbids default type parameters on functions, so just passing `[]` doesn't work :)
161pub const NONE: &[&OsStr] = &[];
162
163
164/// Result of [`compile()`] and `compile_for*()`
165///
166/// Turn this into a `Result` with `manifest_optional()` if the manifest is nice, but isn't required, like when embedding an
167/// icon or some other cosmetic.
168///
169/// Turn this into a `Result` with `manifest_required()` if the manifest is mandatory, like when configuring entry points or
170/// security.
171#[must_use]
172#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
173pub enum CompilationResult {
174    /// not building for windows
175    NotWindows,
176    /// built, linked
177    Ok,
178    /// building for windows, but the environment can't compile a resource (most likely due to a missing compiler)
179    NotAttempted(Cow<'static, str>),
180    /// environment can compile a resource, but has failed to do so
181    Failed(Cow<'static, str>),
182}
183impl CompilationResult {
184    /// `Ok(())` if `NotWindows`, `Ok`, or `NotAttempted`; `Err(self)` if `Failed`
185    pub fn manifest_optional(self) -> Result<(), CompilationResult> {
186        match self {
187            CompilationResult::NotWindows |
188            CompilationResult::Ok |
189            CompilationResult::NotAttempted(..) => Ok(()),
190            err @ CompilationResult::Failed(..) => Err(err),
191        }
192    }
193
194    /// `Ok(())` if `NotWindows`, `Ok`; `Err(self)` if `NotAttempted` or `Failed`
195    pub fn manifest_required(self) -> Result<(), CompilationResult> {
196        match self {
197            CompilationResult::NotWindows |
198            CompilationResult::Ok => Ok(()),
199            err @ CompilationResult::NotAttempted(..) |
200            err @ CompilationResult::Failed(..) => Err(err),
201        }
202    }
203}
204impl Display for CompilationResult {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
206        f.write_str("embed-resource: ")?;
207        match self {
208            CompilationResult::NotWindows => f.write_str("not building for windows"),
209            CompilationResult::Ok => f.write_str("OK"),
210            CompilationResult::NotAttempted(why) => {
211                f.write_str("compilation not attempted: ")?;
212                if !why.contains(' ') {
213                    f.write_str("missing compiler: ")?;
214                }
215                f.write_str(why)
216            }
217            CompilationResult::Failed(err) => f.write_str(err),
218        }
219    }
220}
221impl std::error::Error for CompilationResult {}
222
223macro_rules! try_compile_impl {
224    ($expr:expr) => {
225        match $expr {
226            Result::Ok(val) => val,
227            Result::Err(err) => return err,
228        }
229    };
230}
231
232
233/// Compile the Windows resource file and update the cargo search path if building for Windows.
234///
235/// On non-Windows non-Windows-cross-compile-target this does nothing, on non-MSVC Windows and Windows cross-compile targets,
236/// this chains `windres` with `ar`,
237/// but on MSVC Windows, this will try its hardest to find `RC.EXE` in Windows Kits and/or SDK directories,
238/// falling back to [Jon Blow's VS discovery script](https://pastebin.com/3YvWQa5c),
239/// and on Windows 10 `%INCLUDE%` will be updated to help `RC.EXE` find `windows.h` and friends.
240///
241/// `$OUT_DIR` is added to the include search path.
242///
243/// Note that this does *nothing* if building with rustc before 1.50.0 and there's a library in the crate,
244/// since the resource is linked to the library, if any, instead of the binaries.
245///
246/// Since rustc 1.50.0, the resource is linked only to the binaries
247/// (unless there are none, in which case it's also linked to the library).
248///
249/// `macros` are a list of macros to define, in standard `NAME`/`NAME=VALUE` format.
250///
251/// # Examples
252///
253/// In your build script, assuming the crate's name is "checksums":
254///
255/// ```rust,no_run
256/// extern crate embed_resource;
257///
258/// fn main() {
259///     // Compile and link checksums.rc
260///     embed_resource::compile("checksums.rc", embed_resource::NONE);
261/// }
262/// ```
263pub fn compile<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) -> CompilationResult {
264    let (prefix, out_dir, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
265    let hasbins = fs::read_to_string("Cargo.toml")
266        .unwrap_or_else(|err| {
267            eprintln!("Couldn't read Cargo.toml: {}; assuming src/main.rs or S_ISDIR(src/bin/)", err);
268            String::new()
269        })
270        .parse::<TomlTable>()
271        .unwrap_or_else(|err| {
272            eprintln!("Couldn't parse Cargo.toml: {}; assuming src/main.rs or S_ISDIR(src/bin/)", err);
273            TomlTable::new()
274        })
275        .contains_key("bin") || (Path::new("src/main.rs").exists() || Path::new("src/bin").is_dir());
276    eprintln!("Final verdict: crate has binaries: {}", hasbins);
277
278    if hasbins && rustc_version::version().expect("couldn't get rustc version") >= rustc_version::Version::new(1, 50, 0) {
279        println!("cargo:rustc-link-arg-bins={}", out_file);
280    } else {
281        // Cargo pre-0.51.0 (rustc pre-1.50.0) compat
282        // Only links to the calling crate's library
283        println!("cargo:rustc-link-search=native={}", out_dir);
284        println!("cargo:rustc-link-lib=dylib={}", prefix);
285    }
286    CompilationResult::Ok
287}
288
289/// Likewise, but only for select binaries.
290///
291/// Only available since rustc 1.55.0, does nothing before.
292///
293/// # Examples
294///
295/// ```rust,no_run
296/// extern crate embed_resource;
297///
298/// fn main() {
299/// embed_resource::compile_for("assets/poke-a-mango.rc", &["poke-a-mango", "poke-a-mango-installer"],
300///                             &["VERSION=\"0.5.0\""]);
301///     embed_resource::compile_for("assets/uninstaller.rc", &["unins001"], embed_resource::NONE);
302/// }
303/// ```
304pub fn compile_for<T: AsRef<Path>, J: Display, I: IntoIterator<Item = J>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, for_bins: I,
305                                                                                                                         macros: Mi)
306                                                                                                                         -> CompilationResult {
307    let (_, _, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
308    for bin in for_bins {
309        println!("cargo:rustc-link-arg-bin={}={}", bin, out_file);
310    }
311    CompilationResult::Ok
312}
313
314/// Likewise, but only link the resource to test binaries (select types only. unclear which (and likely to change). you may
315/// prefer [`compile_for_everything()`]).
316///
317/// Only available since rustc 1.60.0, does nothing before.
318pub fn compile_for_tests<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) -> CompilationResult {
319    let (_, _, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
320    println!("cargo:rustc-link-arg-tests={}", out_file);
321    CompilationResult::Ok
322}
323
324/// Likewise, but only link the resource to benchmarks.
325///
326/// Only available since rustc 1.60.0, does nothing before.
327pub fn compile_for_benchmarks<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) -> CompilationResult {
328    let (_, _, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
329    println!("cargo:rustc-link-arg-benches={}", out_file);
330    CompilationResult::Ok
331}
332
333/// Likewise, but only link the resource to examples.
334///
335/// Only available since rustc 1.60.0, does nothing before.
336pub fn compile_for_examples<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) -> CompilationResult {
337    let (_, _, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
338    println!("cargo:rustc-link-arg-examples={}", out_file);
339    CompilationResult::Ok
340}
341
342/// Likewise, but link the resource into *every* artifact: binaries, cdylibs, examples, tests (`[[test]]`/`#[test]`/doctest),
343/// benchmarks, &c.
344///
345/// Only available since rustc 1.50.0, does nothing before.
346pub fn compile_for_everything<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) -> CompilationResult {
347    let (_, _, out_file) = try_compile_impl!(compile_impl(resource_file.as_ref(), macros));
348    println!("cargo:rustc-link-arg={}", out_file);
349    CompilationResult::Ok
350}
351
352fn compile_impl<Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: &Path, macros: Mi) -> Result<(&str, String, String), CompilationResult> {
353    let mut comp = ResourceCompiler::new();
354    if let Some(missing) = comp.is_supported() {
355        if missing.is_empty() {
356            Err(CompilationResult::NotWindows)
357        } else {
358            Err(CompilationResult::NotAttempted(missing))
359        }
360    } else {
361        let prefix = &resource_file.file_stem().expect("resource_file has no stem").to_str().expect("resource_file's stem not UTF-8");
362        let out_dir = env::var("OUT_DIR").expect("No OUT_DIR env var");
363
364        let out_file = comp.compile_resource(&out_dir, &prefix, resource_file.to_str().expect("resource_file not UTF-8"), macros)
365            .map_err(CompilationResult::Failed)?;
366        Ok((prefix, out_dir, out_file))
367    }
368}
369
370fn apply_macros<'t, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(to: &'t mut Command, pref: &str, macros: Mi) -> &'t mut Command {
371    for m in macros {
372        to.arg(pref).arg(m);
373    }
374    to
375}
376
377
378/// Find MSVC build tools other than the compiler and linker
379///
380/// On Windows + MSVC this can be used try to find tools such as `MIDL.EXE` in Windows Kits and/or SDK directories.
381///
382/// The compilers and linkers can be better found with the `cc` or `vswhom` crates.
383/// This always returns `None` on non-MSVC targets.
384///
385/// # Examples
386///
387/// In your build script, find `midl.exe` and use it to compile an IDL file:
388///
389/// ```rust,no_run
390/// # #[cfg(all(target_os = "windows", target_env = "msvc"))]
391/// # {
392/// extern crate embed_resource;
393/// extern crate vswhom;
394/// # use std::env;
395/// # use std::process::Command;
396///
397/// let midl = embed_resource::find_windows_sdk_tool("midl.exe").unwrap();
398///
399/// // midl.exe uses cl.exe as a preprocessor, so it needs to be in PATH
400/// let vs_locations = vswhom::VsFindResult::search().unwrap();
401/// let output = Command::new(midl)
402///     .env("PATH", vs_locations.vs_exe_path.unwrap())
403///     .args(&["/out", &env::var("OUT_DIR").unwrap()])
404///     .arg("haka.pfx.idl").output().unwrap();
405///
406/// assert!(output.status.success());
407/// # }
408/// ```
409pub fn find_windows_sdk_tool<T: AsRef<str>>(tool: T) -> Option<PathBuf> {
410    find_windows_sdk_tool_impl(tool.as_ref())
411}