]> git.lizzy.rs Git - rust.git/blob - crates/proc_macro_srv/src/lib.rs
Rollback env vars changed by a proc macro
[rust.git] / crates / proc_macro_srv / src / lib.rs
1 //! RA Proc Macro Server
2 //!
3 //! This library is able to call compiled Rust custom derive dynamic libraries on arbitrary code.
4 //! The general idea here is based on <https://github.com/fedochet/rust-proc-macro-expander>.
5 //!
6 //! But we adapt it to better fit RA needs:
7 //!
8 //! * We use `tt` for proc-macro `TokenStream` server, it is easier to manipulate and interact with
9 //!   RA than `proc-macro2` token stream.
10 //! * By **copying** the whole rustc `lib_proc_macro` code, we are able to build this with `stable`
11 //!   rustc rather than `unstable`. (Although in general ABI compatibility is still an issue)…
12 #![allow(unreachable_pub)]
13
14 mod dylib;
15 mod abis;
16
17 use std::{
18     collections::{hash_map::Entry, HashMap},
19     env,
20     ffi::OsString,
21     fs,
22     path::{Path, PathBuf},
23     time::SystemTime,
24 };
25
26 use proc_macro_api::{
27     msg::{ExpandMacro, FlatTree, PanicMessage},
28     ProcMacroKind,
29 };
30
31 #[derive(Default)]
32 pub(crate) struct ProcMacroSrv {
33     expanders: HashMap<(PathBuf, SystemTime), dylib::Expander>,
34 }
35
36 impl ProcMacroSrv {
37     pub fn expand(&mut self, task: ExpandMacro) -> Result<FlatTree, PanicMessage> {
38         let expander = self.expander(task.lib.as_ref()).map_err(|err| {
39             debug_assert!(false, "should list macros before asking to expand");
40             PanicMessage(format!("failed to load macro: {}", err))
41         })?;
42
43         let prev_env = EnvSnapshot::new();
44         for (k, v) in &task.env {
45             env::set_var(k, v);
46         }
47         let prev_working_dir = match task.current_dir {
48             Some(dir) => {
49                 let prev_working_dir = std::env::current_dir().ok();
50                 if let Err(err) = std::env::set_current_dir(&dir) {
51                     eprintln!("Failed to set the current working dir to {}. Error: {:?}", dir, err)
52                 }
53                 prev_working_dir
54             }
55             None => None,
56         };
57
58         let macro_body = task.macro_body.to_subtree();
59         let attributes = task.attributes.map(|it| it.to_subtree());
60         let result = expander
61             .expand(&task.macro_name, &macro_body, attributes.as_ref())
62             .map(|it| FlatTree::new(&it));
63
64         prev_env.rollback();
65
66         if let Some(dir) = prev_working_dir {
67             if let Err(err) = std::env::set_current_dir(&dir) {
68                 eprintln!(
69                     "Failed to set the current working dir to {}. Error: {:?}",
70                     dir.display(),
71                     err
72                 )
73             }
74         }
75
76         result.map_err(PanicMessage)
77     }
78
79     pub(crate) fn list_macros(
80         &mut self,
81         dylib_path: &Path,
82     ) -> Result<Vec<(String, ProcMacroKind)>, String> {
83         let expander = self.expander(dylib_path)?;
84         Ok(expander.list_macros())
85     }
86
87     fn expander(&mut self, path: &Path) -> Result<&dylib::Expander, String> {
88         let time = fs::metadata(path).and_then(|it| it.modified()).map_err(|err| {
89             format!("Failed to get file metadata for {}: {:?}", path.display(), err)
90         })?;
91
92         Ok(match self.expanders.entry((path.to_path_buf(), time)) {
93             Entry::Vacant(v) => v.insert(dylib::Expander::new(path).map_err(|err| {
94                 format!("Cannot create expander for {}: {:?}", path.display(), err)
95             })?),
96             Entry::Occupied(e) => e.into_mut(),
97         })
98     }
99 }
100
101 struct EnvSnapshot {
102     vars: HashMap<OsString, OsString>,
103 }
104
105 impl EnvSnapshot {
106     fn new() -> EnvSnapshot {
107         EnvSnapshot { vars: env::vars_os().collect() }
108     }
109
110     fn rollback(self) {
111         let mut old_vars = self.vars;
112         for (name, value) in env::vars_os() {
113             let old_value = old_vars.remove(&name);
114             if old_value != Some(value) {
115                 match old_value {
116                     None => env::remove_var(name),
117                     Some(old_value) => env::set_var(name, old_value),
118                 }
119             }
120         }
121         for (name, old_value) in old_vars {
122             env::set_var(name, old_value)
123         }
124     }
125 }
126
127 pub mod cli;
128
129 #[cfg(test)]
130 mod tests;