Skip to content

Commit d7bffc3

Browse files
committed
Auto merge of #14280 - Flowrey:dry-run-install, r=weihanglo
Add a `--dry-run` flag to the `install` command ### What does this PR try to resolve? This PR add the `--dry-run` flag to the `cargo install` command (see #11123). I've tried to do the bare minimal for this flag to work without changing anything in the output. In my opinion, the `--dry-run` flag should mimic as close as possible the behavior of the normal command to avoid missing potential issue in the normal execution. ~~Currently we're missing information about where the binary will be installed.~~ Unlike #13598 this PR: - Include as much of the compilation process as possible without actually compiling - use the information provided by `BuildContext` instead of `InstallablePackage::new` - in the same way as `unit_graph`, it add a `dry_run` to the `CompileOptions` and return a `Compilation::new` from the function `compile_ws` without actually compiling. - keeps the output the same rather than adding status messages indicating which very broad actions would be performed - ~~remove some warning not relevant in the case of a `--dry-run`~~ Like #13598, the version check and crate downloads still occur. ### How should we test and review this PR? The first commit include a unit tests to ensure that no binary is actually installed after the dry run. There is also a snapshot test that show the diff output of the `--help` flag. ### Additional information Tests and documentation done in #13598, may be cherry picked into this PR if needed.
2 parents 6469a3e + b0e08fc commit d7bffc3

File tree

11 files changed

+324
-91
lines changed

11 files changed

+324
-91
lines changed

src/bin/cargo/commands/install.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub fn cli() -> Command {
6969
)
7070
.arg(opt("root", "Directory to install packages into").value_name("DIR"))
7171
.arg(flag("force", "Force overwriting existing crates or binaries").short('f'))
72+
.arg_dry_run("Perform all checks without installing (unstable)")
7273
.arg(flag("no-track", "Do not save tracking information"))
7374
.arg(flag(
7475
"list",
@@ -200,7 +201,9 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
200201

201202
compile_opts.build_config.requested_profile =
202203
args.get_profile_name("release", ProfileChecking::Custom)?;
203-
204+
if args.dry_run() {
205+
gctx.cli_unstable().fail_if_stable_opt("--dry-run", 11123)?;
206+
}
204207
if args.flag("list") {
205208
ops::install_list(root, gctx)?;
206209
} else {
@@ -213,6 +216,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
213216
&compile_opts,
214217
args.flag("force"),
215218
args.flag("no-track"),
219+
args.dry_run(),
216220
)?;
217221
}
218222
Ok(())

src/cargo/core/compiler/build_config.rs

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub struct BuildConfig {
3131
pub build_plan: bool,
3232
/// Output the unit graph to stdout instead of actually compiling.
3333
pub unit_graph: bool,
34+
/// `true` to avoid really compiling.
35+
pub dry_run: bool,
3436
/// An optional override of the rustc process for primary units
3537
pub primary_unit_rustc: Option<ProcessBuilder>,
3638
/// A thread used by `cargo fix` to receive messages on a socket regarding
@@ -112,6 +114,7 @@ impl BuildConfig {
112114
force_rebuild: false,
113115
build_plan: false,
114116
unit_graph: false,
117+
dry_run: false,
115118
primary_unit_rustc: None,
116119
rustfix_diagnostic_server: Rc::new(RefCell::new(None)),
117120
export_dir: None,

src/cargo/core/compiler/build_runner/mod.rs

+49-25
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
126126
})
127127
}
128128

129+
/// Dry-run the compilation without actually running it.
130+
///
131+
/// This is expected to collect information like the location of output artifacts.
132+
/// Please keep in sync with non-compilation part in [`BuildRunner::compile`].
133+
pub fn dry_run(mut self) -> CargoResult<Compilation<'gctx>> {
134+
let _lock = self
135+
.bcx
136+
.gctx
137+
.acquire_package_cache_lock(CacheLockMode::Shared)?;
138+
self.lto = super::lto::generate(self.bcx)?;
139+
self.prepare_units()?;
140+
self.prepare()?;
141+
self.check_collisions()?;
142+
143+
for unit in &self.bcx.roots {
144+
self.collect_tests_and_executables(unit)?;
145+
}
146+
147+
Ok(self.compilation)
148+
}
149+
129150
/// Starts compilation, waits for it to finish, and returns information
130151
/// about the result of compilation.
131152
///
@@ -214,31 +235,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
214235

215236
// Collect the result of the build into `self.compilation`.
216237
for unit in &self.bcx.roots {
217-
// Collect tests and executables.
218-
for output in self.outputs(unit)?.iter() {
219-
if output.flavor == FileFlavor::DebugInfo || output.flavor == FileFlavor::Auxiliary
220-
{
221-
continue;
222-
}
223-
224-
let bindst = output.bin_dst();
225-
226-
if unit.mode == CompileMode::Test {
227-
self.compilation
228-
.tests
229-
.push(self.unit_output(unit, &output.path));
230-
} else if unit.target.is_executable() {
231-
self.compilation
232-
.binaries
233-
.push(self.unit_output(unit, bindst));
234-
} else if unit.target.is_cdylib()
235-
&& !self.compilation.cdylibs.iter().any(|uo| uo.unit == *unit)
236-
{
237-
self.compilation
238-
.cdylibs
239-
.push(self.unit_output(unit, bindst));
240-
}
241-
}
238+
self.collect_tests_and_executables(unit)?;
242239

243240
// Collect information for `rustdoc --test`.
244241
if unit.mode.is_doc_test() {
@@ -307,6 +304,33 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
307304
Ok(self.compilation)
308305
}
309306

307+
fn collect_tests_and_executables(&mut self, unit: &Unit) -> CargoResult<()> {
308+
for output in self.outputs(unit)?.iter() {
309+
if output.flavor == FileFlavor::DebugInfo || output.flavor == FileFlavor::Auxiliary {
310+
continue;
311+
}
312+
313+
let bindst = output.bin_dst();
314+
315+
if unit.mode == CompileMode::Test {
316+
self.compilation
317+
.tests
318+
.push(self.unit_output(unit, &output.path));
319+
} else if unit.target.is_executable() {
320+
self.compilation
321+
.binaries
322+
.push(self.unit_output(unit, bindst));
323+
} else if unit.target.is_cdylib()
324+
&& !self.compilation.cdylibs.iter().any(|uo| uo.unit == *unit)
325+
{
326+
self.compilation
327+
.cdylibs
328+
.push(self.unit_output(unit, bindst));
329+
}
330+
}
331+
Ok(())
332+
}
333+
310334
/// Returns the executable for the specified unit (if any).
311335
pub fn get_executable(&mut self, unit: &Unit) -> CargoResult<Option<PathBuf>> {
312336
let is_binary = unit.target.is_executable();

src/cargo/ops/cargo_compile/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@ pub fn compile_ws<'a>(
156156
}
157157
crate::core::gc::auto_gc(bcx.gctx);
158158
let build_runner = BuildRunner::new(&bcx)?;
159-
build_runner.compile(exec)
159+
if options.build_config.dry_run {
160+
build_runner.dry_run()
161+
} else {
162+
build_runner.compile(exec)
163+
}
160164
}
161165

162166
/// Executes `rustc --print <VALUE>`.

src/cargo/ops/cargo_install.rs

+43-24
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ impl<'gctx> InstallablePackage<'gctx> {
297297
Ok(duplicates)
298298
}
299299

300-
fn install_one(mut self) -> CargoResult<bool> {
300+
fn install_one(mut self, dry_run: bool) -> CargoResult<bool> {
301301
self.gctx.shell().status("Installing", &self.pkg)?;
302302

303303
let dst = self.root.join("bin").into_path_unlocked();
@@ -321,6 +321,7 @@ impl<'gctx> InstallablePackage<'gctx> {
321321
self.check_yanked_install()?;
322322

323323
let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
324+
self.opts.build_config.dry_run = dry_run;
324325
let compile = ops::compile_ws(&self.ws, &self.opts, &exec).with_context(|| {
325326
if let Some(td) = td_opt.take() {
326327
// preserve the temporary directory, so the user can inspect it
@@ -419,13 +420,15 @@ impl<'gctx> InstallablePackage<'gctx> {
419420
let staging_dir = TempFileBuilder::new()
420421
.prefix("cargo-install")
421422
.tempdir_in(&dst)?;
422-
for &(bin, src) in binaries.iter() {
423-
let dst = staging_dir.path().join(bin);
424-
// Try to move if `target_dir` is transient.
425-
if !self.source_id.is_path() && fs::rename(src, &dst).is_ok() {
426-
continue;
423+
if !dry_run {
424+
for &(bin, src) in binaries.iter() {
425+
let dst = staging_dir.path().join(bin);
426+
// Try to move if `target_dir` is transient.
427+
if !self.source_id.is_path() && fs::rename(src, &dst).is_ok() {
428+
continue;
429+
}
430+
paths::copy(src, &dst)?;
427431
}
428-
paths::copy(src, &dst)?;
429432
}
430433

431434
let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries
@@ -441,11 +444,13 @@ impl<'gctx> InstallablePackage<'gctx> {
441444
let src = staging_dir.path().join(bin);
442445
let dst = dst.join(bin);
443446
self.gctx.shell().status("Installing", dst.display())?;
444-
fs::rename(&src, &dst).with_context(|| {
445-
format!("failed to move `{}` to `{}`", src.display(), dst.display())
446-
})?;
447-
installed.bins.push(dst);
448-
successful_bins.insert(bin.to_string());
447+
if !dry_run {
448+
fs::rename(&src, &dst).with_context(|| {
449+
format!("failed to move `{}` to `{}`", src.display(), dst.display())
450+
})?;
451+
installed.bins.push(dst);
452+
successful_bins.insert(bin.to_string());
453+
}
449454
}
450455

451456
// Repeat for binaries which replace existing ones but don't pop the error
@@ -456,10 +461,12 @@ impl<'gctx> InstallablePackage<'gctx> {
456461
let src = staging_dir.path().join(bin);
457462
let dst = dst.join(bin);
458463
self.gctx.shell().status("Replacing", dst.display())?;
459-
fs::rename(&src, &dst).with_context(|| {
460-
format!("failed to move `{}` to `{}`", src.display(), dst.display())
461-
})?;
462-
successful_bins.insert(bin.to_string());
464+
if !dry_run {
465+
fs::rename(&src, &dst).with_context(|| {
466+
format!("failed to move `{}` to `{}`", src.display(), dst.display())
467+
})?;
468+
successful_bins.insert(bin.to_string());
469+
}
463470
}
464471
Ok(())
465472
};
@@ -476,9 +483,14 @@ impl<'gctx> InstallablePackage<'gctx> {
476483
&self.rustc.verbose_version,
477484
);
478485

479-
if let Err(e) =
480-
remove_orphaned_bins(&self.ws, &mut tracker, &duplicates, &self.pkg, &dst)
481-
{
486+
if let Err(e) = remove_orphaned_bins(
487+
&self.ws,
488+
&mut tracker,
489+
&duplicates,
490+
&self.pkg,
491+
&dst,
492+
dry_run,
493+
) {
482494
// Don't hard error on remove.
483495
self.gctx
484496
.shell()
@@ -515,7 +527,10 @@ impl<'gctx> InstallablePackage<'gctx> {
515527
}
516528
}
517529

518-
if duplicates.is_empty() {
530+
if dry_run {
531+
self.gctx.shell().warn("aborting install due to dry run")?;
532+
Ok(true)
533+
} else if duplicates.is_empty() {
519534
self.gctx.shell().status(
520535
"Installed",
521536
format!(
@@ -620,6 +635,7 @@ pub fn install(
620635
opts: &ops::CompileOptions,
621636
force: bool,
622637
no_track: bool,
638+
dry_run: bool,
623639
) -> CargoResult<()> {
624640
let root = resolve_root(root, gctx)?;
625641
let dst = root.join("bin").into_path_unlocked();
@@ -654,7 +670,7 @@ pub fn install(
654670
)?;
655671
let mut installed_anything = true;
656672
if let Some(installable_pkg) = installable_pkg {
657-
installed_anything = installable_pkg.install_one()?;
673+
installed_anything = installable_pkg.install_one(dry_run)?;
658674
}
659675
(installed_anything, false)
660676
} else {
@@ -705,7 +721,7 @@ pub fn install(
705721

706722
let install_results: Vec<_> = pkgs_to_install
707723
.into_iter()
708-
.map(|(krate, installable_pkg)| (krate, installable_pkg.install_one()))
724+
.map(|(krate, installable_pkg)| (krate, installable_pkg.install_one(dry_run)))
709725
.collect();
710726

711727
for (krate, result) in install_results {
@@ -857,6 +873,7 @@ fn remove_orphaned_bins(
857873
duplicates: &BTreeMap<String, Option<PackageId>>,
858874
pkg: &Package,
859875
dst: &Path,
876+
dry_run: bool,
860877
) -> CargoResult<()> {
861878
let filter = ops::CompileFilter::new_all_targets();
862879
let all_self_names = exe_names(pkg, &filter);
@@ -894,8 +911,10 @@ fn remove_orphaned_bins(
894911
old_pkg
895912
),
896913
)?;
897-
paths::remove_file(&full_path)
898-
.with_context(|| format!("failed to remove {:?}", full_path))?;
914+
if !dry_run {
915+
paths::remove_file(&full_path)
916+
.with_context(|| format!("failed to remove {:?}", full_path))?;
917+
}
899918
}
900919
}
901920
}

src/doc/man/cargo-install.md

+4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ Filesystem path to local crate to install from.
121121
List all installed packages and their versions.
122122
{{/option}}
123123

124+
{{#option "`-n`" "`--dry-run`" }}
125+
(unstable) Perform all checks without installing.
126+
{{/option}}
127+
124128
{{#option "`-f`" "`--force`" }}
125129
Force overwriting existing crates or binaries. This can be used if a package
126130
has installed a binary with the same name as another package. This is also

src/doc/man/generated_txt/cargo-install.txt

+3
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ OPTIONS
120120
--list
121121
List all installed packages and their versions.
122122

123+
-n, --dry-run
124+
(unstable) Perform all checks without installing.
125+
123126
-f, --force
124127
Force overwriting existing crates or binaries. This can be used if a
125128
package has installed a binary with the same name as another

src/doc/src/commands/cargo-install.md

+5
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ treated as a caret requirement like Cargo dependencies are.</dd>
124124
<dd class="option-desc">List all installed packages and their versions.</dd>
125125

126126

127+
<dt class="option-term" id="option-cargo-install--n"><a class="option-anchor" href="#option-cargo-install--n"></a><code>-n</code></dt>
128+
<dt class="option-term" id="option-cargo-install---dry-run"><a class="option-anchor" href="#option-cargo-install---dry-run"></a><code>--dry-run</code></dt>
129+
<dd class="option-desc">(unstable) Perform all checks without installing.</dd>
130+
131+
127132
<dt class="option-term" id="option-cargo-install--f"><a class="option-anchor" href="#option-cargo-install--f"></a><code>-f</code></dt>
128133
<dt class="option-term" id="option-cargo-install---force"><a class="option-anchor" href="#option-cargo-install---force"></a><code>--force</code></dt>
129134
<dd class="option-desc">Force overwriting existing crates or binaries. This can be used if a package

src/etc/man/cargo-install.1

+6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ Filesystem path to local crate to install from.
151151
List all installed packages and their versions.
152152
.RE
153153
.sp
154+
\fB\-n\fR,
155+
\fB\-\-dry\-run\fR
156+
.RS 4
157+
(unstable) Perform all checks without installing.
158+
.RE
159+
.sp
154160
\fB\-f\fR,
155161
\fB\-\-force\fR
156162
.RS 4

0 commit comments

Comments
 (0)