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 = "2.5"
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); // or
49//! embed_resource::compile("checksums.rc", &["VERSION=000901"]);
50//! }
51//! ```
52//!
53//! ## Errata
54//!
55//! If no `cargo:rerun-if-changed` annotations are generated, Cargo scans the entire build root by default.
56//! Because the first step in building a manifest is an unspecified C preprocessor step with-out the ability to generate the
57//! equivalent of `cc -MD`, we do *not* output said annotation.
58//!
59//! If scanning is prohibitively expensive, or you have something else that generates the annotations, you may want to spec the
60//! full non-system dependency list for your manifest manually, so:
61//! ```rust,no_run
62//! println!("cargo:rerun-if-changed=app-name-manifest.rc");
63//! embed_resource::compile("app-name-manifest.rc", embed_resource::NONE);
64//! ```
65//! for the above example (cf, [#41](https://github.com/nabijaczleweli/rust-embed-resource/issues/41)).
66//!
67//! # Cross-compilation
68//!
69//! It is possible to embed resources in Windows executables built on non-Windows hosts. There are two ways to do this:
70//!
71//! When targetting `*-pc-windows-gnu`, `*-w64-mingw32-windres` is attempted by default, for `*-pc-windows-msvc` it's `llvm-rc`,
72//! this can be overriden by setting `RC_$TARGET`, `RC_${TARGET//-/_}`, or `RC` environment variables.
73//!
74//! When compiling with LLVM-RC, an external C compiler is used to preprocess the resource,
75//! preloaded with configuration from
76//! [`cc`](https://github.com/alexcrichton/cc-rs#external-configuration-via-environment-variables).
77//!
78//! ## Migration
79//! ### 2.x
80//!
81//! Add `embed_resource::NONE` as the last argument to `embed_resource::compile()` and `embed_resource::compile_for()`.
82//!
83//! # Credit
84//!
85//! In chronological order:
86//!
87//! [@liigo](https://github.com/liigo) -- persistency in pestering me and investigating problems where I have failed
88//!
89//! [@mzji](https://github.com/mzji) -- MSVC lab rat
90//!
91//! [@TheCatPlusPlus](https://github.com/TheCatPlusPlus) -- knowledge and providing first iteration of manifest-embedding code
92//!
93//! [@azyobuzin](https://github.com/azyobuzin) -- providing code for finding places where RC.EXE could hide
94//!
95//! [@retep998](https://github.com/retep998) -- fixing MSVC support
96//!
97//! [@SonnyX](https://github.com/SonnyX) -- Windows cross-compilation support and testing
98//!
99//! [@MSxDOS](https://github.com/MSxDOS) -- finding and supplying RC.EXE its esoteric header include paths
100//!
101//! [@roblabla](https://github.com/roblabla) -- cross-compilation to Windows MSVC via LLVM-RC
102//!
103//! # Special thanks
104//!
105//! To all who support further development on [Patreon](https://patreon.com/nabijaczleweli), in particular:
106//!
107//! * ThePhD
108//! * Embark Studios
109//! * Lars Strojny
110//! * EvModder
111
112
113#[cfg(any(not(target_os = "windows"), all(target_os = "windows", target_env = "msvc")))]
114extern crate cc;
115#[cfg(not(target_os = "windows"))]
116extern crate memchr;
117#[cfg(all(target_os = "windows", target_env = "msvc"))]
118extern crate vswhom;
119#[cfg(all(target_os = "windows", target_env = "msvc"))]
120extern crate winreg;
121extern crate rustc_version;
122extern crate toml;
123
124#[cfg(not(target_os = "windows"))]
125mod non_windows;
126#[cfg(all(target_os = "windows", target_env = "msvc"))]
127mod windows_msvc;
128#[cfg(all(target_os = "windows", not(target_env = "msvc")))]
129mod windows_not_msvc;
130
131#[cfg(not(target_os = "windows"))]
132use self::non_windows::*;
133#[cfg(all(target_os = "windows", target_env = "msvc"))]
134use self::windows_msvc::*;
135#[cfg(all(target_os = "windows", not(target_env = "msvc")))]
136use self::windows_not_msvc::*;
137
138use std::{env, fs};
139use std::ffi::OsStr;
140use std::fmt::Display;
141use std::process::Command;
142use toml::Value as TomlValue;
143use toml::map::Map as TomlMap;
144use std::path::{Path, PathBuf};
145
146
147/// Empty slice, properly-typed for `compile()` and `compile_for()`'s macro list.
148///
149/// Rust helpfully forbids default type parameters on functions, so just passing `[]` doesn't work :)
150pub const NONE: &[&OsStr] = &[];
151
152
153/// Compile the Windows resource file and update the cargo search path if building for Windows.
154///
155/// On non-Windows non-Windows-cross-compile-target this does nothing, on non-MSVC Windows and Windows cross-compile targets,
156/// this chains `windres` with `ar`,
157/// but on MSVC Windows, this will try its hardest to find `RC.EXE` in Windows Kits and/or SDK directories,
158/// falling back to [Jon Blow's VS discovery script](https://pastebin.com/3YvWQa5c),
159/// and on Windows 10 `%INCLUDE%` will be updated to help `RC.EXE` find `windows.h` and friends.
160///
161/// `$OUT_DIR` is added to the include search path.
162///
163/// Note that this does *nothing* if building with rustc before 1.50.0 and there's a library in the crate,
164/// since the resource is linked to the library, if any, instead of the binaries.
165///
166/// Since rustc 1.50.0, the resource is linked only to the binaries
167/// (unless there are none, in which case it's also linked to the library).
168///
169/// `macros` are a list of macros to define, in standard `NAME`/`NAME=VALUE` format.
170///
171/// # Examples
172///
173/// In your build script, assuming the crate's name is "checksums":
174///
175/// ```rust,no_run
176/// extern crate embed_resource;
177///
178/// fn main() {
179/// // Compile and link checksums.rc
180/// embed_resource::compile("checksums.rc", embed_resource::NONE);
181/// }
182/// ```
183pub fn compile<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) {
184 if let Some((prefix, out_dir, out_file)) = compile_impl(resource_file.as_ref(), macros) {
185 let hasbins = fs::read_to_string("Cargo.toml")
186 .unwrap_or_else(|err| {
187 eprintln!("Couldn't read Cargo.toml: {}; assuming src/main.rs or S_ISDIR(src/bin/)", err);
188 String::new()
189 })
190 .parse::<TomlValue>()
191 .unwrap_or_else(|err| {
192 eprintln!("Couldn't parse Cargo.toml: {}; assuming src/main.rs or S_ISDIR(src/bin/)", err);
193 TomlValue::Table(TomlMap::new())
194 })
195 .as_table()
196 .map(|t| t.contains_key("bin"))
197 .unwrap_or(false) || (Path::new("src/main.rs").exists() || Path::new("src/bin").is_dir());
198 eprintln!("Final verdict: crate has binaries: {}", hasbins);
199
200 if hasbins && rustc_version::version().expect("couldn't get rustc version") >= rustc_version::Version::new(1, 50, 0) {
201 println!("cargo:rustc-link-arg-bins={}", out_file);
202 } else {
203 // Cargo pre-0.51.0 (rustc pre-1.50.0) compat
204 // Only links to the calling crate's library
205 println!("cargo:rustc-link-search=native={}", out_dir);
206 println!("cargo:rustc-link-lib=dylib={}", prefix);
207 }
208 }
209}
210
211/// Likewise, but only for select binaries.
212///
213/// Only available since rustc 1.55.0, does nothing before.
214///
215/// # Examples
216///
217/// ```rust,no_run
218/// extern crate embed_resource;
219///
220/// fn main() {
221/// embed_resource::compile_for("assets/poke-a-mango.rc", &["poke-a-mango", "poke-a-mango-installer"],
222/// &["VERSION=\"0.5.0\""]);
223/// embed_resource::compile_for("assets/uninstaller.rc", &["unins001"], embed_resource::NONE);
224/// }
225/// ```
226pub 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,
227 macros: Mi) {
228 if let Some((_, _, out_file)) = compile_impl(resource_file.as_ref(), macros) {
229 for bin in for_bins {
230 println!("cargo:rustc-link-arg-bin={}={}", bin, out_file);
231 }
232 }
233}
234
235/// Likewise, but only link the resource to test binaries (select types only. unclear which (and likely to change). you may prefer [`compile_for_everything()`]).
236///
237/// Only available since rustc 1.60.0, does nothing before.
238pub fn compile_for_tests<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) {
239 if let Some((_, _, out_file)) = compile_impl(resource_file.as_ref(), macros) {
240 println!("cargo:rustc-link-arg-tests={}", out_file);
241 }
242}
243
244/// Likewise, but only link the resource to benchmarks.
245///
246/// Only available since rustc 1.60.0, does nothing before.
247pub fn compile_for_benchmarks<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) {
248 if let Some((_, _, out_file)) = compile_impl(resource_file.as_ref(), macros) {
249 println!("cargo:rustc-link-arg-benches={}", out_file);
250 }
251}
252
253/// Likewise, but only link the resource to examples.
254///
255/// Only available since rustc 1.60.0, does nothing before.
256pub fn compile_for_examples<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) {
257 if let Some((_, _, out_file)) = compile_impl(resource_file.as_ref(), macros) {
258 println!("cargo:rustc-link-arg-examples={}", out_file);
259 }
260}
261
262/// Likewise, but link the resource into *every* artifact: binaries, cdylibs, examples, tests (`[[test]]`/`#[test]`/doctest), benchmarks, &c.
263///
264/// Only available since rustc 1.50.0, does nothing before.
265pub fn compile_for_everything<T: AsRef<Path>, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: T, macros: Mi) {
266 if let Some((_, _, out_file)) = compile_impl(resource_file.as_ref(), macros) {
267 println!("cargo:rustc-link-arg={}", out_file);
268 }
269}
270
271fn compile_impl<Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(resource_file: &Path, macros: Mi) -> Option<(&str, String, String)> {
272 let comp = ResourceCompiler::new();
273 if comp.is_supported() {
274 let prefix = &resource_file.file_stem().expect("resource_file has no stem").to_str().expect("resource_file's stem not UTF-8");
275 let out_dir = env::var("OUT_DIR").expect("No OUT_DIR env var");
276
277 let out_file = comp.compile_resource(&out_dir, &prefix, resource_file.to_str().expect("resource_file not UTF-8"), macros);
278 Some((prefix, out_dir, out_file))
279 } else {
280 None
281 }
282}
283
284fn apply_macros<'t, Ms: AsRef<OsStr>, Mi: IntoIterator<Item = Ms>>(to: &'t mut Command, pref: &str, macros: Mi) -> &'t mut Command {
285 for m in macros {
286 to.arg(pref).arg(m);
287 }
288 to
289}
290
291
292/// Find MSVC build tools other than the compiler and linker
293///
294/// On Windows + MSVC this can be used try to find tools such as `MIDL.EXE` in Windows Kits and/or SDK directories.
295///
296/// The compilers and linkers can be better found with the `cc` or `vswhom` crates.
297/// This always returns `None` on non-MSVC targets.
298///
299/// # Examples
300///
301/// In your build script, find `midl.exe` and use it to compile an IDL file:
302///
303/// ```rust,no_run
304/// # #[cfg(all(target_os = "windows", target_env = "msvc"))]
305/// # {
306/// extern crate embed_resource;
307/// extern crate vswhom;
308/// # use std::env;
309/// # use std::process::Command;
310///
311/// let midl = embed_resource::find_windows_sdk_tool("midl.exe").unwrap();
312///
313/// // midl.exe uses cl.exe as a preprocessor, so it needs to be in PATH
314/// let vs_locations = vswhom::VsFindResult::search().unwrap();
315/// let output = Command::new(midl)
316/// .env("PATH", vs_locations.vs_exe_path.unwrap())
317/// .args(&["/out", &env::var("OUT_DIR").unwrap()])
318/// .arg("haka.pfx.idl").output().unwrap();
319///
320/// assert!(output.status.success());
321/// # }
322/// ```
323pub fn find_windows_sdk_tool<T: AsRef<str>>(tool: T) -> Option<PathBuf> {
324 find_windows_sdk_tool_impl(tool.as_ref())
325}