#![recursion_limit = "256"]
extern crate proc_macro;
use proc_macro2::{Delimiter, Group, Literal, TokenTree};
use quote::quote;
const MY_VERSION: &str = env!("CARGO_PKG_VERSION");
fn test_env_var(name: &str) -> bool {
use std::env;
match env::var(name) {
Ok(_) => true,
Err(env::VarError::NotPresent) => false,
Err(env::VarError::NotUnicode(s)) => panic!(env::VarError::NotUnicode(s)),
}
}
#[proc_macro]
pub fn load(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ts = proc_macro2::TokenStream::from(ts);
let stmts = parse_ts(ts);
let mut core = None;
let mut plugins = Vec::new();
for stmt in stmts.into_iter() {
match stmt {
TsStmt::Core { package, toml } => {
if core.is_some() {
panic!("Encountered multiple core definitions");
}
core = Some((package, toml));
}
TsStmt::Plugin { package, toml } => {
plugins.push((package, toml));
}
}
}
let core = core.expect("Missing core definition");
let deps = plugins
.iter()
.map(|(package, toml)| format!("{} = {}", package, toml))
.collect::<Vec<_>>()
.join("\n");
let plugin_runtime_path: String = if test_env_var("PS_LOCAL_RUNTIME") {
"{path = \"../plugin-runtime\"}".into()
} else {
format!("\"{}\"", MY_VERSION)
};
let cargo = format!(
r##"[workspace]
[package]
name = "crate"
version = "0.0.0"
edition = "2018"
[dependencies]
plugin-runtime = {}
{} = {}
{}
"##,
plugin_runtime_path, core.0, core.1, deps
);
let cargo = Literal::byte_string(cargo.as_bytes());
let core_ident = core.0.replace("-", "_");
let plugin_pushes = plugins
.iter()
.map(|(package, _)| {
let plugin_ident = package.replace("-", "_");
format!("require_plugin!(plugins, {});", &plugin_ident[1..(plugin_ident.len() - 1)])
})
.collect::<String>();
let main = format!(
r##"// Auto generated by plugin-system
use plugin_runtime::*;
fn main() {{
let mut plugins = PluginList::default();
{plugin_pushes}
{core_ident}::start(plugins);
}}
"##,
core_ident = &core_ident[1..(core_ident.len() - 1)],
plugin_pushes = plugin_pushes
);
let main = Literal::byte_string(main.as_bytes());
let out = quote! {
fn main() {
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::{self, Command};
let stage_dir = env::var("STAGE_DIR").unwrap_or("./stage".into());
let stage_dir = PathBuf::from(stage_dir);
if !stage_dir.is_dir() {
fs::create_dir_all(&stage_dir).expect("Failed to create stage directory");
}
let mut f = fs::File::create(stage_dir.join("Cargo.toml")).expect("Failed to open Cargo.toml for writing");
f.write_all(#cargo).expect("Failed to write Cargo.toml");
drop(f);
let src_dir = stage_dir.join("src");
if src_dir.is_dir() {
fs::remove_dir_all(&src_dir).expect("Failed to remove old stage/src");
}
fs::create_dir_all(&src_dir).expect("Failed to create stage/src");
let mut f = fs::File::create(src_dir.join("main.rs")).expect("Failed to open src/main.rs for writing");
f.write_all(#main).expect("Failed to write src/main.rs");
drop(f);
let mut cmd = Command::new("cargo");
cmd.arg(if test_env_var("PS_BUILD_ONLY") { "build" } else { "run" });
if !test_env_var("PS_DEBUG") {
cmd.arg("--release");
}
cmd.env("RUST_BACKTRACE", "1");
cmd.current_dir(stage_dir);
let status = cmd.status().expect("Failed to run child process");
let code = status.code().expect("Process was terminated by signal");
process::exit(code);
}
fn test_env_var(name: &str) -> bool {
use std::env;
match env::var(name) {
Ok(_) => true,
Err(env::VarError::NotPresent) => false,
Err(env::VarError::NotUnicode(s)) => panic!(env::VarError::NotUnicode(s)),
}
}
};
out.into()
}
type TsIter = proc_macro2::token_stream::IntoIter;
type TsPeek = std::iter::Peekable<TsIter>;
fn parse_ts(ts: proc_macro2::TokenStream) -> Vec<TsStmt> {
let mut iter = ts.into_iter().peekable();
let mut out = Vec::new();
while let Some(s) = parse_stmt(&mut iter) {
out.push(s);
}
out
}
enum TsStmt {
Core { package: String, toml: String },
Plugin { package: String, toml: String },
}
fn parse_stmt(iter: &mut TsPeek) -> Option<TsStmt> {
let cmd = iter.next()?;
let ident = match cmd {
TokenTree::Ident(ident) => ident,
_ => panic!("Syntax error: Expected 'core' or 'plugin', got {}", cmd),
};
let stmt = match ident.to_string().as_str() {
"core" => {
let package = expect_lit(
iter,
"Syntax error: Expected core package name after 'core'",
);
let toml = read_toml_value(iter, "Expected core version or dependency info table");
TsStmt::Core { package, toml }
}
"plugin" => {
let package = expect_lit(
iter,
"Syntax error: Expected plugin package name after 'plugin'",
);
let toml = read_toml_value(iter, "Expected plugin version or dependency info table");
TsStmt::Plugin { package, toml }
}
_ => {
panic!("Syntax error: Expected 'core' or 'plugin', got {}", ident);
}
};
match iter.peek() {
Some(TokenTree::Punct(punct)) => {
if punct.as_char() == ',' {
iter.next().unwrap();
}
}
_ => (),
}
Some(stmt)
}
fn expect_lit(iter: &mut TsPeek, error: &'static str) -> String {
match iter.next().expect(error) {
TokenTree::Literal(lit) => lit.to_string(),
_ => panic!(error),
}
}
fn read_toml_value(iter: &mut TsPeek, error: &'static str) -> String {
let tt = iter.next().expect(&format!("Syntax error: {}", error));
match tt {
TokenTree::Literal(lit) => lit.to_string(),
TokenTree::Group(group) => unwrap_table_trim_comma(group),
_ => panic!("Syntax error: {}, got {}", error, tt),
}
}
fn unwrap_table_trim_comma(group: Group) -> String {
let delim = group.delimiter();
if delim != Delimiter::Brace {
panic!("TOML value should be enclosed by braces");
}
let mut vec = group
.stream()
.into_iter()
.map(|tt| match tt {
TokenTree::Ident(ident) => ident.to_string(),
TokenTree::Literal(literal) => literal.to_string(),
TokenTree::Punct(punct) => punct.to_string(),
TokenTree::Group(group) => unwrap_group(group),
})
.collect::<Vec<_>>();
if vec.last() == Some(&",".into()) {
vec.pop();
}
format!("{{{}}}", vec.into_iter().collect::<String>())
}
fn unwrap_group(group: Group) -> String {
group
.stream()
.into_iter()
.map(|tt| match tt {
TokenTree::Ident(ident) => ident.to_string(),
TokenTree::Literal(literal) => literal.to_string(),
TokenTree::Punct(punct) => punct.to_string(),
TokenTree::Group(group) => unwrap_group(group),
})
.collect()
}