plugin-system 0.1.1

A Rust project plugin management system using Cargo
Documentation
// plugin-system
// Copyright (C) SOFe
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![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); // flush before build

            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); // flush before build

            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()
}