1extern crate proc_macro;
4
5use proc_macro_hack::*;
6use proc_macro::*;
7use quote::quote;
8
9use std::ffi::*;
10use std::fs::{self, *};
11use std::io::{self, Write};
12use std::path::*;
13use std::process::*;
14
15macro_rules! fatal {
16 (user, $($tt:tt)+) => {{ eprintln!($($tt)+); exit(1); }};
17 (system, $($tt:tt)+) => {{ eprintln!($($tt)+); exit(1); }};
18 (bug, $($tt:tt)+) => {{ eprintln!($($tt)+); eprintln!("This is a bug! Please file an issue against https://github.com/MaulingMonkey/lies/issues if one doesn't already exist"); exit(1); }};
19}
20
21mod features {
22 pub const ABOUT_PER_CRATE : bool = cfg!(feature = "about-per-crate");
23 pub const ABOUT_PER_WORKSPACE : bool = cfg!(feature = "about-per-workspace");
24}
25
26lazy_static::lazy_static! {
27 static ref CARGO_METADATA : cargo_metadata::Metadata = cargo_metadata::MetadataCommand::new().exec().unwrap_or_else(|err| fatal!(system, "Failed to exec cargo metadata: {}", err));
29 static ref WORKSPACE_DIR : PathBuf = CARGO_METADATA.workspace_root.clone();
30 static ref CARGO_MANIFEST_DIR : PathBuf = get_env_path("CARGO_MANIFEST_DIR");
31 static ref ABOUT_TOML_DIR : PathBuf = get_about_toml_dir();
32}
33
34#[proc_macro_hack]
35pub fn licenses_text(_input: TokenStream) -> TokenStream {
36 emit_quote_cargo_about(include_bytes!("../templates/about.console.hbs"), "about.console.hbs")
37}
38
39#[proc_macro_hack]
40pub fn licenses_ansi(_input: TokenStream) -> TokenStream {
41 emit_quote_cargo_about(include_bytes!("../templates/about.ansi.hbs"), "about.ansi.hbs")
42}
43
44fn emit_quote_cargo_about(input_text: &[u8], input_name: &str) -> TokenStream {
45 let cargo_lock = WORKSPACE_DIR.join("Cargo.lock");
46 let about_toml = ensure_about_toml_exists();
47 let about_out_txt = ensure_about_out_txt_exists(input_text, input_name, &cargo_lock, &about_toml);
48
49 let cargo_lock = cargo_lock .to_str().unwrap_or_else(|| fatal!(system, "Path to Cargo.lock contains invalid unicode: {}", cargo_lock.display()));
50 let about_toml = about_toml .to_str().unwrap_or_else(|| fatal!(system, "Path to about.toml contains invalid unicode: {}", about_toml.display()));
51 let about_out_txt = about_out_txt .to_str().unwrap_or_else(|| fatal!(system, "Path to about.out.txt contains invalid unicode: {}", about_out_txt.display()));
52
53 TokenStream::from(quote!{
54 {
55 const _ : &'static [u8] = include_bytes!(#about_toml);
57 const _ : &'static [u8] = include_bytes!(#cargo_lock);
58
59 include_str!(#about_out_txt)
60 }
61 })
62}
63fn ensure_cargo_about_installed() -> PathBuf {
66 let expected_path = PathBuf::from("cargo-about");
67 let version = cmd_output(format!("{} about --version", expected_path.display()).as_str()).ok();
68 let version = version.as_ref().and_then(|output|{
69 let ws = output.find(' ')?;
70 let (_name, version) = output.split_at(ws);
71 Some(version.trim()) });
73
74 let install = match version {
75 None => { eprintln!("Installing cargo-about"); true },
76 Some("0.0.1") => { eprintln!("Upgrading cargo-about"); true },
77 Some(v) if v.starts_with("0.1.") => false, Some(v) => { eprintln!("cargo-about {} may have breaking changes vs expected version 0.1.x", v); false }, };
80
81 if install {
82 cmd_run(format!("cargo install cargo-about --vers ^0.1 --force").as_str()).unwrap_or_else(|err|
83 fatal!(system, "Failed to install cargo-about 0.0.1: {}", err)
84 );
85 }
86
87 expected_path
88}
89
90fn ensure_about_toml_exists() -> PathBuf {
91 let path = ABOUT_TOML_DIR.join("about.toml");
92 if !path.exists() {
93 let mut about = File::create(&path).unwrap_or_else(|err| fatal!(system, "about.toml does not exist, and cannot be opened for writing: {}", err));
94 about.write_all(include_bytes!("../templates/about.toml")).unwrap_or_else(|err| fatal!(system, "Created but failed to fully write out about.toml: {}", err));
95 }
96 path
97}
98
99fn ensure_about_out_txt_exists(input_text: &[u8], input_name: &str, cargo_lock: &PathBuf, about_toml: &PathBuf) -> PathBuf {
100 let cargo_about = ensure_cargo_about_installed();
101
102 let target_lies = CARGO_METADATA.target_directory.join("lies");
103 if !target_lies.exists() {
104 create_dir_all(&target_lies).unwrap_or_else(|err| fatal!(system, "Failed to create target/lies directory: {}", err));
105 }
106
107 let about_out_txt = if !features::ABOUT_PER_WORKSPACE {
108 format!("{}-{}-{}.out.txt", get_env_path("CARGO_PKG_NAME").display(), get_env_path("CARGO_PKG_VERSION").display(), input_name)
109 } else {
110 format!("{}.out.txt", input_name)
111 };
112 let about_out_txt = target_lies.join(about_out_txt);
113 if let Ok(about_out_txt_mod) = about_out_txt.metadata().and_then(|md| md.modified()) {
114 let mut up_to_date = true;
115 for dependency in [cargo_lock, about_toml].iter() {
116 let dep_mod = dependency
117 .metadata().unwrap_or_else(|err| fatal!(system, "Cannot read {} metadata: {}", dependency.display(), err))
118 .modified().unwrap_or_else(|err| fatal!(system, "Cannot read {} last modified time: {}", dependency.display(), err));
119 if dep_mod >= about_out_txt_mod { up_to_date = false;
121 }
122 }
123 if up_to_date {
124 return about_out_txt;
125 }
126 }
127
128 let tmp_template_path = std::env::temp_dir().join(format!("{}-{}-{}",
129 get_env_path("CARGO_PKG_NAME" ).display(),
130 get_env_path("CARGO_PKG_VERSION").display(),
131 input_name
132 ));
133
134 File::create(&tmp_template_path)
135 .unwrap_or_else(|err| fatal!(system, "Unable to create output .hbs file: {}", err))
136 .write_all(input_text)
137 .unwrap_or_else(|err| fatal!(system, "Unable to write entire output .hbs file: {}", err));
138
139 let output = cmd_output(format!("{} about generate {}", cargo_about.display(), tmp_template_path.display()).as_str()).unwrap_or_else(|err|
140 fatal!(system, "Failed to '{} about generate {}'\n{}", cargo_about.display(), tmp_template_path.display(), err)
141 );
142
143 let output = reprocess(output.as_str());
144 fs::write(&about_out_txt, output).unwrap_or_else(|err| fatal!(system, "Failed to write {}: {}", about_out_txt.display(), err));
145 about_out_txt
146}
147
148fn reprocess(text: &str) -> String {
149 let mut lines = text.lines().map(|line| line
150 .replace(""", "\"")
151 .replace("&", "&")
152 .replace("©", "(c)")
153 ).collect::<Vec<String>>();
154 let lines_n = lines.len();
155
156 for start_line in 0..lines_n {
157 while lines[start_line].contains('\t') {
158 let mut max_col = 0;
160 let mut end_line = start_line;
161 while end_line < lines_n {
162 if let Some(tab) = lines[end_line].find('\t') {
163 max_col = max_col.max(tab);
164 end_line += 1;
165 } else {
166 break;
167 }
168 }
169
170 max_col += 4; for line in start_line..end_line {
174 let line = &mut lines[line];
175 let tab = line.find('\t').unwrap_or_else(|| fatal!(bug, "Markdown table line missing tabs after previous enumeration found tabs"));
176 let mut fixed = line[..tab].to_string();
177 for _ in fixed.chars().count()..max_col {
178 fixed.push(' ');
179 }
180 fixed.push_str(&line[tab+1..]);
181 *line = fixed;
182 }
183 }
184 }
185
186 lines.join("\n")
187}
188
189fn get_about_toml_dir() -> PathBuf {
190 let (workspace_dir, crate_dir) = (&*WORKSPACE_DIR, &*CARGO_MANIFEST_DIR);
191 match (features::ABOUT_PER_WORKSPACE, features::ABOUT_PER_CRATE) {
192 (true, false) => workspace_dir.clone(),
193 (false, true ) => crate_dir.clone(),
194 (true, true ) => fatal!(user, "The \"about-per-crate\" and \"about-per-workspace\" features were enabled"),
195 (false, false) => {
196 if workspace_dir != crate_dir {
197 fatal!(user, "The workspace path doesn't match the crate path, so you must specify the \"about-per-crate\" or \"about-per-workspace\" feature.");
198 }
199 workspace_dir.clone()
200 },
201 }
202}
203
204
205
206
207fn cmd(args_str: &str) -> Command {
208 let wd = get_about_toml_dir();
209 let mut args = args_str.split_whitespace();
210 let exe = args.next().unwrap_or_else(|| fatal!(bug, "cmd expected an exe: {:?}", args_str));
211 let mut cmd = Command::new(exe);
212 cmd.current_dir(wd);
213 for arg in args { cmd.arg(arg); }
214 cmd
215}
216
217fn cmd_run(args: &str) -> io::Result<()> {
218 let status = cmd(args).status()?;
219 if !status.success() {
220 Err(io::Error::new(io::ErrorKind::Other, format!("Failed to successfully run \"{}\": {:?}", args, status)))
221 } else {
222 Ok(())
223 }
224}
225
226fn cmd_output(args: &str) -> io::Result<String> {
227 let output = cmd(args).output()?;
228 if !output.status.success() {
229 let mut s = format!("Failed with {}: {}", output.status, args);
230 for (channel, output ) in [
231 ("stdout", &output.stdout ),
232 ("stderr", &output.stderr ),
233 ].iter().copied() {
234 if !output.is_empty() {
235 s.push_str("\n");
236 s.push_str(channel);
237 s.push_str(":\n");
238 s.push_str("-------");
239 s.push_str(&String::from_utf8_lossy(output));
240 }
241 }
242
243 Err(io::Error::new(io::ErrorKind::Other, s))
244 } else {
245 String::from_utf8(output.stdout).map_err(|err| io::Error::new(
246 io::ErrorKind::InvalidData,
247 format!("{:?} output invalid UTF8: {}", args, err)
248 ))
249 }
250}
251
252fn get_env_path(name: &str) -> PathBuf {
253 PathBuf::from(get_env_os(name))
254}
255
256fn get_env_os(name: &str) -> OsString {
257 std::env::var_os(name).unwrap_or_else(||{
258 if cfg!(windows) {
259 fatal!(system, "%{}%: Not set", name);
260 } else {
261 fatal!(system, "${{{}}}: Not set", name);
262 }
263 })
264}