1#![forbid(unsafe_code)]
2#![warn(rust_2018_idioms)]
3#![recursion_limit = "256"]
4
5mod cargo_udeps;
6mod process;
7mod ra_proc_macro;
8mod rust;
9mod rustfmt;
10pub mod shell;
11mod toolchain;
12mod workspace;
13
14use crate::{
15 ra_proc_macro::ProcMacroExpander,
16 rust::CodeEdit,
17 shell::Shell,
18 workspace::{
19 Edition, MetadataExt as _, PackageExt as _, PackageIdExt as _, ResolveBehavior,
20 TargetExt as _,
21 },
22};
23use anyhow::Context as _;
24use cargo_metadata as cm;
25use indoc::indoc;
26use itertools::{iproduct, Itertools as _};
27use krates::PkgSpec;
28use maplit::{btreeset, hashmap, hashset};
29use petgraph::{
30 graph::{Graph, NodeIndex},
31 visit::Dfs,
32};
33use prettytable::{cell, format::FormatBuilder, row, Table};
34use quote::quote;
35use std::{
36 cmp,
37 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
38 fmt::Debug,
39 path::{Path, PathBuf},
40 rc::Rc,
41 str::FromStr,
42};
43use structopt::{clap::AppSettings, StructOpt};
44
45#[allow(clippy::large_enum_variant)]
49#[derive(StructOpt, Debug)]
50#[structopt(
51 author("Ryo Yamashita <qryxip@gmail.com>"),
52 about("Please run as `cargo equip`, not `cargo-equip`."),
53 bin_name("cargo"),
54 global_settings(&[AppSettings::DeriveDisplayOrder, AppSettings::UnifiedHelpMessage])
55)]
56pub enum Opt {
57 #[structopt(
58 about(indoc! {r#"
59
60 A Cargo subcommand to bundle your code into one `.rs` file for competitive programming.
61
62 Use -h for short descriptions and --help for more detials.
63 "#}),
64 author("Ryo Yamashita <qryxip@gmail.com>"),
65 usage(
66 r#"cargo equip [OPTIONS]
67 cargo equip [OPTIONS] --lib
68 cargo equip [OPTIONS] --bin <NAME>
69 cargo equip [OPTIONS] --example <NAME>
70 cargo equip [OPTIONS] --src <PATH>"#,
71 )
72 )]
73 Equip(OptEquip),
74}
75
76#[derive(StructOpt, Debug)]
77pub struct OptEquip {
78 #[structopt(
80 long,
81 value_name("PATH"),
82 conflicts_with_all(&["lib", "bin", "example"]),
83 long_help(indoc! {r#"
84 Bundle the lib/bin/example target and its dependencies.
85
86 This option is intended to be used from editors such as VSCode. Use `--lib`, `--bin` or `--example` for normal usage.
87 "#})
88 )]
89 src: Option<PathBuf>,
90
91 #[structopt(long, conflicts_with_all(&["bin", "example"]))]
93 lib: bool,
94
95 #[structopt(long, value_name("NAME"), conflicts_with("example"))]
97 bin: Option<String>,
98
99 #[structopt(long, value_name("NAME"))]
101 example: Option<String>,
102
103 #[structopt(long, value_name("PATH"))]
105 manifest_path: Option<PathBuf>,
106
107 #[structopt(long, value_name("SPEC"))]
109 exclude: Vec<PkgSpec>,
110
111 #[structopt(
113 long,
114 long_help(Box::leak(
115 format!(
116 "Alias for:\n--exclude {}\n ",
117 ATCODER_CRATES.iter().format("\n "),
118 )
119 .into_boxed_str(),
120 ))
121 )]
122 exclude_atcoder_crates: bool,
123
124 #[structopt(
126 long,
127 long_help(Box::leak(
128 format!(
129 "Alias for:\n--exclude {}\n ",
130 ATCODER_202301_CRATES.iter().format("\n "),
131 )
132 .into_boxed_str(),
133 ))
134 )]
135 exclude_atcoder_202301_crates: bool,
136
137 #[structopt(
139 long,
140 long_help(Box::leak(
141 format!(
142 "Alias for:\n--exclude {}\n ",
143 CODINGAME_CRATES.iter().format("\n "),
144 )
145 .into_boxed_str(),
146 ))
147 )]
148 exclude_codingame_crates: bool,
149
150 #[structopt(
152 long,
153 value_name("DOMAIN_AND_USERNAME"),
154 long_help(
155 concat!(
156 indoc! {r#"
157 Do not include license and copyright notices for the users.
158
159 Supported formats:
160 * github.com/{username}
161 * gitlab.com/{username}
162 "#},
163 ' ',
164 )
165 )
166 )]
167 mine: Vec<User>,
168
169 #[structopt(long, value_name("TOOLCHAIN"), conflicts_with("toolchain_for_udeps"))]
171 toolchain: Option<String>,
172
173 #[structopt(long, value_name("TOOLCHAIN"), default_value("nightly"))]
175 toolchain_for_udeps: String,
176
177 #[structopt(long, value_name("TOOLCHAIN"))]
179 toolchain_for_proc_macro_srv: Option<String>,
180
181 #[structopt(long, value_name("MODULE_PATH"), default_value("crate::__cargo_equip"))]
183 mod_path: CrateSinglePath,
184
185 #[structopt(
187 long,
188 value_name("REMOVE"),
189 possible_values(Remove::VARIANTS),
190 hide_possible_values(true),
191 long_help(concat!(
192 indoc! {r#"
193 Removes
194 * doc comments (`//! ..`, `/// ..`, `/** .. */`, `#[doc = ".."]`) with `--remove docs`.
195 * comments (`// ..`, `/* .. */`) with `--remove comments`.
196
197 ```
198 #[allow(dead_code)]
199 pub mod a {
200 //! A.
201
202 /// A.
203 pub struct A; // aaaaa
204 }
205 ```
206
207 ↓
208
209 ```
210 #[allow(dead_code)]
211 pub mod a {
212 pub struct A;
213 }
214 ```
215 "#},
216 ' ',
217 ))
218 )]
219 remove: Vec<Remove>,
220
221 #[structopt(
223 long,
224 value_name("MINIFY"),
225 possible_values(Minify::VARIANTS),
226 hide_possible_values(true),
227 default_value("none"),
228 hide_default_value(true),
229 long_help(concat!(
230 indoc! {r#"
231 Minifies
232 - each expaned library with `--minify lib`.
233 - the whole code with `--minify all`.
234
235 Not that the minification function is incomplete. Unnecessary spaces may be inserted.
236 "#},
237 ' ',
238 ))
239 )]
240 minify: Minify,
241
242 #[structopt(long)]
244 no_resolve_cfgs: bool,
245
246 #[structopt(long)]
248 no_rustfmt: bool,
249
250 #[structopt(long)]
252 no_check: bool,
253
254 #[structopt(short, long, value_name("PATH"))]
256 output: Option<PathBuf>,
257
258 #[structopt(
260 long,
261 value_name("MINIFY"),
262 possible_values(Minify::VARIANTS),
263 default_value("none")
264 )]
265 oneline: Minify,
266
267 #[structopt(long, conflicts_with("no_resolve_cfgs"))]
269 resolve_cfgs: bool,
270
271 #[structopt(long, conflicts_with("no_rustfmt"))]
273 rustfmt: bool,
274
275 #[structopt(long, conflicts_with("no_check"))]
277 check: bool,
278}
279
280#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
281pub enum User {
282 Github(String),
283 GitlabCom(String),
284}
285
286impl FromStr for User {
287 type Err = &'static str;
288
289 fn from_str(s: &str) -> Result<Self, &'static str> {
290 return if let Some(username) = s.strip_prefix("github.com/") {
291 Ok(Self::Github(username.to_owned()))
292 } else if let Some(username) = s.strip_prefix("gitlab.com/") {
293 Ok(Self::GitlabCom(username.to_owned()))
294 } else {
295 Err(MSG)
296 };
297
298 static MSG: &str = indoc! {r"
299 Supported formats:
300 * github.com/{username}
301 * gitlab.com/{username}
302 "};
303 }
304}
305
306#[derive(Debug, derive_more::Display)]
307#[display(fmt = "crate::{}", _0)]
308pub struct CrateSinglePath(syn::Ident);
309
310impl FromStr for CrateSinglePath {
311 type Err = &'static str;
312
313 fn from_str(s: &str) -> Result<Self, &'static str> {
314 (|| {
315 let syn::Path {
316 leading_colon,
317 segments,
318 } = syn::parse_str(s).map_err(|_| ())?;
319 match (leading_colon, &*segments.into_iter().collect::<Vec<_>>()) {
320 (None, [p1, p2]) if p1.ident == "crate" => Ok(Self(p2.ident.clone())),
321 _ => Err(()),
322 }
323 })()
324 .map_err(|()| "expected `crate::$ident`")
325 }
326}
327
328#[derive(Clone, Copy, PartialEq, Debug)]
329pub enum Remove {
330 Docs,
331 Comments,
332}
333
334impl Remove {
335 const VARIANTS: &'static [&'static str] = &["docs", "comments"];
336}
337
338impl FromStr for Remove {
339 type Err = &'static str;
340
341 fn from_str(s: &str) -> Result<Self, &'static str> {
342 match s {
343 "docs" => Ok(Self::Docs),
344 "comments" => Ok(Self::Comments),
345 _ => Err(r#"expected "docs", or "comments""#),
346 }
347 }
348}
349
350#[derive(Clone, Copy, PartialEq, Debug)]
351pub enum Minify {
352 None,
353 Libs,
354 All,
355}
356
357impl Minify {
358 const VARIANTS: &'static [&'static str] = &["none", "libs", "all"];
359}
360
361impl FromStr for Minify {
362 type Err = &'static str;
363
364 fn from_str(s: &str) -> Result<Self, &'static str> {
365 match s {
366 "none" => Ok(Self::None),
367 "libs" => Ok(Self::Libs),
368 "all" => Ok(Self::All),
369 _ => Err(r#"expected "none", "libs", or "all""#),
370 }
371 }
372}
373
374pub struct Context<'a> {
375 pub cwd: PathBuf,
376 pub cache_dir: PathBuf,
377 pub shell: &'a mut Shell,
378}
379
380static ATCODER_CRATES: &[&str] = &[
381 "https://github.com/rust-lang/crates.io-index#alga:0.9.3",
382 "https://github.com/rust-lang/crates.io-index#ascii:1.0.0",
383 "https://github.com/rust-lang/crates.io-index#bitset-fixed:0.1.0",
384 "https://github.com/rust-lang/crates.io-index#either:1.5.3",
385 "https://github.com/rust-lang/crates.io-index#fixedbitset:0.2.0",
386 "https://github.com/rust-lang/crates.io-index#getrandom:0.1.14",
387 "https://github.com/rust-lang/crates.io-index#im-rc:14.3.0",
388 "https://github.com/rust-lang/crates.io-index#indexmap:1.3.2",
389 "https://github.com/rust-lang/crates.io-index#itertools:0.9.0",
390 "https://github.com/rust-lang/crates.io-index#itertools-num:0.1.3",
391 "https://github.com/rust-lang/crates.io-index#lazy_static:1.4.0",
392 "https://github.com/rust-lang/crates.io-index#libm:0.2.1",
393 "https://github.com/rust-lang/crates.io-index#maplit:1.0.2",
394 "https://github.com/rust-lang/crates.io-index#nalgebra:0.20.0",
395 "https://github.com/rust-lang/crates.io-index#ndarray:0.13.0",
396 "https://github.com/rust-lang/crates.io-index#num:0.2.1",
397 "https://github.com/rust-lang/crates.io-index#num-bigint:0.2.6",
398 "https://github.com/rust-lang/crates.io-index#num-complex:0.2.4",
399 "https://github.com/rust-lang/crates.io-index#num-derive:0.3.0",
400 "https://github.com/rust-lang/crates.io-index#num-integer:0.1.42",
401 "https://github.com/rust-lang/crates.io-index#num-iter:0.1.40",
402 "https://github.com/rust-lang/crates.io-index#num-rational:0.2.4",
403 "https://github.com/rust-lang/crates.io-index#num-traits:0.2.11",
404 "https://github.com/rust-lang/crates.io-index#ordered-float:1.0.2",
405 "https://github.com/rust-lang/crates.io-index#permutohedron:0.2.4",
406 "https://github.com/rust-lang/crates.io-index#petgraph:0.5.0",
407 "https://github.com/rust-lang/crates.io-index#proconio:0.3.6",
408 "https://github.com/rust-lang/crates.io-index#proconio:0.3.7",
409 "https://github.com/rust-lang/crates.io-index#proconio:0.3.8",
410 "https://github.com/rust-lang/crates.io-index#rand:0.7.3",
411 "https://github.com/rust-lang/crates.io-index#rand_chacha:0.2.2",
412 "https://github.com/rust-lang/crates.io-index#rand_core:0.5.1",
413 "https://github.com/rust-lang/crates.io-index#rand_distr:0.2.2",
414 "https://github.com/rust-lang/crates.io-index#rand_hc:0.2.0",
415 "https://github.com/rust-lang/crates.io-index#rand_pcg:0.2.1",
416 "https://github.com/rust-lang/crates.io-index#regex:1.3.6",
417 "https://github.com/rust-lang/crates.io-index#rustc-hash:1.1.0",
418 "https://github.com/rust-lang/crates.io-index#smallvec:1.2.0",
419 "https://github.com/rust-lang/crates.io-index#superslice:1.0.0",
420 "https://github.com/rust-lang/crates.io-index#text_io:0.1.8",
421 "https://github.com/rust-lang/crates.io-index#whiteread:0.5.0",
422];
423
424static ATCODER_202301_CRATES: &[&str] = &[
425 "https://github.com/rust-lang/crates.io-index#ac-library-rs:0.1.1",
426 "https://github.com/rust-lang/crates.io-index#once_cell:1.18.0",
427 "https://github.com/rust-lang/crates.io-index#static_assertions:1.1.0",
428 "https://github.com/rust-lang/crates.io-index#varisat:0.2.2",
429 "https://github.com/rust-lang/crates.io-index#memoise:0.3.2",
430 "https://github.com/rust-lang/crates.io-index#argio:0.2.0",
431 "https://github.com/rust-lang/crates.io-index#bitvec:1.0.1",
432 "https://github.com/rust-lang/crates.io-index#counter:0.5.7",
433 "https://github.com/rust-lang/crates.io-index#hashbag:0.1.11",
434 "https://github.com/rust-lang/crates.io-index#pathfinding:4.3.0",
435 "https://github.com/rust-lang/crates.io-index#recur-fn:2.2.0",
436 "https://github.com/rust-lang/crates.io-index#indexing:0.4.1",
437 "https://github.com/rust-lang/crates.io-index#amplify:3.14.2",
438 "https://github.com/rust-lang/crates.io-index#amplify_derive:2.11.3",
439 "https://github.com/rust-lang/crates.io-index#amplify_num:0.4.1",
440 "https://github.com/rust-lang/crates.io-index#easy-ext:1.0.1",
441 "https://github.com/rust-lang/crates.io-index#multimap:0.9.0",
442 "https://github.com/rust-lang/crates.io-index#btreemultimap:0.1.1",
443 "https://github.com/rust-lang/crates.io-index#bstr:1.6.0",
444 "https://github.com/rust-lang/crates.io-index#az:1.2.1",
445 "https://github.com/rust-lang/crates.io-index#glidesort:0.1.2",
446 "https://github.com/rust-lang/crates.io-index#tap:1.0.1",
447 "https://github.com/rust-lang/crates.io-index#omniswap:0.1.0",
448 "https://github.com/rust-lang/crates.io-index#multiversion:0.7.2",
449 "https://github.com/rust-lang/crates.io-index#num:0.4.1",
450 "https://github.com/rust-lang/crates.io-index#num-bigint:0.4.3",
451 "https://github.com/rust-lang/crates.io-index#num-complex:0.4.3",
452 "https://github.com/rust-lang/crates.io-index#num-integer:0.1.45",
453 "https://github.com/rust-lang/crates.io-index#num-iter:0.1.43",
454 "https://github.com/rust-lang/crates.io-index#num-rational:0.4.1",
455 "https://github.com/rust-lang/crates.io-index#num-traits:0.2.15",
456 "https://github.com/rust-lang/crates.io-index#num-derive:0.4.0",
457 "https://github.com/rust-lang/crates.io-index#ndarray:0.15.6",
458 "https://github.com/rust-lang/crates.io-index#nalgebra:0.32.3",
459 "https://github.com/rust-lang/crates.io-index#alga:0.9.3",
460 "https://github.com/rust-lang/crates.io-index#libm:0.2.7",
461 "https://github.com/rust-lang/crates.io-index#rand:0.8.5",
462 "https://github.com/rust-lang/crates.io-index#getrandom:0.2.10",
463 "https://github.com/rust-lang/crates.io-index#rand_chacha:0.3.1",
464 "https://github.com/rust-lang/crates.io-index#rand_core:0.6.4",
465 "https://github.com/rust-lang/crates.io-index#rand_hc:0.3.2",
466 "https://github.com/rust-lang/crates.io-index#rand_pcg:0.3.1",
467 "https://github.com/rust-lang/crates.io-index#rand_distr:0.4.3",
468 "https://github.com/rust-lang/crates.io-index#petgraph:0.6.3",
469 "https://github.com/rust-lang/crates.io-index#indexmap:2.0.0",
470 "https://github.com/rust-lang/crates.io-index#regex:1.9.1",
471 "https://github.com/rust-lang/crates.io-index#lazy_static:1.4.0",
472 "https://github.com/rust-lang/crates.io-index#ordered-float:3.7.0",
473 "https://github.com/rust-lang/crates.io-index#ascii:1.1.0",
474 "https://github.com/rust-lang/crates.io-index#permutohedron:0.2.4",
475 "https://github.com/rust-lang/crates.io-index#superslice:1.0.0",
476 "https://github.com/rust-lang/crates.io-index#itertools:0.11.0",
477 "https://github.com/rust-lang/crates.io-index#itertools-num:0.1.3",
478 "https://github.com/rust-lang/crates.io-index#maplit:1.0.2",
479 "https://github.com/rust-lang/crates.io-index#either:1.8.1",
480 "https://github.com/rust-lang/crates.io-index#im-rc:15.1.0",
481 "https://github.com/rust-lang/crates.io-index#fixedbitset:0.4.2",
482 "https://github.com/rust-lang/crates.io-index#bitset-fixed:0.1.0",
483 "https://github.com/rust-lang/crates.io-index#proconio:0.4.5",
484 "https://github.com/rust-lang/crates.io-index#text_io:0.1.12",
485 "https://github.com/rust-lang/crates.io-index#rustc-hash:1.1.0",
486 "https://github.com/rust-lang/crates.io-index#smallvec:1.11.0",
487];
488
489static CODINGAME_CRATES: &[&str] = &[
490 "https://github.com/rust-lang/crates.io-index#chrono:0.4.19",
491 "https://github.com/rust-lang/crates.io-index#itertools:0.10.0",
492 "https://github.com/rust-lang/crates.io-index#libc:0.2.93",
493 "https://github.com/rust-lang/crates.io-index#rand:0.8.3",
494 "https://github.com/rust-lang/crates.io-index#regex:1.4.5",
495 "https://github.com/rust-lang/crates.io-index#time:0.2.26",
496];
497
498pub fn run(opt: Opt, ctx: Context<'_>) -> anyhow::Result<()> {
499 let Opt::Equip(OptEquip {
500 src,
501 lib,
502 bin,
503 example,
504 manifest_path,
505 exclude,
506 exclude_atcoder_crates,
507 exclude_atcoder_202301_crates,
508 exclude_codingame_crates,
509 mine,
510 toolchain: deprecated_toolchain_opt,
511 toolchain_for_udeps,
512 toolchain_for_proc_macro_srv,
513 mod_path: CrateSinglePath(cargo_equip_mod_name),
514 remove,
515 minify,
516 no_resolve_cfgs,
517 no_rustfmt,
518 no_check,
519 output,
520 oneline: deprecated_oneline_opt,
521 resolve_cfgs: deprecated_resolve_cfgs_flag,
522 rustfmt: deprecated_rustfmt_flag,
523 check: deprecated_check_flag,
524 }) = opt;
525
526 let minify = match (minify, deprecated_oneline_opt) {
527 (Minify::None, oneline) => oneline,
528 (minify, _) => minify,
529 };
530
531 let exclude = {
532 let mut exclude = exclude;
533 if exclude_atcoder_crates {
534 exclude.extend(ATCODER_CRATES.iter().map(|s| s.parse().unwrap()));
535 }
536 if exclude_atcoder_202301_crates {
537 exclude.extend(ATCODER_202301_CRATES.iter().map(|s| s.parse().unwrap()));
538 }
539 if exclude_codingame_crates {
540 exclude.extend(CODINGAME_CRATES.iter().map(|s| s.parse().unwrap()));
541 }
542 exclude
543 };
544
545 let toolchain_for_udeps = deprecated_toolchain_opt
546 .as_ref()
547 .unwrap_or(&toolchain_for_udeps);
548
549 let Context {
550 cwd,
551 cache_dir,
552 shell,
553 } = ctx;
554
555 if deprecated_toolchain_opt.is_some() {
556 shell.warn("`--toolchain` was renamed to `--toolchain-for-udeps`")?;
557 }
558 if deprecated_resolve_cfgs_flag {
559 shell.warn("`--resolve-cfgs` is deprecated. `#[cfg(..)]`s are resolved by default")?;
560 }
561 if deprecated_rustfmt_flag {
562 shell.warn("`--rustfmt` is deprecated. the output is formatted by default")?;
563 }
564 if deprecated_check_flag {
565 shell.warn("`--check` is deprecated. the output is checked by default")?;
566 }
567
568 let manifest_path = if let Some(manifest_path) = manifest_path {
569 cwd.join(manifest_path.strip_prefix(".").unwrap_or(&manifest_path))
570 } else {
571 workspace::locate_project(&cwd)?
572 };
573
574 let metadata = workspace::cargo_metadata(&manifest_path, &cwd)?;
575
576 let (root, root_package) = if lib {
577 metadata.lib_target()
578 } else if let Some(bin) = bin {
579 metadata.bin_target_by_name(&bin)
580 } else if let Some(example) = example {
581 metadata.example_target_by_name(&example)
582 } else if let Some(src) = src {
583 metadata.target_by_src_path(&cwd.join(src))
584 } else {
585 metadata.exactly_one_target()
586 }?;
587
588 if root_package.edition() == Edition::Edition2015 {
589 shell.warn("Rust 2015 is unsupported")?;
590 }
591 let resolve_behavior = workspace::resolve_behavior(root_package, &metadata.workspace_root)?;
592 if resolve_behavior >= ResolveBehavior::V2 {
593 shell.warn(
594 "Currently cargo-equip only support Feature Resovler v1, and may go search more crates \
595 than real Cargo does. Please watch https://github.com/qryxip/cargo-equip/issues/94",
596 )?;
597 }
598
599 let libs_to_bundle = {
600 let unused_deps = &if root.is_lib() {
601 hashset!()
602 } else {
603 match cargo_udeps::cargo_udeps(root_package, root, toolchain_for_udeps, shell) {
604 Ok(unused_deps) => unused_deps,
605 Err(warning) => {
606 shell.warn(warning)?;
607 hashset!()
608 }
609 }
610 };
611 let mut libs_to_bundle =
612 metadata.libs_to_bundle(&root_package.id, root.is_example(), unused_deps, &exclude)?;
613 if root.is_lib() {
614 libs_to_bundle.insert(&root_package.id, (root, root.crate_name()));
615 }
616 libs_to_bundle
617 };
618
619 let error_message = |head: &str| {
620 let mut msg = head.to_owned();
621
622 msg += "\n\n";
623 msg += &libs_to_bundle
624 .iter()
625 .map(|(package_id, (_, pseudo_extern_crate_name))| {
626 format!(
627 "- `{}` as `crate::{}::crates::{}`\n",
628 package_id, cargo_equip_mod_name, pseudo_extern_crate_name,
629 )
630 })
631 .join("");
632
633 let crates_available_on_atcoder = iproduct!(libs_to_bundle.keys(), ATCODER_CRATES)
634 .filter(|(id, s)| s.parse::<PkgSpec>().unwrap().matches(&metadata[id]))
635 .map(|(id, _)| format!("- `{}`\n", id))
636 .join("");
637
638 if !crates_available_on_atcoder.is_empty() {
639 msg += &format!(
640 "\nnote: attempted to bundle with the following crate(s), which are available on \
641 AtCoder. to exclude them from bundling, run with `--exclude-atcoder-crates`\n\n{}",
642 crates_available_on_atcoder,
643 );
644 }
645
646 msg
647 };
648
649 let code = bundle(
650 &metadata,
651 if root.is_lib() {
652 RootCrate::Lib(root_package, root)
653 } else {
654 RootCrate::BinLike(root_package, root)
655 },
656 &libs_to_bundle,
657 &mine,
658 &cargo_equip_mod_name,
659 !no_resolve_cfgs,
660 &remove,
661 minify,
662 !no_rustfmt,
663 toolchain_for_proc_macro_srv.as_deref(),
664 &cache_dir,
665 shell,
666 )
667 .with_context(|| error_message("could not bundle the code"))?;
668
669 if !no_check {
670 workspace::cargo_check_using_current_lockfile_and_cache(
671 &metadata,
672 root_package,
673 root,
674 &exclude,
675 &code,
676 )
677 .with_context(|| error_message("the bundled code was not valid"))?;
678 }
679
680 if let Some(output) = output {
681 let output = cwd.join(output);
682 cargo_util::paths::write(output, code)
683 } else {
684 write!(shell.out(), "{}", code)?;
685 Ok(())
686 }
687}
688
689#[allow(clippy::too_many_arguments)]
690fn bundle(
691 metadata: &cm::Metadata,
692 root_crate: RootCrate<'_>,
693 libs_to_bundle: &BTreeMap<&cm::PackageId, (&cm::Target, String)>,
694 mine: &[User],
695 cargo_equip_mod_name: &syn::Ident,
696 resolve_cfgs: bool,
697 remove: &[Remove],
698 minify: Minify,
699 rustfmt: bool,
700 toolchain_for_proc_macro_srv: Option<&str>,
701 cache_dir: &Path,
702 shell: &mut Shell,
703) -> anyhow::Result<String> {
704 let cargo_check_message_format_json = |toolchain: &str, shell: &mut Shell| -> _ {
705 let (package, krate) = root_crate.split();
706 workspace::cargo_check_message_format_json(toolchain, metadata, package, krate, shell)
707 .map(Rc::new)
708 };
709
710 let active_toolchain = &*toolchain::active_toolchain(root_crate.package().manifest_dir())?;
711 let toolchain_for_proc_macro_srv = toolchain_for_proc_macro_srv.unwrap_or(active_toolchain);
712
713 let has_custom_build = libs_to_bundle
714 .keys()
715 .any(|p| metadata[p].has_custom_build());
716 let has_proc_macro = libs_to_bundle.keys().any(|p| metadata[p].has_proc_macro());
717
718 let (cargo_messages_for_out_dirs, cargo_messages_for_proc_macro_dll_paths) =
719 if has_custom_build && has_proc_macro && active_toolchain == toolchain_for_proc_macro_srv {
720 let cargo_messages = cargo_check_message_format_json(active_toolchain, shell)?;
721 (cargo_messages.clone(), Some(cargo_messages))
722 } else {
723 let cargo_messages_for_out_dirs = has_custom_build
724 .then(|| cargo_check_message_format_json(active_toolchain, shell))
725 .unwrap_or_else(|| Ok(Default::default()))?;
726
727 let cargo_messages_for_proc_macro_dll_paths = has_proc_macro
728 .then(|| cargo_check_message_format_json(toolchain_for_proc_macro_srv, shell))
729 .transpose()?;
730
731 (
732 cargo_messages_for_out_dirs,
733 cargo_messages_for_proc_macro_dll_paths,
734 )
735 };
736
737 let out_dirs = workspace::list_out_dirs(metadata, &cargo_messages_for_out_dirs);
738
739 let macro_expander = cargo_messages_for_proc_macro_dll_paths
740 .as_ref()
741 .map(|cargo_messages_for_proc_macro_dll_paths| {
742 let proc_macro_srv_exe = &toolchain::find_rust_analyzer_proc_macro_srv(
743 root_crate.package().manifest_dir(),
744 toolchain_for_proc_macro_srv,
745 )?;
746
747 let proc_macro_crate_dylibs = &ra_proc_macro::list_proc_macro_dylibs(
748 cargo_messages_for_proc_macro_dll_paths,
749 |p| libs_to_bundle.contains_key(p),
750 );
751
752 ProcMacroExpander::spawn(proc_macro_srv_exe, proc_macro_crate_dylibs)
753 })
754 .transpose()?;
755
756 let proc_macro_names = macro_expander
757 .as_ref()
758 .map(|macro_expander| {
759 let mut proc_macro_names = HashMap::<_, BTreeSet<_>>::new();
760 for (pkg, macro_names) in macro_expander.macro_names() {
761 for macro_name in macro_names {
762 proc_macro_names
763 .entry(pkg)
764 .or_default()
765 .insert(macro_name.to_owned());
766 }
767 }
768 proc_macro_names
769 })
770 .unwrap_or_default();
771
772 let resolve_nodes = metadata
773 .resolve
774 .as_ref()
775 .map(|cm::Resolve { nodes, .. }| &nodes[..])
776 .unwrap_or(&[])
777 .iter()
778 .map(|node| (&node.id, node))
779 .collect::<HashMap<_, _>>();
780
781 let mut code = if let Some((_, bin_target)) = root_crate.bin_like() {
782 let code = cargo_util::paths::read(bin_target.src_path.as_ref())?;
783 if rust::find_skip_attribute(&code)? {
784 shell.status("Found", "`#![cfg_attr(cargo_equip, cargo_equip::skip)]`")?;
785 return Ok(code);
786 }
787 code
788 } else {
789 "".to_owned()
790 };
791
792 shell.status("Bundling", "the code")?;
793
794 if let Some((bin_package, bin_target)) = root_crate.bin_like() {
795 code = rust::process_bin(
796 cargo_equip_mod_name,
797 &bin_target.src_path,
798 { macro_expander }.as_mut(),
799 |extern_crate_name| {
800 metadata
801 .dep_lib_by_extern_crate_name(&bin_package.id, extern_crate_name)
802 .map(|_| extern_crate_name.to_owned())
803 },
804 |extern_crate_name| {
805 matches!(
806 metadata.dep_lib_by_extern_crate_name(&bin_package.id, extern_crate_name),
807 Some(lib_package) if libs_to_bundle.contains_key(&lib_package.id)
808 )
809 },
810 || (bin_target.crate_name(), &bin_package.id.repr),
811 )?;
812 }
813
814 let libs = libs_to_bundle
815 .iter()
816 .map(|(pkg, (krate, pseudo_extern_crate_name))| {
817 let mut edit = CodeEdit::new(cargo_equip_mod_name, &krate.src_path, || {
818 (krate.crate_name(), &pkg.repr)
819 })?;
820 if let Some(out_dir) = out_dirs.get(pkg) {
821 edit.expand_includes(out_dir)?;
822 }
823 Ok((*pkg, (*krate, &**pseudo_extern_crate_name, edit)))
824 })
825 .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
826
827 let (graph, indices) = normal_non_host_dep_graph(&resolve_nodes, libs_to_bundle);
828
829 let libs_using_proc_macros = {
830 let mut crates_using_proc_macros = BTreeMap::<_, HashSet<_>>::new();
831 for (pkg, names) in &proc_macro_names {
832 let (_, pseudo_extern_crate_name) = &libs_to_bundle[pkg];
833 for name in names {
834 crates_using_proc_macros
835 .entry(&**name)
836 .or_default()
837 .insert(&**pseudo_extern_crate_name);
838 }
839 }
840 for goal in libs_to_bundle.keys() {
841 if metadata[goal].has_proc_macro() {
842 let mut dfs = Dfs::new(&graph, indices[goal]);
843 while let Some(next) = dfs.next(&graph) {
844 let (_, pseudo_extern_crate_name) = &libs_to_bundle[graph[next]];
845 for name in &proc_macro_names[goal] {
846 crates_using_proc_macros
847 .entry(name)
848 .or_default()
849 .insert(pseudo_extern_crate_name);
850 }
851 }
852 }
853 }
854 crates_using_proc_macros
855 };
856
857 let libs_with_local_inner_macros = {
858 let mut libs_with_local_inner_macros = libs
859 .keys()
860 .map(|pkg| (*pkg, btreeset!()))
861 .collect::<HashMap<_, _>>();
862 for (goal, (_, pseudo_extern_crate_name, edit)) in &libs {
863 if edit.has_local_inner_macros_attr() {
864 libs_with_local_inner_macros
865 .get_mut(*goal)
866 .unwrap()
867 .insert(&**pseudo_extern_crate_name);
868 let mut dfs = Dfs::new(&graph, indices[goal]);
869 while let Some(next) = dfs.next(&graph) {
870 libs_with_local_inner_macros
871 .get_mut(graph[next])
872 .unwrap()
873 .insert(&**pseudo_extern_crate_name);
874 }
875 }
876 }
877 libs_with_local_inner_macros
878 };
879
880 let libs = libs
881 .into_iter()
882 .map(
883 |(lib_package, (lib_target, pseudo_extern_crate_name, mut edit))| {
884 let lib_package: &cm::Package = &metadata[lib_package];
885
886 if let Some(names) = proc_macro_names.get(&lib_package.id) {
887 debug_assert_eq!(["proc-macro".to_owned()], *lib_target.kind);
888 let names = names
889 .iter()
890 .map(|name| {
891 let rename = format!(
892 "{}_macro_def_{}_{}",
893 cargo_equip_mod_name, pseudo_extern_crate_name, name,
894 );
895 (name, rename)
896 })
897 .collect::<Vec<_>>();
898 let crate_mod_content = format!(
899 "pub use crate::{}::macros::{}::*;{}",
900 cargo_equip_mod_name,
901 pseudo_extern_crate_name,
902 names
903 .iter()
904 .map(|(name, rename)| {
905 let msg = format!(
906 "`{}` from `{} {}` should have been expanded",
907 name, lib_package.name, lib_package.version,
908 );
909 format!(
910 "#[macro_export]macro_rules!{}\
911 {{($(_:tt)*)=>(::std::compile_error!({});)}}",
912 rename,
913 quote!(#msg),
914 )
915 })
916 .join("")
917 );
918 let macro_mod_content = format!(
919 "pub use crate::{}{}{};",
920 if names.len() == 1 { " " } else { "{" },
921 names
922 .iter()
923 .map(|(name, rename)| format!("{} as {}", rename, name))
924 .format(","),
925 if names.len() == 1 { "" } else { "}" },
926 );
927 return Ok((
928 pseudo_extern_crate_name,
929 (
930 lib_package,
931 crate_mod_content,
932 macro_mod_content,
933 "".to_owned(),
934 ),
935 ));
936 }
937
938 let cm::Node { features, .. } = resolve_nodes[&lib_package.id];
939
940 let translate_extern_crate_name = |dst: &_| -> _ {
941 let dst_package =
942 metadata.dep_lib_by_extern_crate_name(&lib_package.id, dst)?;
943 let (_, dst_pseudo_extern_crate_name) =
944 libs_to_bundle.get(&dst_package.id).unwrap_or_else(|| {
945 panic!(
946 "missing `extern_crate_name` for `{}`. generated one should be \
947 given beforehead. this is a bug",
948 dst_package.id,
949 );
950 });
951 Some(dst_pseudo_extern_crate_name.clone())
952 };
953
954 edit.translate_crate_path(pseudo_extern_crate_name)?;
955 edit.translate_extern_crate_paths(translate_extern_crate_name)?;
956 edit.process_extern_crates_in_lib(translate_extern_crate_name, shell)?;
957 let macro_mod_content = edit.modify_declarative_macros(pseudo_extern_crate_name)?;
958 let prelude_mod_content = edit.resolve_pseudo_prelude(
959 pseudo_extern_crate_name,
960 &libs_with_local_inner_macros[&lib_package.id],
961 &{
962 metadata
963 .libs_with_extern_crate_names(
964 &lib_package.id,
965 &libs_to_bundle.keys().copied().collect(),
966 )?
967 .into_iter()
968 .map(|(package_id, extern_crate_name)| {
969 let (_, pseudo_extern_crate_name) =
970 libs_to_bundle.get(package_id).with_context(|| {
971 "could not translate pseudo extern crate names. this is a bug"
972 })?;
973 Ok((extern_crate_name, pseudo_extern_crate_name.clone()))
974 })
975 .collect::<anyhow::Result<_>>()?
976 },
977 )?;
978 if resolve_cfgs {
979 edit.resolve_cfgs(features)?;
980 }
981 if remove.contains(&Remove::Docs) {
982 edit.allow_missing_docs();
983 edit.erase_docs()?;
984 }
985 if remove.contains(&Remove::Comments) {
986 edit.erase_comments()?;
987 }
988
989 let crate_mod_content = edit.finish()?;
990
991 Ok((
992 pseudo_extern_crate_name,
993 (
994 lib_package,
995 crate_mod_content,
996 macro_mod_content,
997 prelude_mod_content,
998 ),
999 ))
1000 },
1001 )
1002 .collect::<anyhow::Result<Vec<(&str, (&cm::Package, String, String, String))>>>()?;
1003
1004 if !libs.is_empty() {
1005 if !root_crate.package().authors.is_empty() {
1006 shell.warn(
1007 "`package.authors` are no longer used to skip Copyright and License Notices",
1008 )?;
1009 shell.warn("instead, add `--mine github.com/{your username}` to the arguments")?;
1010 }
1011
1012 code = rust::insert_prelude_for_main_crate(&code, cargo_equip_mod_name)?;
1013
1014 code =
1015 rust::allow_unused_imports_for_seemingly_proc_macros(&code, |mod_name, item_name| {
1016 matches!(
1017 libs_using_proc_macros.get(item_name), Some(pseudo_extern_crate_names)
1018 if pseudo_extern_crate_names.contains(mod_name)
1019 )
1020 })?;
1021
1022 let doc = &{
1023 fn list_packages<'a>(
1024 doc: &mut String,
1025 title: &str,
1026 cargo_equip_mod_name: &syn::Ident,
1027 contents: impl Iterator<Item = (Option<&'a str>, &'a cm::Package)>,
1028 ) {
1029 let mut table = Table::new();
1030
1031 *table.get_format() = FormatBuilder::new()
1032 .column_separator(' ')
1033 .borders(' ')
1034 .build();
1035
1036 let contents = contents.collect::<Vec<_>>();
1037 let any_from_local_filesystem = contents.iter().any(|(_, p)| p.source.is_none());
1038
1039 for (pseudo_extern_crate_name, package) in contents {
1040 let mut row = row![format!("- `{}`", package.id.mask_path())];
1041
1042 if any_from_local_filesystem {
1043 row.add_cell(if package.source.is_some() {
1044 cell!("")
1045 } else if let Some(repository) = &package.repository {
1046 cell!(format!("published in {}", repository))
1047 } else {
1048 cell!("published in **missing**")
1049 });
1050 }
1051
1052 row.add_cell(if let Some(license) = &package.license {
1053 cell!(format!("licensed under `{}`", license))
1054 } else {
1055 cell!("licensed under **missing**")
1056 });
1057
1058 if let Some(pseudo_extern_crate_name) = pseudo_extern_crate_name {
1059 row.add_cell(cell!(format!(
1060 "as `crate::{}::crates::{}`",
1061 cargo_equip_mod_name, pseudo_extern_crate_name,
1062 )));
1063 }
1064
1065 table.add_row(row);
1066 }
1067
1068 if !table.is_empty() {
1069 if !doc.is_empty() {
1070 *doc += "\n";
1071 }
1072 *doc += &format!(" # {}\n\n", title);
1073 for line in table.to_string().lines() {
1074 *doc += line.trim_end();
1075 *doc += "\n";
1076 }
1077 }
1078 }
1079
1080 let mut doc = "".to_owned();
1081
1082 list_packages(
1083 &mut doc,
1084 "Bundled libraries",
1085 cargo_equip_mod_name,
1086 libs.iter()
1087 .filter(|(_, (p, _, _, _))| p.has_lib())
1088 .map(|(k, (p, _, _, _))| (Some(*k), *p)),
1089 );
1090
1091 list_packages(
1092 &mut doc,
1093 "Procedural macros",
1094 cargo_equip_mod_name,
1095 libs.iter()
1096 .filter(|(_, (p, _, _, _))| p.has_proc_macro())
1097 .map(|(_, (p, _, _, _))| (None, *p)),
1098 );
1099
1100 let notices = libs
1101 .iter()
1102 .filter(|(_, (p, _, _, _))| {
1103 p.has_lib() && !metadata.workspace_members.contains(&p.id)
1104 })
1105 .map(|(_, (lib_package, _, _, _))| {
1106 shell.status("Checking", format!("the license of `{}`", lib_package.id))?;
1107 lib_package
1108 .read_license_text(mine, cache_dir)
1109 .map(|license_text| {
1110 license_text.map(|license_text| (&lib_package.id, license_text))
1111 })
1112 })
1113 .flat_map(Result::transpose)
1114 .collect::<Result<Vec<_>, _>>()?;
1115
1116 if !notices.is_empty() {
1117 doc += "\n # License and Copyright Notices\n";
1118 for (package_id, license_text) in notices {
1119 doc += &format!("\n - `{}`\n\n", package_id.mask_path());
1120 let backquotes = {
1121 let (mut n, mut m) = (2, None);
1122 for c in license_text.chars() {
1123 if c == '`' {
1124 m = Some(m.unwrap_or(0) + 1);
1125 } else if let Some(m) = m.take() {
1126 n = cmp::max(n, m);
1127 }
1128 }
1129 "`".repeat(cmp::max(n, m.unwrap_or(0)) + 1)
1130 };
1131 doc += &format!(" {}text\n", backquotes);
1132 for line in license_text.lines() {
1133 match line {
1134 "" => doc += "\n",
1135 line => doc += &format!(" {}\n", line),
1136 }
1137 }
1138 doc += &format!(" {}\n", backquotes);
1139 }
1140 }
1141
1142 doc
1143 };
1144
1145 code += "\n";
1146 code += &match root_crate {
1147 RootCrate::BinLike(..) => {
1148 "// The following code was expanded by `cargo-equip`.\n".to_owned()
1149 }
1150 RootCrate::Lib(..) => format!("use {}::prelude::*;\n", cargo_equip_mod_name),
1151 };
1152 code += "\n";
1153
1154 let crate_mods = libs
1155 .iter()
1156 .map(|(name, (_, content, _, _))| (*name, &**content))
1157 .collect::<Vec<_>>();
1158
1159 let macro_mods = libs
1160 .iter()
1161 .map(|(name, (_, _, content, _))| (*name, &**content))
1162 .collect::<Vec<_>>();
1163
1164 let prelude_mods = libs
1165 .iter()
1166 .map(|(name, (_, _, _, content))| (*name, &**content))
1167 .collect::<Vec<_>>();
1168
1169 let render_mods = |code: &mut String, mods: &[(&str, &str)]| -> anyhow::Result<()> {
1170 if minify == Minify::Libs {
1171 for (pseudo_extern_crate_name, mod_content) in mods {
1172 *code += " pub mod ";
1173 *code += pseudo_extern_crate_name;
1174 *code += " {";
1175 *code += &rustminify::minify_file(&rust::parse_file(mod_content)?);
1176 *code += "}\n";
1177 }
1178 } else {
1179 for (i, (pseudo_extern_crate_name, mod_content)) in mods.iter().enumerate() {
1180 if i > 0 {
1181 *code += "\n";
1182 }
1183 *code += " pub mod ";
1184 *code += pseudo_extern_crate_name;
1185 *code += " {\n";
1186 *code += &rust::indent_code(mod_content, 3);
1187 *code += " }\n";
1188 }
1189 }
1190 Ok(())
1191 };
1192
1193 for doc in doc.lines() {
1194 code += "///";
1195 if !code.is_empty() {
1196 code += " ";
1197 }
1198 code += doc;
1199 code += "\n";
1200 }
1201 if minify == Minify::Libs {
1202 code += "#[cfg_attr(any(), rustfmt::skip)]\n";
1203 }
1204 code += "#[allow(unused)]\n";
1205 code += &format!("mod {} {{\n", cargo_equip_mod_name);
1206 code += " pub(crate) mod crates {\n";
1207 render_mods(&mut code, &crate_mods)?;
1208 code += " }\n";
1209 code += "\n";
1210 code += " pub(crate) mod macros {\n";
1211 render_mods(&mut code, ¯o_mods)?;
1212 code += " }\n";
1213 code += "\n";
1214 code += " pub(crate) mod prelude {";
1215 match root_crate {
1216 RootCrate::BinLike(..) => {
1217 let prelude_for_main = {
1218 let local_macro_uses_in_main_crate = libs_with_local_inner_macros
1219 .values()
1220 .flatten()
1221 .unique()
1222 .sorted()
1223 .map(|name| format!("{}::*", name))
1224 .collect::<Vec<_>>();
1225
1226 let local_macro_uses_in_main_crate = match &*local_macro_uses_in_main_crate {
1227 [] => None,
1228 [part] => Some(part.clone()),
1229 parts => Some(format!("{{{}}}", parts.iter().format(","))),
1230 };
1231
1232 format!(
1233 "pub use crate::{}::{};",
1234 cargo_equip_mod_name,
1235 if let Some(local_macro_uses_in_main_crate) = local_macro_uses_in_main_crate
1236 {
1237 format!("{{crates::*,macros::{}}}", local_macro_uses_in_main_crate)
1238 } else {
1239 "crates::*".to_owned()
1240 }
1241 )
1242 };
1243 code += &if minify == Minify::Libs {
1244 prelude_for_main
1245 } else {
1246 format!("\n {}\n ", prelude_for_main)
1247 };
1248 }
1249 RootCrate::Lib(_, krate) => {
1250 code += &format!("pub use crate::{}::crates::", cargo_equip_mod_name);
1251 code += &krate.crate_name();
1252 code += ";";
1253 }
1254 }
1255 code += "}\n";
1256 code += "\n";
1257 code += " mod preludes {\n";
1258 render_mods(&mut code, &prelude_mods)?;
1259 code += " }\n";
1260 code += "}\n";
1261 }
1262
1263 if minify == Minify::All {
1264 code = rustminify::minify_file(&rust::parse_file(&code)?);
1265 }
1266
1267 if rustfmt {
1268 code = rustfmt::rustfmt(
1269 &metadata.workspace_root,
1270 &code,
1271 &root_crate.package().edition,
1272 )?;
1273 }
1274
1275 Ok(code)
1276}
1277
1278fn normal_non_host_dep_graph<'cm>(
1279 resolve_nodes: &HashMap<&'cm cm::PackageId, &cm::Node>,
1280 libs_to_bundle: &BTreeMap<&'cm cm::PackageId, (&cm::Target, String)>,
1281) -> (
1282 Graph<&'cm cm::PackageId, ()>,
1283 HashMap<&'cm cm::PackageId, NodeIndex>,
1284) {
1285 let mut graph = Graph::new();
1286 let mut indices = hashmap!();
1287 for pkg in libs_to_bundle.keys() {
1288 indices.insert(*pkg, graph.add_node(*pkg));
1289 }
1290 for (from_pkg, (from_crate, _)) in libs_to_bundle {
1291 if from_crate.is_lib() {
1292 for cm::NodeDep {
1293 pkg: to, dep_kinds, ..
1294 } in &resolve_nodes[from_pkg].deps
1295 {
1296 if *from_pkg != to
1297 && dep_kinds
1298 .iter()
1299 .any(|cm::DepKindInfo { kind, .. }| *kind == cm::DependencyKind::Normal)
1300 && libs_to_bundle.contains_key(to)
1301 {
1302 graph.add_edge(indices[to], indices[*from_pkg], ());
1303 }
1304 }
1305 }
1306 }
1307 (graph, indices)
1308}
1309
1310#[derive(Clone, Copy)]
1311enum RootCrate<'cm> {
1312 BinLike(&'cm cm::Package, &'cm cm::Target),
1313 Lib(&'cm cm::Package, &'cm cm::Target),
1314}
1315
1316impl<'cm> RootCrate<'cm> {
1317 fn split(self) -> (&'cm cm::Package, &'cm cm::Target) {
1318 match self {
1319 RootCrate::BinLike(p, t) | RootCrate::Lib(p, t) => (p, t),
1320 }
1321 }
1322
1323 fn package(self) -> &'cm cm::Package {
1324 match self {
1325 RootCrate::BinLike(p, _) | RootCrate::Lib(p, _) => p,
1326 }
1327 }
1328
1329 fn bin_like(self) -> Option<(&'cm cm::Package, &'cm cm::Target)> {
1330 match self {
1331 RootCrate::BinLike(p, t) => Some((p, t)),
1332 RootCrate::Lib(..) => None,
1333 }
1334 }
1335}