plugin_system/
lib.rs

1// plugin-system
2// Copyright (C) SOFe
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![recursion_limit = "256"]
17
18extern crate proc_macro;
19
20use proc_macro2::{Delimiter, Group, Literal, TokenTree};
21use quote::quote;
22
23const MY_VERSION: &str = env!("CARGO_PKG_VERSION");
24
25fn test_env_var(name: &str) -> bool {
26    use std::env;
27
28    match env::var(name) {
29        Ok(_) => true,
30        Err(env::VarError::NotPresent) => false,
31        Err(env::VarError::NotUnicode(s)) => panic!(env::VarError::NotUnicode(s)),
32    }
33}
34
35#[proc_macro]
36pub fn load(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
37    let ts = proc_macro2::TokenStream::from(ts);
38    let stmts = parse_ts(ts);
39
40    let mut core = None;
41    let mut plugins = Vec::new();
42    for stmt in stmts.into_iter() {
43        match stmt {
44            TsStmt::Core { package, toml } => {
45                if core.is_some() {
46                    panic!("Encountered multiple core definitions");
47                }
48                core = Some((package, toml));
49            }
50            TsStmt::Plugin { package, toml } => {
51                plugins.push((package, toml));
52            }
53        }
54    }
55    let core = core.expect("Missing core definition");
56
57    let deps = plugins
58        .iter()
59        .map(|(package, toml)| format!("{} = {}", package, toml))
60        .collect::<Vec<_>>()
61        .join("\n");
62
63    let plugin_runtime_path: String = if test_env_var("PS_LOCAL_RUNTIME") {
64        "{path = \"../plugin-runtime\"}".into()
65    } else {
66        format!("\"{}\"", MY_VERSION)
67    };
68
69    let cargo = format!(
70        r##"[workspace]
71
72[package]
73name = "crate"
74version = "0.0.0"
75edition = "2018"
76
77[dependencies]
78plugin-runtime = {}
79{} = {}
80{}
81"##,
82        plugin_runtime_path, core.0, core.1, deps
83    );
84    let cargo = Literal::byte_string(cargo.as_bytes());
85
86    let core_ident = core.0.replace("-", "_");
87    let plugin_pushes = plugins
88        .iter()
89        .map(|(package, _)| {
90            let plugin_ident = package.replace("-", "_");
91            format!("require_plugin!(plugins, {});", &plugin_ident[1..(plugin_ident.len() - 1)])
92        })
93        .collect::<String>();
94    let main = format!(
95        r##"// Auto generated by plugin-system
96
97use plugin_runtime::*;
98
99fn main() {{
100    let mut plugins = PluginList::default();
101    {plugin_pushes}
102    {core_ident}::start(plugins);
103}}
104"##,
105        core_ident = &core_ident[1..(core_ident.len() - 1)],
106        plugin_pushes = plugin_pushes
107    );
108    let main = Literal::byte_string(main.as_bytes());
109
110    let out = quote! {
111        fn main() {
112            use std::env;
113            use std::fs;
114            use std::io::Write;
115            use std::path::PathBuf;
116            use std::process::{self, Command};
117
118            let stage_dir = env::var("STAGE_DIR").unwrap_or("./stage".into());
119            let stage_dir = PathBuf::from(stage_dir);
120
121            if !stage_dir.is_dir() {
122                fs::create_dir_all(&stage_dir).expect("Failed to create stage directory");
123            }
124            let mut f = fs::File::create(stage_dir.join("Cargo.toml")).expect("Failed to open Cargo.toml for writing");
125            f.write_all(#cargo).expect("Failed to write Cargo.toml");
126            drop(f); // flush before build
127
128            let src_dir = stage_dir.join("src");
129            if src_dir.is_dir() {
130                fs::remove_dir_all(&src_dir).expect("Failed to remove old stage/src");
131            }
132            fs::create_dir_all(&src_dir).expect("Failed to create stage/src");
133            let mut f = fs::File::create(src_dir.join("main.rs")).expect("Failed to open src/main.rs for writing");
134            f.write_all(#main).expect("Failed to write src/main.rs");
135            drop(f); // flush before build
136
137            let mut cmd = Command::new("cargo");
138            cmd.arg(if test_env_var("PS_BUILD_ONLY") { "build" } else { "run" });
139            if !test_env_var("PS_DEBUG") {
140                cmd.arg("--release");
141            }
142            cmd.env("RUST_BACKTRACE", "1");
143            cmd.current_dir(stage_dir);
144
145            let status = cmd.status().expect("Failed to run child process");
146            let code = status.code().expect("Process was terminated by signal");
147            process::exit(code);
148        }
149
150        fn test_env_var(name: &str) -> bool {
151            use std::env;
152
153            match env::var(name) {
154                Ok(_) => true,
155                Err(env::VarError::NotPresent) => false,
156                Err(env::VarError::NotUnicode(s)) => panic!(env::VarError::NotUnicode(s)),
157            }
158        }
159    };
160    out.into()
161}
162
163type TsIter = proc_macro2::token_stream::IntoIter;
164type TsPeek = std::iter::Peekable<TsIter>;
165
166fn parse_ts(ts: proc_macro2::TokenStream) -> Vec<TsStmt> {
167    let mut iter = ts.into_iter().peekable();
168    let mut out = Vec::new();
169    while let Some(s) = parse_stmt(&mut iter) {
170        out.push(s);
171    }
172    out
173}
174
175enum TsStmt {
176    Core { package: String, toml: String },
177    Plugin { package: String, toml: String },
178}
179
180fn parse_stmt(iter: &mut TsPeek) -> Option<TsStmt> {
181    let cmd = iter.next()?;
182    let ident = match cmd {
183        TokenTree::Ident(ident) => ident,
184        _ => panic!("Syntax error: Expected 'core' or 'plugin', got {}", cmd),
185    };
186
187    let stmt = match ident.to_string().as_str() {
188        "core" => {
189            let package = expect_lit(
190                iter,
191                "Syntax error: Expected core package name after 'core'",
192            );
193            let toml = read_toml_value(iter, "Expected core version or dependency info table");
194            TsStmt::Core { package, toml }
195        }
196        "plugin" => {
197            let package = expect_lit(
198                iter,
199                "Syntax error: Expected plugin package name after 'plugin'",
200            );
201            let toml = read_toml_value(iter, "Expected plugin version or dependency info table");
202            TsStmt::Plugin { package, toml }
203        }
204        _ => {
205            panic!("Syntax error: Expected 'core' or 'plugin', got {}", ident);
206        }
207    };
208
209    match iter.peek() {
210        Some(TokenTree::Punct(punct)) => {
211            if punct.as_char() == ',' {
212                iter.next().unwrap();
213            }
214        }
215        _ => (),
216    }
217
218    Some(stmt)
219}
220
221fn expect_lit(iter: &mut TsPeek, error: &'static str) -> String {
222    match iter.next().expect(error) {
223        TokenTree::Literal(lit) => lit.to_string(),
224        _ => panic!(error),
225    }
226}
227
228fn read_toml_value(iter: &mut TsPeek, error: &'static str) -> String {
229    let tt = iter.next().expect(&format!("Syntax error: {}", error));
230    match tt {
231        TokenTree::Literal(lit) => lit.to_string(),
232        TokenTree::Group(group) => unwrap_table_trim_comma(group),
233        _ => panic!("Syntax error: {}, got {}", error, tt),
234    }
235}
236
237fn unwrap_table_trim_comma(group: Group) -> String {
238    let delim = group.delimiter();
239    if delim != Delimiter::Brace {
240        panic!("TOML value should be enclosed by braces");
241    }
242    let mut vec = group
243        .stream()
244        .into_iter()
245        .map(|tt| match tt {
246            TokenTree::Ident(ident) => ident.to_string(),
247            TokenTree::Literal(literal) => literal.to_string(),
248            TokenTree::Punct(punct) => punct.to_string(),
249            TokenTree::Group(group) => unwrap_group(group),
250        })
251        .collect::<Vec<_>>();
252    if vec.last() == Some(&",".into()) {
253        vec.pop();
254    }
255
256    format!("{{{}}}", vec.into_iter().collect::<String>())
257}
258
259fn unwrap_group(group: Group) -> String {
260    group
261        .stream()
262        .into_iter()
263        .map(|tt| match tt {
264            TokenTree::Ident(ident) => ident.to_string(),
265            TokenTree::Literal(literal) => literal.to_string(),
266            TokenTree::Punct(punct) => punct.to_string(),
267            TokenTree::Group(group) => unwrap_group(group),
268        })
269        .collect()
270}