compile_time_run/
lib.rs

1// Copyright 2019 Maarten de Vries <maarten@de-vri.es>
2//
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
5//
6// 1. Redistributions of source code must retain the above copyright notice, this
7//    list of conditions and the following disclaimer.
8//
9// 2. Redistributions in binary form must reproduce the above copyright notice,
10//    this list of conditions and the following disclaimer in the documentation
11//    and/or other materials provided with the distribution.
12//
13// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
24//! This crate contains macros to run commands on the host system at compile time.
25//! It can be used in some situations to take over functionaility that would otherwise
26//! have to be done using a build script.
27//!
28//!
29//! An example:
30//! ```
31//! use compile_time_run::{run_command, run_command_str};
32//! const VALUE_STR   : &'static str  = run_command_str!("echo", "Hello World!");
33//! const VALUE_BYTES : &'static [u8] = run_command!("echo", "Hello World!");
34//! ```
35//!
36//! Keep in mind that running arbitrary commands during your build phase can easily hurt portability.
37
38use syn::parse_macro_input;
39
40/// Run a command at compile time, and return the output as a byte slice.
41///
42/// The output is a static &[u8], and can be used for the value of consts.
43/// If the command fails to run, a compile error is generated.
44///
45/// If the output ends with a newline, it is stripped.
46/// At most one newline character is stripped this way.
47///
48/// For example:
49/// ```
50/// use compile_time_run::run_command;
51/// const VALUE : &'static [u8] = run_command!("echo", "Hello World!");
52/// ```
53#[proc_macro]
54pub fn run_command(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
55	detail::run_command(parse_macro_input!(input))
56		.unwrap_or_else(|error| error.to_compile_error())
57		.into()
58}
59
60/// Run a command at compile time, and return the output as a str.
61///
62/// The output is a static &str, and can be used for the value of consts.
63/// If the command fails to run, a compile error is generated.
64///
65/// If the output ends with a newline, it is stripped.
66/// At most one newline character is stripped this way.
67///
68/// For example:
69/// ```
70/// use compile_time_run::run_command_str;
71/// const VALUE : &'static str = run_command_str!("echo", "Hello World!");
72/// ```
73#[proc_macro]
74pub fn run_command_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
75	detail::run_command_str(parse_macro_input!(input))
76		.unwrap_or_else(|error| error.to_compile_error())
77		.into()
78}
79
80mod detail {
81	use std::process::Command;
82
83	use proc_macro2::Span;
84	use quote::quote;
85	use syn::{Error, Result};
86
87	pub fn run_command_str(input: ArgList) -> Result<proc_macro2::TokenStream> {
88		let args: Vec<_> = input.args.iter().map(|x| x.value()).collect();
89
90		let output = execute_command(Command::new(&args[0]).args(&args[1..]))?;
91		let output = strip_trailing_newline(output.stdout);
92		let output = std::str::from_utf8(&output).expect("invalid UTF-8 in command output");
93
94		Ok(quote!(#output))
95	}
96
97	pub fn run_command(input: ArgList) -> Result<proc_macro2::TokenStream> {
98		let args: Vec<_> = input.args.iter().map(|x| x.value()).collect();
99
100		let output = execute_command(Command::new(&args[0]).args(&args[1..]))?;
101		let output = strip_trailing_newline(output.stdout);
102		let output = syn::LitByteStr::new(&output, proc_macro2::Span::call_site());
103
104		Ok(quote!(#output))
105	}
106
107	/// Comma seperated argument list of string literals.
108	pub struct ArgList {
109		args: syn::punctuated::Punctuated<syn::LitStr, syn::token::Comma>,
110	}
111
112	impl syn::parse::Parse for ArgList {
113		fn parse(input: syn::parse::ParseStream) -> Result<Self> {
114			type Inner = syn::punctuated::Punctuated<syn::LitStr, syn::token::Comma>;
115			let args = Inner::parse_terminated(input)?;
116
117			if args.is_empty() {
118				Err(Error::new(input.cursor().span(), "missing required argument: command"))
119			} else {
120				Ok(Self { args })
121			}
122		}
123	}
124
125	fn execute_command(command: &mut Command) -> Result<std::process::Output> {
126		let output = command
127			.output()
128			.map_err(|error| Error::new(Span::call_site(), format!("failed to execute command: {}", error)))?;
129
130		verbose_command_error(output).map_err(|message| Error::new(Span::call_site(), message))
131	}
132
133	/// Check if a command ran successfully, and if not, return a verbose error.
134	fn verbose_command_error(output: std::process::Output) -> std::result::Result<std::process::Output, String> {
135		// If the command succeeded, just return the output as is.
136		if output.status.success() {
137			Ok(output)
138
139		// If the command terminated with non-zero exit code, return an error.
140		} else if let Some(status) = output.status.code() {
141			// Include stderr in the error message if it's not empty, no too long,
142			// has no newlines and is valid UTF-8.
143			let message = Some(strip_trailing_newline(output.stderr));
144
145			let message = message.filter(|m| !m.is_empty() && m.len() <= 500);
146			let message = message.filter(|m| !m.iter().any(|c| c == &b'\n'));
147			let message = message.and_then(|m| String::from_utf8(m).ok());
148
149			if let Some(message) = message {
150				Err(format!("external command exited with status {}: {}", status, message))
151			} else {
152				Err(format!("external command exited with status {}", status))
153			}
154
155		// The command was killed by a signal.
156		} else {
157			// Include the signal number on Unix.
158			#[cfg(target_family = "unix")]
159			{
160				use std::os::unix::process::ExitStatusExt;
161				if let Some(signal) = output.status.signal() {
162					Err(format!("external command killed by signal {}", signal))
163				} else {
164					Err("external command failed, but did not exit and was not killed by a signal, this can only be a bug in std::process".into())
165				}
166			}
167			#[cfg(not(target_family = "unix"))]
168			{
169				Err(format!("external command killed by signal"))
170			}
171		}
172	}
173
174	/// Remove a trailing newline from a byte string.
175	fn strip_trailing_newline(mut input: Vec<u8>) -> Vec<u8> {
176		if !input.is_empty() && input[input.len() - 1] == b'\n' {
177			input.pop();
178		}
179		input
180	}
181}