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}