Skip to content

Commit 9f3be85

Browse files
committed
Add cargo xtask export command
1 parent a8c306d commit 9f3be85

File tree

8 files changed

+285
-0
lines changed

8 files changed

+285
-0
lines changed

.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
[alias]
2+
xtask = ["run", "-p", "xtask", "--bin", "xtask", "--"]
3+
14
[build]
25
rustdocflags = ["--html-in-header", "./doc/katex-header.html"]

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace]
2+
members = ["xtask/"]
3+
14
[package]
25
name = "ac-library-rs"
36
version = "0.1.0"

xtask/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "xtask"
3+
version = "0.0.0"
4+
authors = ["rust-lang-ja developers"]
5+
edition = "2018"
6+
license = "CC0-1.0"
7+
repository = "https://github.com/rust-lang-ja/ac-library-rs"
8+
publish = false
9+
10+
[dependencies]
11+
anyhow = "1.0.32"
12+
atty = "0.2.14"
13+
cargo_metadata = "0.11.2"
14+
duct = "0.13.4"
15+
proc-macro2 = { version = "1.0.21", features = ["span-locations"] }
16+
quote = "1.0.7"
17+
structopt = "0.3.17"
18+
syn = { version = "1.0.40", features = ["full"] }
19+
tempfile = "3.1.0"
20+
termcolor = "1.1.0"

xtask/src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod export;

xtask/src/commands/export.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use crate::shell::Shell;
2+
use anyhow::{anyhow, bail, Context as _};
3+
use cargo_metadata::{self as cm, MetadataCommand};
4+
use duct::cmd;
5+
use quote::ToTokens as _;
6+
use std::{
7+
env,
8+
io::{self, Write as _},
9+
path::{Path, PathBuf},
10+
};
11+
use structopt::StructOpt;
12+
use syn::{Item, ItemMod};
13+
14+
#[derive(StructOpt, Debug)]
15+
pub struct OptExport {
16+
/// Save the output to the file
17+
#[structopt(short, long, value_name("PATH"))]
18+
output: Option<PathBuf>,
19+
}
20+
21+
pub(crate) fn run(opt: OptExport, shell: &mut Shell) -> anyhow::Result<()> {
22+
let OptExport { output } = opt;
23+
24+
let metadata = MetadataCommand::new()
25+
.no_deps()
26+
.exec()
27+
.map_err(|err| match err {
28+
cm::Error::CargoMetadata { stderr } => {
29+
anyhow!("{}", stderr.trim_start_matches("error: "))
30+
}
31+
err => anyhow!("{}", err),
32+
})?;
33+
34+
let cm::Target { src_path, .. } = metadata
35+
.packages
36+
.iter()
37+
.filter(|p| p.manifest_path == metadata.workspace_root.join("Cargo.toml"))
38+
.flat_map(|p| &p.targets)
39+
.find(|cm::Target { kind, .. }| *kind == ["lib".to_owned()])
40+
.with_context(|| "could find the library")?;
41+
42+
let code = std::fs::read_to_string(src_path)?;
43+
let syn::File { items, .. } =
44+
syn::parse_file(&code).with_context(|| format!("`{}` is broken", src_path.display()))?;
45+
46+
let mut acc = "".to_owned();
47+
48+
for item in items {
49+
match item {
50+
Item::Mod(ItemMod {
51+
attrs,
52+
vis,
53+
ident,
54+
content: None,
55+
semi: Some(_),
56+
..
57+
}) => {
58+
let path = src_path
59+
.with_file_name(ident.to_string())
60+
.with_extension("rs");
61+
if !path.exists() {
62+
unimplemented!("is this `mod.rs`?: {}", ident);
63+
}
64+
let content = std::fs::read_to_string(&path)?;
65+
let is_safe_to_indent = !syn::parse_file(&content)
66+
.map_err(|e| anyhow!("{:?}", e))
67+
.with_context(|| format!("could not parse `{}`", path.display()))?
68+
.into_token_stream()
69+
.into_iter()
70+
.any(|tt| {
71+
matches!(
72+
tt, proc_macro2::TokenTree::Literal(lit)
73+
if lit.span().start().line != lit.span().end().line
74+
)
75+
});
76+
77+
for attr in attrs {
78+
acc += &attr.to_token_stream().to_string();
79+
acc += "\n";
80+
}
81+
acc += &vis.to_token_stream().to_string();
82+
acc += " mod ";
83+
acc += &ident.to_string();
84+
acc += " {\n";
85+
if is_safe_to_indent {
86+
for line in content.lines() {
87+
acc += " ";
88+
acc += line;
89+
acc += "\n";
90+
}
91+
} else {
92+
acc += &content;
93+
}
94+
acc += "}\n";
95+
}
96+
item => {
97+
acc += &item.to_token_stream().to_string();
98+
acc += "\n";
99+
}
100+
}
101+
}
102+
103+
acc = rustfmt(&acc)?;
104+
105+
shell.status(
106+
"Expanded",
107+
format!("{} ({} B)", src_path.display(), acc.len()),
108+
)?;
109+
110+
if let Some(output) = output {
111+
std::fs::write(&output, acc)
112+
.with_context(|| format!("could not write `{}`", output.display()))?;
113+
shell.status("Wrote", output.display())?;
114+
} else {
115+
io::stdout().write_all(acc.as_ref())?;
116+
io::stdout().flush()?;
117+
}
118+
Ok(())
119+
}
120+
121+
fn rustfmt(code: &str) -> anyhow::Result<String> {
122+
let tempdir = tempfile::Builder::new()
123+
.prefix("ac-library-rs-xtask")
124+
.tempdir()?;
125+
126+
let path = tempdir.path().join("expanded.rs");
127+
128+
std::fs::write(&path, code)?;
129+
130+
let rustfmt_exe = Path::new(&env::var_os("CARGO").with_context(|| "missing `$CARGO`")?)
131+
.with_file_name("rustfmt")
132+
.with_extension(env::consts::EXE_EXTENSION);
133+
134+
if !rustfmt_exe.exists() {
135+
bail!(
136+
"`{}` does not exist. Run `rustup component add rustfmt` first",
137+
rustfmt_exe.display(),
138+
);
139+
}
140+
141+
cmd!(rustfmt_exe, "--edition", "2018", &path)
142+
.run()
143+
.with_context(|| "could not format the output")?;
144+
145+
let output = std::fs::read_to_string(path)?;
146+
tempdir.close()?;
147+
Ok(output)
148+
}

xtask/src/lib.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
mod commands;
2+
pub mod shell;
3+
4+
use crate::commands::export::OptExport;
5+
use crate::shell::Shell;
6+
use std::io::Write as _;
7+
use structopt::StructOpt;
8+
9+
#[derive(StructOpt, Debug)]
10+
#[structopt(bin_name("cargo xtask"))]
11+
pub enum Opt {
12+
/// Export the library
13+
Export(OptExport),
14+
}
15+
16+
pub fn run(opt: Opt, shell: &mut Shell) -> anyhow::Result<()> {
17+
match opt {
18+
Opt::Export(opt) => commands::export::run(opt, shell),
19+
}
20+
}
21+
22+
pub fn exit_with_error(err: anyhow::Error, shell: &mut Shell) -> ! {
23+
let _ = shell.error(&err);
24+
25+
for cause in err.chain().skip(1) {
26+
let _ = writeln!(shell.err(), "\nCaused by:");
27+
28+
for line in cause.to_string().lines() {
29+
let _ = match line {
30+
"" => writeln!(shell.err()),
31+
line => writeln!(shell.err(), " {}", line),
32+
};
33+
}
34+
}
35+
36+
std::process::exit(1);
37+
}

xtask/src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use structopt::StructOpt as _;
2+
use xtask::{shell::Shell, Opt};
3+
4+
fn main() {
5+
let opt = Opt::from_args();
6+
let mut shell = Shell::new();
7+
if let Err(err) = xtask::run(opt, &mut shell) {
8+
xtask::exit_with_error(err, &mut shell);
9+
}
10+
}

xtask/src/shell.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use std::{
2+
fmt,
3+
io::{self, Write as _},
4+
};
5+
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor as _};
6+
7+
pub struct Shell {
8+
stderr: StandardStream,
9+
}
10+
11+
impl Shell {
12+
pub fn new() -> Self {
13+
Self {
14+
stderr: StandardStream::stderr(if atty::is(atty::Stream::Stderr) {
15+
ColorChoice::Auto
16+
} else {
17+
ColorChoice::Never
18+
}),
19+
}
20+
}
21+
22+
pub(crate) fn err(&mut self) -> &mut StandardStream {
23+
&mut self.stderr
24+
}
25+
26+
pub(crate) fn status(
27+
&mut self,
28+
status: impl fmt::Display,
29+
message: impl fmt::Display,
30+
) -> io::Result<()> {
31+
self.print(status, message, Color::Green, true)
32+
}
33+
34+
pub fn error(&mut self, message: impl fmt::Display) -> io::Result<()> {
35+
self.print("error", message, Color::Red, false)
36+
}
37+
38+
fn print(
39+
&mut self,
40+
status: impl fmt::Display,
41+
message: impl fmt::Display,
42+
color: Color,
43+
justified: bool,
44+
) -> io::Result<()> {
45+
self.stderr
46+
.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
47+
if justified {
48+
write!(self.stderr, "{:>12}", status)?;
49+
} else {
50+
write!(self.stderr, "{}", status)?;
51+
self.stderr.set_color(ColorSpec::new().set_bold(true))?;
52+
write!(self.stderr, ":")?;
53+
}
54+
self.stderr.reset()?;
55+
writeln!(self.stderr, " {}", message)
56+
}
57+
}
58+
59+
impl Default for Shell {
60+
fn default() -> Self {
61+
Self::new()
62+
}
63+
}

0 commit comments

Comments
 (0)