diff --git a/.cargo/config.toml b/.cargo/config.toml index 3d99f63..adea6df 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[alias] +xtask = ["run", "-p", "xtask", "--bin", "xtask", "--"] + [build] # This works both on local `cargo doc` and on docs.rs because we don't have any `normal` dependency. # In most cases, `package.metadata.docs.rs` is recommended. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9658366..114d0ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --workspace --no-fail-fast + args: --no-fail-fast env: RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 5348943..3ab81ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["xtask/"] + [package] name = "ac-library-rs" version = "0.1.0" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..da156d7 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "xtask" +version = "0.0.0" +authors = ["rust-lang-ja developers"] +edition = "2018" +license = "CC0-1.0" +repository = "https://github.com/rust-lang-ja/ac-library-rs" +publish = false + +[dependencies] +anyhow = "1.0.32" +atty = "0.2.14" +cargo_metadata = "0.11.2" +duct = "0.13.4" +proc-macro2 = { version = "1.0.21", features = ["span-locations"] } +quote = "1.0.7" +structopt = "0.3.17" +syn = { version = "1.0.40", features = ["full"] } +tempfile = "3.1.0" +termcolor = "1.1.0" diff --git a/xtask/src/commands.rs b/xtask/src/commands.rs new file mode 100644 index 0000000..743c3e4 --- /dev/null +++ b/xtask/src/commands.rs @@ -0,0 +1 @@ +pub(crate) mod export; diff --git a/xtask/src/commands/export.rs b/xtask/src/commands/export.rs new file mode 100644 index 0000000..8b5ad32 --- /dev/null +++ b/xtask/src/commands/export.rs @@ -0,0 +1,150 @@ +use crate::shell::Shell; +use anyhow::{anyhow, bail, Context as _}; +use cargo_metadata::{self as cm, MetadataCommand}; +use duct::cmd; +use quote::ToTokens as _; +use std::{ + env, + io::{self, Write as _}, + path::{Path, PathBuf}, +}; +use structopt::StructOpt; +use syn::{Item, ItemMod}; + +#[derive(StructOpt, Debug)] +pub struct OptExport { + /// Save the output to the file + #[structopt(short, long, value_name("PATH"))] + output: Option, +} + +pub(crate) fn run(opt: OptExport, shell: &mut Shell) -> anyhow::Result<()> { + let OptExport { output } = opt; + + let metadata = MetadataCommand::new() + .no_deps() + .exec() + .map_err(|err| match err { + cm::Error::CargoMetadata { stderr } => { + anyhow!("{}", stderr.trim_start_matches("error: ")) + } + err => anyhow!("{}", err), + })?; + + let cm::Target { src_path, .. } = metadata + .packages + .iter() + .filter(|p| p.manifest_path == metadata.workspace_root.join("Cargo.toml")) + .flat_map(|p| &p.targets) + .find(|cm::Target { kind, .. }| *kind == ["lib".to_owned()]) + .with_context(|| "could find the library")?; + + let code = std::fs::read_to_string(src_path)?; + let syn::File { items, .. } = + syn::parse_file(&code).with_context(|| format!("`{}` is broken", src_path.display()))?; + + let mut acc = vec!["".to_owned(), "".to_owned()]; + + for item in items { + match item { + Item::Mod(ItemMod { + attrs, + vis, + ident, + content: None, + semi: Some(_), + .. + }) => { + let acc = &mut acc[1]; + let path = src_path + .with_file_name(ident.to_string()) + .with_extension("rs"); + if !path.exists() { + unimplemented!("is this `mod.rs`?: {}", ident); + } + let content = std::fs::read_to_string(&path)?; + let is_safe_to_indent = !syn::parse_file(&content) + .map_err(|e| anyhow!("{:?}", e)) + .with_context(|| format!("could not parse `{}`", path.display()))? + .into_token_stream() + .into_iter() + .any(|tt| { + matches!( + tt, proc_macro2::TokenTree::Literal(lit) + if lit.span().start().line != lit.span().end().line + ) + }); + + for attr in attrs { + *acc += &attr.to_token_stream().to_string(); + *acc += "\n"; + } + *acc += &vis.to_token_stream().to_string(); + *acc += " mod "; + *acc += &ident.to_string(); + *acc += " {\n"; + if is_safe_to_indent { + for line in content.lines() { + *acc += " "; + *acc += line; + *acc += "\n"; + } + } else { + *acc += &content; + } + *acc += "}\n"; + } + item => { + let acc = &mut acc[0]; + *acc += &item.to_token_stream().to_string(); + *acc += "\n"; + } + } + } + + let acc = rustfmt(&acc.join("\n"))?; + + shell.status( + "Expanded", + format!("{} ({} B)", src_path.display(), acc.len()), + )?; + + if let Some(output) = output { + std::fs::write(&output, acc) + .with_context(|| format!("could not write `{}`", output.display()))?; + shell.status("Wrote", output.display())?; + } else { + io::stdout().write_all(acc.as_ref())?; + io::stdout().flush()?; + } + Ok(()) +} + +fn rustfmt(code: &str) -> anyhow::Result { + let tempdir = tempfile::Builder::new() + .prefix("ac-library-rs-xtask") + .tempdir()?; + + let path = tempdir.path().join("expanded.rs"); + + std::fs::write(&path, code)?; + + let rustfmt_exe = Path::new(&env::var_os("CARGO").with_context(|| "missing `$CARGO`")?) + .with_file_name("rustfmt") + .with_extension(env::consts::EXE_EXTENSION); + + if !rustfmt_exe.exists() { + bail!( + "`{}` does not exist. Run `rustup component add rustfmt` first", + rustfmt_exe.display(), + ); + } + + cmd!(rustfmt_exe, "--edition", "2018", &path) + .run() + .with_context(|| "could not format the output")?; + + let output = std::fs::read_to_string(path)?; + tempdir.close()?; + Ok(output) +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 index 0000000..d3e3fdd --- /dev/null +++ b/xtask/src/lib.rs @@ -0,0 +1,37 @@ +mod commands; +pub mod shell; + +use crate::commands::export::OptExport; +use crate::shell::Shell; +use std::io::Write as _; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(bin_name("cargo xtask"))] +pub enum Opt { + /// Export the library + Export(OptExport), +} + +pub fn run(opt: Opt, shell: &mut Shell) -> anyhow::Result<()> { + match opt { + Opt::Export(opt) => commands::export::run(opt, shell), + } +} + +pub fn exit_with_error(err: anyhow::Error, shell: &mut Shell) -> ! { + let _ = shell.error(&err); + + for cause in err.chain().skip(1) { + let _ = writeln!(shell.err(), "\nCaused by:"); + + for line in cause.to_string().lines() { + let _ = match line { + "" => writeln!(shell.err()), + line => writeln!(shell.err(), " {}", line), + }; + } + } + + std::process::exit(1); +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..620399d --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,10 @@ +use structopt::StructOpt as _; +use xtask::{shell::Shell, Opt}; + +fn main() { + let opt = Opt::from_args(); + let mut shell = Shell::new(); + if let Err(err) = xtask::run(opt, &mut shell) { + xtask::exit_with_error(err, &mut shell); + } +} diff --git a/xtask/src/shell.rs b/xtask/src/shell.rs new file mode 100644 index 0000000..b1ff808 --- /dev/null +++ b/xtask/src/shell.rs @@ -0,0 +1,63 @@ +use std::{ + fmt, + io::{self, Write as _}, +}; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor as _}; + +pub struct Shell { + stderr: StandardStream, +} + +impl Shell { + pub fn new() -> Self { + Self { + stderr: StandardStream::stderr(if atty::is(atty::Stream::Stderr) { + ColorChoice::Auto + } else { + ColorChoice::Never + }), + } + } + + pub(crate) fn err(&mut self) -> &mut StandardStream { + &mut self.stderr + } + + pub(crate) fn status( + &mut self, + status: impl fmt::Display, + message: impl fmt::Display, + ) -> io::Result<()> { + self.print(status, message, Color::Green, true) + } + + pub fn error(&mut self, message: impl fmt::Display) -> io::Result<()> { + self.print("error", message, Color::Red, false) + } + + fn print( + &mut self, + status: impl fmt::Display, + message: impl fmt::Display, + color: Color, + justified: bool, + ) -> io::Result<()> { + self.stderr + .set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?; + if justified { + write!(self.stderr, "{:>12}", status)?; + } else { + write!(self.stderr, "{}", status)?; + self.stderr.set_color(ColorSpec::new().set_bold(true))?; + write!(self.stderr, ":")?; + } + self.stderr.reset()?; + writeln!(self.stderr, " {}", message) + } +} + +impl Default for Shell { + fn default() -> Self { + Self::new() + } +}