lies_impl/
lib.rs

1//! [lies](https://docs.rs/lies/) implementation details.
2
3extern 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    // NOTE:  I intentionally avoid listing most file paths here, to force you to use ensure_* methods to e.g. create them first.
28    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            // Ensure license strings are rebuilt when modified [1]
56            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}
63// [1] https://internals.rust-lang.org/t/pre-rfc-add-a-builtin-macro-to-indicate-build-dependency-to-file/9242/2
64
65fn 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()) // leading ' ', trailing '\n'
72    });
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, // Expected version
78        Some(v)                             => { eprintln!("cargo-about {} may have breaking changes vs expected version 0.1.x", v); false }, // Newer (0.2.x?) but leave alone
79    };
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 { // Dependency was modified more recently than result
120                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            // Find out the size of this "table"
159            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; // Ensure minimum spacing
171
172            // Fixup this "table"
173            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}