#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
#![recursion_limit = "256"]
mod cargo_udeps;
mod process;
mod ra_proc_macro;
mod rust;
mod rustfmt;
pub mod shell;
mod toolchain;
mod workspace;
use crate::{
ra_proc_macro::ProcMacroExpander,
rust::CodeEdit,
shell::Shell,
workspace::{MetadataExt as _, PackageExt as _, PackageIdExt as _, TargetExt as _},
};
use anyhow::Context as _;
use cargo_metadata as cm;
use indoc::indoc;
use itertools::{iproduct, Itertools as _};
use krates::PkgSpec;
use maplit::{btreeset, hashmap, hashset};
use petgraph::{
graph::{Graph, NodeIndex},
visit::Dfs,
};
use prettytable::{cell, format::FormatBuilder, row, Table};
use quote::quote;
use ra_ap_paths::{AbsPath, AbsPathBuf};
use ra_ap_proc_macro_srv as proc_macro_srv;
use std::{
cmp,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
path::{Path, PathBuf},
str::FromStr,
};
use structopt::{clap::AppSettings, StructOpt};
#[allow(clippy::large_enum_variant)]
#[derive(StructOpt, Debug)]
#[structopt(
author("Ryo Yamashita <qryxip@gmail.com>"),
about("Please run as `cargo equip`, not `cargo-equip`."),
bin_name("cargo"),
global_settings(&[AppSettings::DeriveDisplayOrder, AppSettings::UnifiedHelpMessage])
)]
pub enum Opt {
#[structopt(
about(indoc! {r#"
A Cargo subcommand to bundle your code into one `.rs` file for competitive programming.
Use -h for short descriptions and --help for more detials.
"#}),
author("Ryo Yamashita <qryxip@gmail.com>"),
usage(
r#"cargo equip [OPTIONS]
cargo equip [OPTIONS] --lib
cargo equip [OPTIONS] --bin <NAME>
cargo equip [OPTIONS] --example <NAME>
cargo equip [OPTIONS] --src <PATH>"#,
)
)]
Equip(OptEquip),
#[structopt(setting(AppSettings::Hidden))]
RustAnalyzerProcMacro {},
}
#[derive(StructOpt, Debug)]
pub struct OptEquip {
#[structopt(
long,
value_name("PATH"),
conflicts_with_all(&["lib", "bin", "example"]),
long_help(indoc! {r#"
Bundle the lib/bin/example target and its dependencies.
This option is intended to be used from editors such as VSCode. Use `--lib`, `--bin` or `--example` for normal usage.
"#})
)]
src: Option<PathBuf>,
#[structopt(long, conflicts_with_all(&["bin", "example"]))]
lib: bool,
#[structopt(long, value_name("NAME"), conflicts_with("example"))]
bin: Option<String>,
#[structopt(long, value_name("NAME"))]
example: Option<String>,
#[structopt(long, value_name("PATH"))]
manifest_path: Option<PathBuf>,
#[structopt(long, value_name("SPEC"))]
exclude: Vec<PkgSpec>,
#[structopt(
long,
long_help(Box::leak(
format!(
"Alias for:\n--exclude {}\n ",
ATCODER_CRATES.iter().format("\n "),
)
.into_boxed_str(),
))
)]
exclude_atcoder_crates: bool,
#[structopt(
long,
long_help(Box::leak(
format!(
"Alias for:\n--exclude {}\n ",
CODINGAME_CRATES.iter().format("\n "),
)
.into_boxed_str(),
))
)]
exclude_codingame_crates: bool,
#[structopt(
long,
value_name("DOMAIN_AND_USERNAME"),
long_help(
concat!(
indoc! {r#"
Do not include license and copyright notices for the users.
Supported formats:
* github.com/{username}
* gitlab.com/{username}
"#},
' ',
)
)
)]
mine: Vec<User>,
#[structopt(long, value_name("TOOLCHAIN"), default_value("nightly"))]
toolchain: String,
#[structopt(long, value_name("MODULE_PATH"), default_value("crate::__cargo_equip"))]
mod_path: CrateSinglePath,
#[structopt(
long,
value_name("REMOVE"),
possible_values(Remove::VARIANTS),
hide_possible_values(true),
long_help(concat!(
indoc! {r#"
Removes
* doc comments (`//! ..`, `/// ..`, `/** .. */`, `#[doc = ".."]`) with `--remove docs`.
* comments (`// ..`, `/* .. */`) with `--remove comments`.
```
#[allow(dead_code)]
pub mod a {
//! A.
/// A.
pub struct A; // aaaaa
}
```
↓
```
#[allow(dead_code)]
pub mod a {
pub struct A;
}
```
"#},
' ',
))
)]
remove: Vec<Remove>,
#[structopt(
long,
value_name("MINIFY"),
possible_values(Minify::VARIANTS),
hide_possible_values(true),
default_value("none"),
hide_default_value(true),
long_help(concat!(
indoc! {r#"
Minifies
- each expaned library with `--minify lib`.
- the whole code with `--minify all`.
Not that the minification function is incomplete. Unnecessary spaces may be inserted.
"#},
' ',
))
)]
minify: Minify,
#[structopt(long)]
no_resolve_cfgs: bool,
#[structopt(long)]
no_rustfmt: bool,
#[structopt(long)]
no_check: bool,
#[structopt(short, long, value_name("PATH"))]
output: Option<PathBuf>,
#[structopt(
long,
value_name("MINIFY"),
possible_values(Minify::VARIANTS),
default_value("none")
)]
oneline: Minify,
#[structopt(long, conflicts_with("no_resolve_cfgs"))]
resolve_cfgs: bool,
#[structopt(long, conflicts_with("no_rustfmt"))]
rustfmt: bool,
#[structopt(long, conflicts_with("no_check"))]
check: bool,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum User {
Github(String),
GitlabCom(String),
}
impl FromStr for User {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, &'static str> {
return if let Some(username) = s.strip_prefix("github.com/") {
Ok(Self::Github(username.to_owned()))
} else if let Some(username) = s.strip_prefix("gitlab.com/") {
Ok(Self::GitlabCom(username.to_owned()))
} else {
Err(MSG)
};
static MSG: &str = indoc! {r"
Supported formats:
* github.com/{username}
* gitlab.com/{username}
"};
}
}
#[derive(Debug, derive_more::Display)]
#[display(fmt = "crate::{}", _0)]
pub struct CrateSinglePath(syn::Ident);
impl FromStr for CrateSinglePath {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, &'static str> {
(|| {
let syn::Path {
leading_colon,
segments,
} = syn::parse_str(s).map_err(|_| ())?;
match (leading_colon, &*segments.into_iter().collect::<Vec<_>>()) {
(None, [p1, p2]) if p1.ident == "crate" => Ok(Self(p2.ident.clone())),
_ => Err(()),
}
})()
.map_err(|()| "expected `crate::$ident`")
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Remove {
Docs,
Comments,
}
impl Remove {
const VARIANTS: &'static [&'static str] = &["docs", "comments"];
}
impl FromStr for Remove {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, &'static str> {
match s {
"docs" => Ok(Self::Docs),
"comments" => Ok(Self::Comments),
_ => Err(r#"expected "docs", or "comments""#),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Minify {
None,
Libs,
All,
}
impl Minify {
const VARIANTS: &'static [&'static str] = &["none", "libs", "all"];
}
impl FromStr for Minify {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, &'static str> {
match s {
"none" => Ok(Self::None),
"libs" => Ok(Self::Libs),
"all" => Ok(Self::All),
_ => Err(r#"expected "none", "libs", or "all""#),
}
}
}
pub struct Context<'a> {
pub cwd: PathBuf,
pub cargo_equip_exe: AbsPathBuf,
pub cache_dir: PathBuf,
pub shell: &'a mut Shell,
}
static ATCODER_CRATES: &[&str] = &[
"https://github.com/rust-lang/crates.io-index#alga:0.9.3",
"https://github.com/rust-lang/crates.io-index#ascii:1.0.0",
"https://github.com/rust-lang/crates.io-index#bitset-fixed:0.1.0",
"https://github.com/rust-lang/crates.io-index#either:1.5.3",
"https://github.com/rust-lang/crates.io-index#fixedbitset:0.2.0",
"https://github.com/rust-lang/crates.io-index#getrandom:0.1.14",
"https://github.com/rust-lang/crates.io-index#im-rc:14.3.0",
"https://github.com/rust-lang/crates.io-index#indexmap:1.3.2",
"https://github.com/rust-lang/crates.io-index#itertools:0.9.0",
"https://github.com/rust-lang/crates.io-index#itertools-num:0.1.3",
"https://github.com/rust-lang/crates.io-index#lazy_static:1.4.0",
"https://github.com/rust-lang/crates.io-index#libm:0.2.1",
"https://github.com/rust-lang/crates.io-index#maplit:1.0.2",
"https://github.com/rust-lang/crates.io-index#nalgebra:0.20.0",
"https://github.com/rust-lang/crates.io-index#ndarray:0.13.0",
"https://github.com/rust-lang/crates.io-index#num:0.2.1",
"https://github.com/rust-lang/crates.io-index#num-bigint:0.2.6",
"https://github.com/rust-lang/crates.io-index#num-complex:0.2.4",
"https://github.com/rust-lang/crates.io-index#num-derive:0.3.0",
"https://github.com/rust-lang/crates.io-index#num-integer:0.1.42",
"https://github.com/rust-lang/crates.io-index#num-iter:0.1.40",
"https://github.com/rust-lang/crates.io-index#num-rational:0.2.4",
"https://github.com/rust-lang/crates.io-index#num-traits:0.2.11",
"https://github.com/rust-lang/crates.io-index#ordered-float:1.0.2",
"https://github.com/rust-lang/crates.io-index#permutohedron:0.2.4",
"https://github.com/rust-lang/crates.io-index#petgraph:0.5.0",
"https://github.com/rust-lang/crates.io-index#proconio:0.3.6",
"https://github.com/rust-lang/crates.io-index#proconio:0.3.7",
"https://github.com/rust-lang/crates.io-index#proconio:0.3.8",
"https://github.com/rust-lang/crates.io-index#rand:0.7.3",
"https://github.com/rust-lang/crates.io-index#rand_chacha:0.2.2",
"https://github.com/rust-lang/crates.io-index#rand_core:0.5.1",
"https://github.com/rust-lang/crates.io-index#rand_distr:0.2.2",
"https://github.com/rust-lang/crates.io-index#rand_hc:0.2.0",
"https://github.com/rust-lang/crates.io-index#rand_pcg:0.2.1",
"https://github.com/rust-lang/crates.io-index#regex:1.3.6",
"https://github.com/rust-lang/crates.io-index#rustc-hash:1.1.0",
"https://github.com/rust-lang/crates.io-index#smallvec:1.2.0",
"https://github.com/rust-lang/crates.io-index#superslice:1.0.0",
"https://github.com/rust-lang/crates.io-index#text_io:0.1.8",
"https://github.com/rust-lang/crates.io-index#whiteread:0.5.0",
];
static CODINGAME_CRATES: &[&str] = &[
"https://github.com/rust-lang/crates.io-index#chrono:0.4.19",
"https://github.com/rust-lang/crates.io-index#itertools:0.10.0",
"https://github.com/rust-lang/crates.io-index#libc:0.2.93",
"https://github.com/rust-lang/crates.io-index#rand:0.8.3",
"https://github.com/rust-lang/crates.io-index#regex:1.4.5",
"https://github.com/rust-lang/crates.io-index#time:0.2.26",
];
pub fn run(opt: Opt, ctx: Context<'_>) -> anyhow::Result<()> {
let opt = match opt {
Opt::Equip(opt) => opt,
Opt::RustAnalyzerProcMacro {} => return proc_macro_srv::cli::run().map_err(Into::into),
};
let OptEquip {
src,
lib,
bin,
example,
manifest_path,
exclude,
exclude_atcoder_crates,
exclude_codingame_crates,
mine,
toolchain,
mod_path: CrateSinglePath(cargo_equip_mod_name),
remove,
minify,
no_resolve_cfgs,
no_rustfmt,
no_check,
output,
oneline: deprecated_oneline_opt,
resolve_cfgs: deprecated_resolve_cfgs_flag,
rustfmt: deprecated_rustfmt_flag,
check: deprecated_check_flag,
} = opt;
let minify = match (minify, deprecated_oneline_opt) {
(Minify::None, oneline) => oneline,
(minify, _) => minify,
};
let exclude = {
let mut exclude = exclude;
if exclude_atcoder_crates {
exclude.extend(ATCODER_CRATES.iter().map(|s| s.parse().unwrap()));
}
if exclude_codingame_crates {
exclude.extend(CODINGAME_CRATES.iter().map(|s| s.parse().unwrap()));
}
exclude
};
let Context {
cwd,
cargo_equip_exe,
cache_dir,
shell,
} = ctx;
if deprecated_resolve_cfgs_flag {
shell.warn("`--resolve-cfgs` is deprecated. `#[cfg(..)]`s are resolved by default")?;
}
if deprecated_rustfmt_flag {
shell.warn("`--rustfmt` is deprecated. the output is formatted by default")?;
}
if deprecated_check_flag {
shell.warn("`--check` is deprecated. the output is checked by default")?;
}
let manifest_path = if let Some(manifest_path) = manifest_path {
cwd.join(manifest_path.strip_prefix(".").unwrap_or(&manifest_path))
} else {
workspace::locate_project(&cwd)?
};
let metadata = workspace::cargo_metadata(&manifest_path, &cwd)?;
let (root, root_package) = if lib {
metadata.lib_target()
} else if let Some(bin) = bin {
metadata.bin_target_by_name(&bin)
} else if let Some(example) = example {
metadata.example_target_by_name(&example)
} else if let Some(src) = src {
metadata.target_by_src_path(&cwd.join(src))
} else {
metadata.exactly_one_target()
}?;
let libs_to_bundle = {
let unused_deps = &if root.is_lib() {
hashset!()
} else {
match cargo_udeps::cargo_udeps(root_package, root, &toolchain, shell) {
Ok(unused_deps) => unused_deps,
Err(warning) => {
shell.warn(warning)?;
hashset!()
}
}
};
let mut libs_to_bundle =
metadata.libs_to_bundle(&root_package.id, root.is_example(), unused_deps, &exclude)?;
if root.is_lib() {
libs_to_bundle.insert(&root_package.id, (root, root.crate_name()));
}
libs_to_bundle
};
let error_message = |head: &str| {
let mut msg = head.to_owned();
msg += "\n\n";
msg += &libs_to_bundle
.iter()
.map(|(package_id, (_, pseudo_extern_crate_name))| {
format!(
"- `{}` as `crate::{}::crates::{}`\n",
package_id, cargo_equip_mod_name, pseudo_extern_crate_name,
)
})
.join("");
let crates_available_on_atcoder = iproduct!(libs_to_bundle.keys(), ATCODER_CRATES)
.filter(|(id, s)| s.parse::<PkgSpec>().unwrap().matches(&metadata[id]))
.map(|(id, _)| format!("- `{}`\n", id))
.join("");
if !crates_available_on_atcoder.is_empty() {
msg += &format!(
"\nnote: attempted to bundle with the following crate(s), which are available on \
AtCoder. to exclude them from bundling, run with `--exclude-atcoder-crates`\n\n{}",
crates_available_on_atcoder,
);
}
msg
};
let code = bundle(
&metadata,
if root.is_lib() {
RootCrate::Lib(root_package, root)
} else {
RootCrate::BinLike(root_package, root)
},
&libs_to_bundle,
&mine,
&cargo_equip_mod_name,
!no_resolve_cfgs,
&remove,
minify,
!no_rustfmt,
&cargo_equip_exe,
&cache_dir,
shell,
)
.with_context(|| error_message("could not bundle the code"))?;
if !no_check {
workspace::cargo_check_using_current_lockfile_and_cache(
&metadata,
root_package,
root,
&exclude,
&code,
)
.with_context(|| error_message("the bundled code was not valid"))?;
}
if let Some(output) = output {
let output = cwd.join(output);
cargo_util::paths::write(&output, code)
} else {
write!(shell.out(), "{}", code)?;
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
fn bundle(
metadata: &cm::Metadata,
root_crate: RootCrate<'_>,
libs_to_bundle: &BTreeMap<&cm::PackageId, (&cm::Target, String)>,
mine: &[User],
cargo_equip_mod_name: &syn::Ident,
resolve_cfgs: bool,
remove: &[Remove],
minify: Minify,
rustfmt: bool,
cargo_equip_exe: &AbsPath,
cache_dir: &Path,
shell: &mut Shell,
) -> anyhow::Result<String> {
let cargo_check_message_format_json = |toolchain: &str, shell: &mut Shell| -> _ {
let (package, krate) = root_crate.split();
workspace::cargo_check_message_format_json(toolchain, metadata, package, krate, shell)
};
let cargo_messages_for_out_dirs = &libs_to_bundle
.keys()
.any(|p| metadata[p].has_custom_build())
.then(|| {
let toolchain = &toolchain::active_toolchain(root_crate.package().manifest_dir())?;
cargo_check_message_format_json(toolchain, shell)
})
.unwrap_or_else(|| Ok(vec![]))?;
let has_proc_macro = libs_to_bundle.keys().any(|p| metadata[p].has_proc_macro());
let cargo_messages_for_proc_macro_dll_paths = &has_proc_macro
.then(|| {
let toolchain = toolchain::find_toolchain_compatible_with_ra(
root_crate.package().manifest_dir(),
shell,
)?;
let msgs = cargo_check_message_format_json(&toolchain, shell)?;
Ok::<_, anyhow::Error>(msgs)
})
.unwrap_or_else(|| Ok(vec![]))?;
let out_dirs = workspace::list_out_dirs(metadata, cargo_messages_for_out_dirs);
let proc_macro_crate_dylibs =
&ra_proc_macro::list_proc_macro_dylibs(cargo_messages_for_proc_macro_dll_paths, |p| {
libs_to_bundle.contains_key(p)
});
let macro_expander = has_proc_macro
.then(|| ProcMacroExpander::spawn(cargo_equip_exe, proc_macro_crate_dylibs))
.transpose()?;
let proc_macro_names = macro_expander
.as_ref()
.map(|macro_expander| {
let mut proc_macro_names = HashMap::<_, BTreeSet<_>>::new();
for (pkg, macro_names) in macro_expander.macro_names() {
for macro_name in macro_names {
proc_macro_names
.entry(pkg)
.or_default()
.insert(macro_name.to_owned());
}
}
proc_macro_names
})
.unwrap_or_default();
let resolve_nodes = metadata
.resolve
.as_ref()
.map(|cm::Resolve { nodes, .. }| &nodes[..])
.unwrap_or(&[])
.iter()
.map(|node| (&node.id, node))
.collect::<HashMap<_, _>>();
let mut code = if let Some((_, bin_target)) = root_crate.bin_like() {
let code = cargo_util::paths::read(bin_target.src_path.as_ref())?;
if rust::find_skip_attribute(&code)? {
shell.status("Found", "`#![cfg_attr(cargo_equip, cargo_equip::skip)]`")?;
return Ok(code);
}
code
} else {
"".to_owned()
};
shell.status("Bundling", "the code")?;
if let Some((bin_package, bin_target)) = root_crate.bin_like() {
code = rust::process_bin(
cargo_equip_mod_name,
&bin_target.src_path,
{ macro_expander }.as_mut(),
|extern_crate_name| {
metadata
.dep_lib_by_extern_crate_name(&bin_package.id, extern_crate_name)
.map(|_| extern_crate_name.to_owned())
},
|extern_crate_name| {
matches!(
metadata.dep_lib_by_extern_crate_name(&bin_package.id, extern_crate_name),
Some(lib_package) if libs_to_bundle.contains_key(&lib_package.id)
)
},
|| (bin_target.crate_name(), &bin_package.id.repr),
)?;
}
let libs = libs_to_bundle
.iter()
.map(|(pkg, (krate, pseudo_extern_crate_name))| {
let mut edit = CodeEdit::new(cargo_equip_mod_name, &krate.src_path, || {
(krate.crate_name(), &pkg.repr)
})?;
if let Some(out_dir) = out_dirs.get(pkg) {
edit.expand_includes(out_dir)?;
}
Ok((*pkg, (*krate, &**pseudo_extern_crate_name, edit)))
})
.collect::<anyhow::Result<BTreeMap<_, _>>>()?;
let (graph, indices) = normal_non_host_dep_graph(&resolve_nodes, libs_to_bundle);
let libs_using_proc_macros = {
let mut crates_using_proc_macros = BTreeMap::<_, HashSet<_>>::new();
for (pkg, names) in &proc_macro_names {
let (_, pseudo_extern_crate_name) = &libs_to_bundle[pkg];
for name in names {
crates_using_proc_macros
.entry(&**name)
.or_default()
.insert(&**pseudo_extern_crate_name);
}
}
for goal in libs_to_bundle.keys() {
if metadata[goal].has_proc_macro() {
let mut dfs = Dfs::new(&graph, indices[goal]);
while let Some(next) = dfs.next(&graph) {
let (_, pseudo_extern_crate_name) = &libs_to_bundle[graph[next]];
for name in &proc_macro_names[goal] {
crates_using_proc_macros
.entry(name)
.or_default()
.insert(pseudo_extern_crate_name);
}
}
}
}
crates_using_proc_macros
};
let libs_with_local_inner_macros = {
let mut libs_with_local_inner_macros = libs
.keys()
.map(|pkg| (*pkg, btreeset!()))
.collect::<HashMap<_, _>>();
for (goal, (_, pseudo_extern_crate_name, edit)) in &libs {
if edit.has_local_inner_macros_attr() {
libs_with_local_inner_macros
.get_mut(*goal)
.unwrap()
.insert(&**pseudo_extern_crate_name);
let mut dfs = Dfs::new(&graph, indices[goal]);
while let Some(next) = dfs.next(&graph) {
libs_with_local_inner_macros
.get_mut(graph[next])
.unwrap()
.insert(&**pseudo_extern_crate_name);
}
}
}
libs_with_local_inner_macros
};
let libs = libs
.into_iter()
.map(
|(lib_package, (lib_target, pseudo_extern_crate_name, mut edit))| {
let lib_package: &cm::Package = &metadata[lib_package];
if let Some(names) = proc_macro_names.get(&lib_package.id) {
debug_assert_eq!(["proc-macro".to_owned()], *lib_target.kind);
let names = names
.iter()
.map(|name| {
let rename = format!(
"{}_macro_def_{}_{}",
cargo_equip_mod_name, pseudo_extern_crate_name, name,
);
(name, rename)
})
.collect::<Vec<_>>();
let crate_mod_content = format!(
"pub use crate::{}::macros::{}::*;{}",
cargo_equip_mod_name,
pseudo_extern_crate_name,
names
.iter()
.map(|(name, rename)| {
let msg = format!(
"`{}` from `{} {}` should have been expanded",
name, lib_package.name, lib_package.version,
);
format!(
"#[macro_export]macro_rules!{}\
{{($(_:tt)*)=>(::std::compile_error!({});)}}",
rename,
quote!(#msg),
)
})
.join("")
);
let macro_mod_content = format!(
"pub use crate::{}{}{};",
if names.len() == 1 { " " } else { "{" },
names
.iter()
.map(|(name, rename)| format!("{} as {}", rename, name))
.format(","),
if names.len() == 1 { "" } else { "}" },
);
return Ok((
pseudo_extern_crate_name,
(
lib_package,
crate_mod_content,
macro_mod_content,
"".to_owned(),
),
));
}
let cm::Node { features, .. } = resolve_nodes[&lib_package.id];
let translate_extern_crate_name = |dst: &_| -> _ {
let dst_package =
metadata.dep_lib_by_extern_crate_name(&lib_package.id, dst)?;
let (_, dst_pseudo_extern_crate_name) =
libs_to_bundle.get(&dst_package.id).unwrap_or_else(|| {
panic!(
"missing `extern_crate_name` for `{}`. generated one should be \
given beforehead. this is a bug",
dst_package.id,
);
});
Some(dst_pseudo_extern_crate_name.clone())
};
edit.translate_crate_path(pseudo_extern_crate_name)?;
edit.translate_extern_crate_paths(translate_extern_crate_name)?;
edit.process_extern_crates_in_lib(translate_extern_crate_name, shell)?;
let macro_mod_content = edit.modify_declarative_macros(pseudo_extern_crate_name)?;
let prelude_mod_content = edit.resolve_pseudo_prelude(
pseudo_extern_crate_name,
&libs_with_local_inner_macros[&lib_package.id],
&{
metadata
.libs_with_extern_crate_names(
&lib_package.id,
&libs_to_bundle.keys().copied().collect(),
)?
.into_iter()
.map(|(package_id, extern_crate_name)| {
let (_, pseudo_extern_crate_name) =
libs_to_bundle.get(package_id).with_context(|| {
"could not translate pseudo extern crate names. this is a bug"
})?;
Ok((extern_crate_name, pseudo_extern_crate_name.clone()))
})
.collect::<anyhow::Result<_>>()?
},
)?;
if resolve_cfgs {
edit.resolve_cfgs(features)?;
}
if remove.contains(&Remove::Docs) {
edit.allow_missing_docs();
edit.erase_docs()?;
}
if remove.contains(&Remove::Comments) {
edit.erase_comments()?;
}
let crate_mod_content = edit.finish()?;
Ok((
pseudo_extern_crate_name,
(
lib_package,
crate_mod_content,
macro_mod_content,
prelude_mod_content,
),
))
},
)
.collect::<anyhow::Result<Vec<(&str, (&cm::Package, String, String, String))>>>()?;
if !libs.is_empty() {
if !root_crate.package().authors.is_empty() {
shell.warn(
"`package.authors` are no longer used to skip Copyright and License Notices",
)?;
shell.warn("instead, add `--mine github.com/{your username}` to the arguments")?;
}
code = rust::insert_prelude_for_main_crate(&code, cargo_equip_mod_name)?;
code =
rust::allow_unused_imports_for_seemingly_proc_macros(&code, |mod_name, item_name| {
matches!(
libs_using_proc_macros.get(item_name), Some(pseudo_extern_crate_names)
if pseudo_extern_crate_names.contains(mod_name)
)
})?;
let doc = &{
fn list_packages<'a>(
doc: &mut String,
title: &str,
cargo_equip_mod_name: &syn::Ident,
contents: impl Iterator<Item = (Option<&'a str>, &'a cm::Package)>,
) {
let mut table = Table::new();
*table.get_format() = FormatBuilder::new()
.column_separator(' ')
.borders(' ')
.build();
let contents = contents.collect::<Vec<_>>();
let any_from_local_filesystem = contents.iter().any(|(_, p)| p.source.is_none());
for (pseudo_extern_crate_name, package) in contents {
let mut row = row![format!("- `{}`", package.id.mask_path())];
if any_from_local_filesystem {
row.add_cell(if package.source.is_some() {
cell!("")
} else if let Some(repository) = &package.repository {
cell!(format!("published in {}", repository))
} else {
cell!("published in **missing**")
});
}
row.add_cell(if let Some(license) = &package.license {
cell!(format!("licensed under `{}`", license))
} else {
cell!("licensed under **missing**")
});
if let Some(pseudo_extern_crate_name) = pseudo_extern_crate_name {
row.add_cell(cell!(format!(
"as `crate::{}::crates::{}`",
cargo_equip_mod_name, pseudo_extern_crate_name,
)));
}
table.add_row(row);
}
if !table.is_empty() {
if !doc.is_empty() {
*doc += "\n";
}
*doc += &format!(" # {}\n\n", title);
for line in table.to_string().lines() {
*doc += line.trim_end();
*doc += "\n";
}
}
}
let mut doc = "".to_owned();
list_packages(
&mut doc,
"Bundled libraries",
cargo_equip_mod_name,
libs.iter()
.filter(|(_, (p, _, _, _))| p.has_lib())
.map(|(k, (p, _, _, _))| (Some(*k), *p)),
);
list_packages(
&mut doc,
"Procedural macros",
cargo_equip_mod_name,
libs.iter()
.filter(|(_, (p, _, _, _))| p.has_proc_macro())
.map(|(_, (p, _, _, _))| (None, *p)),
);
let notices = libs
.iter()
.filter(|(_, (p, _, _, _))| {
p.has_lib() && !metadata.workspace_members.contains(&p.id)
})
.map(|(_, (lib_package, _, _, _))| {
shell.status("Checking", format!("the license of `{}`", lib_package.id))?;
lib_package
.read_license_text(mine, cache_dir)
.map(|license_text| {
license_text.map(|license_text| (&lib_package.id, license_text))
})
})
.flat_map(Result::transpose)
.collect::<Result<Vec<_>, _>>()?;
if !notices.is_empty() {
doc += "\n # License and Copyright Notices\n";
for (package_id, license_text) in notices {
doc += &format!("\n - `{}`\n\n", package_id.mask_path());
let backquotes = {
let (mut n, mut m) = (2, None);
for c in license_text.chars() {
if c == '`' {
m = Some(m.unwrap_or(0) + 1);
} else if let Some(m) = m.take() {
n = cmp::max(n, m);
}
}
"`".repeat(cmp::max(n, m.unwrap_or(0)) + 1)
};
doc += &format!(" {}text\n", backquotes);
for line in license_text.lines() {
match line {
"" => doc += "\n",
line => doc += &format!(" {}\n", line),
}
}
doc += &format!(" {}\n", backquotes);
}
}
doc
};
code += "\n";
code += &match root_crate {
RootCrate::BinLike(..) => {
"// The following code was expanded by `cargo-equip`.\n".to_owned()
}
RootCrate::Lib(..) => format!("use {}::prelude::*;\n", cargo_equip_mod_name),
};
code += "\n";
let crate_mods = libs
.iter()
.map(|(name, (_, content, _, _))| (*name, &**content))
.collect::<Vec<_>>();
let macro_mods = libs
.iter()
.map(|(name, (_, _, content, _))| (*name, &**content))
.collect::<Vec<_>>();
let prelude_mods = libs
.iter()
.map(|(name, (_, _, _, content))| (*name, &**content))
.collect::<Vec<_>>();
let render_mods = |code: &mut String, mods: &[(&str, &str)]| -> anyhow::Result<()> {
if minify == Minify::Libs {
for (pseudo_extern_crate_name, mod_content) in mods {
*code += " pub mod ";
*code += pseudo_extern_crate_name;
*code += " {";
*code += &rustminify::minify_file(&rust::parse_file(mod_content)?);
*code += "}\n";
}
} else {
for (i, (pseudo_extern_crate_name, mod_content)) in mods.iter().enumerate() {
if i > 0 {
*code += "\n";
}
*code += " pub mod ";
*code += pseudo_extern_crate_name;
*code += " {\n";
*code += &rust::indent_code(mod_content, 3);
*code += " }\n";
}
}
Ok(())
};
for doc in doc.lines() {
code += "///";
if !code.is_empty() {
code += " ";
}
code += doc;
code += "\n";
}
if minify == Minify::Libs {
code += "#[cfg_attr(any(), rustfmt::skip)]\n";
}
code += "#[allow(unused)]\n";
code += &format!("mod {} {{\n", cargo_equip_mod_name);
code += " pub(crate) mod crates {\n";
render_mods(&mut code, &crate_mods)?;
code += " }\n";
code += "\n";
code += " pub(crate) mod macros {\n";
render_mods(&mut code, ¯o_mods)?;
code += " }\n";
code += "\n";
code += " pub(crate) mod prelude {";
match root_crate {
RootCrate::BinLike(..) => {
let prelude_for_main = {
let local_macro_uses_in_main_crate = libs_with_local_inner_macros
.values()
.flatten()
.unique()
.sorted()
.map(|name| format!("{}::*", name))
.collect::<Vec<_>>();
let local_macro_uses_in_main_crate = match &*local_macro_uses_in_main_crate {
[] => None,
[part] => Some(part.clone()),
parts => Some(format!("{{{}}}", parts.iter().format(","))),
};
format!(
"pub use crate::{}::{};",
cargo_equip_mod_name,
if let Some(local_macro_uses_in_main_crate) = local_macro_uses_in_main_crate
{
format!("{{crates::*,macros::{}}}", local_macro_uses_in_main_crate)
} else {
"crates::*".to_owned()
}
)
};
code += &if minify == Minify::Libs {
prelude_for_main
} else {
format!("\n {}\n ", prelude_for_main)
};
}
RootCrate::Lib(_, krate) => {
code += &format!("pub use crate::{}::crates::", cargo_equip_mod_name);
code += &krate.crate_name();
code += ";";
}
}
code += "}\n";
code += "\n";
code += " mod preludes {\n";
render_mods(&mut code, &prelude_mods)?;
code += " }\n";
code += "}\n";
}
if minify == Minify::All {
code = rustminify::minify_file(&rust::parse_file(&code)?);
}
if rustfmt {
code = rustfmt::rustfmt(
&metadata.workspace_root,
&code,
&root_crate.package().edition,
)?;
}
Ok(code)
}
fn normal_non_host_dep_graph<'cm>(
resolve_nodes: &HashMap<&'cm cm::PackageId, &cm::Node>,
libs_to_bundle: &BTreeMap<&'cm cm::PackageId, (&cm::Target, String)>,
) -> (
Graph<&'cm cm::PackageId, ()>,
HashMap<&'cm cm::PackageId, NodeIndex>,
) {
let mut graph = Graph::new();
let mut indices = hashmap!();
for pkg in libs_to_bundle.keys() {
indices.insert(*pkg, graph.add_node(*pkg));
}
for (from_pkg, (from_crate, _)) in libs_to_bundle {
if from_crate.is_lib() {
for cm::NodeDep {
pkg: to, dep_kinds, ..
} in &resolve_nodes[from_pkg].deps
{
if *from_pkg != to
&& dep_kinds
.iter()
.any(|cm::DepKindInfo { kind, .. }| *kind == cm::DependencyKind::Normal)
&& libs_to_bundle.contains_key(to)
{
graph.add_edge(indices[to], indices[*from_pkg], ());
}
}
}
}
(graph, indices)
}
#[derive(Clone, Copy)]
enum RootCrate<'cm> {
BinLike(&'cm cm::Package, &'cm cm::Target),
Lib(&'cm cm::Package, &'cm cm::Target),
}
impl<'cm> RootCrate<'cm> {
fn split(self) -> (&'cm cm::Package, &'cm cm::Target) {
match self {
RootCrate::BinLike(p, t) | RootCrate::Lib(p, t) => (p, t),
}
}
fn package(self) -> &'cm cm::Package {
match self {
RootCrate::BinLike(p, _) | RootCrate::Lib(p, _) => p,
}
}
fn bin_like(self) -> Option<(&'cm cm::Package, &'cm cm::Target)> {
match self {
RootCrate::BinLike(p, t) => Some((p, t)),
RootCrate::Lib(..) => None,
}
}
}