Skip to content

Commit 9bbab73

Browse files
committed
Auto merge of #5824 - alexcrichton:careful-transition, r=alexcrichton
Add more diagnostics to smooth edition transition This commit adds two diagnostics in particular to ease the transition into the 2018 edition. The current transition process is pretty particular and must be done carefully, so let's try to automate things to make it as painless as possible! Notably the new diagnostics are: * If you `cargo fix --prepare-for 2018` a crate which already has the 2018 edition enabled, then an error is generated. This is because the compiler can't prepare for the 2018 edition if you're already in the 2018 edition, the lints won't have a chance to fire. You can only execute `--prepare-for 2018` over crates in the 2015 edition. * If you `cargo fix --prepare-for 2018` and have forgotten the `rust_2018_preview` feature, a warning is issued. The lints don't fire unless the feature is enabled, so this is intended to warn in this situation to ensure that lints fire as much as they can. After this commit if `cargo fix --prepare-for` exits successfully with zero warnings then crates should be guaranteed to be compatible! Closes #5778
2 parents d9feff2 + fa7a387 commit 9bbab73

File tree

7 files changed

+299
-50
lines changed

7 files changed

+299
-50
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ pub struct Context<'a, 'cfg: 'a> {
9696
pub links: Links<'a>,
9797
pub used_in_plugin: HashSet<Unit<'a>>,
9898
pub jobserver: Client,
99+
primary_packages: HashSet<&'a PackageId>,
99100
unit_dependencies: HashMap<Unit<'a>, Vec<Unit<'a>>>,
100101
files: Option<CompilationFiles<'a, 'cfg>>,
101102
}
@@ -129,6 +130,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
129130
jobserver,
130131
build_script_overridden: HashSet::new(),
131132

133+
primary_packages: HashSet::new(),
132134
unit_dependencies: HashMap::new(),
133135
files: None,
134136
})
@@ -321,6 +323,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
321323
Some(target) => Some(Layout::new(self.bcx.ws, Some(target), dest)?),
322324
None => None,
323325
};
326+
self.primary_packages.extend(units.iter().map(|u| u.pkg.package_id()));
324327

325328
build_unit_dependencies(units, self.bcx, &mut self.unit_dependencies)?;
326329
self.build_used_in_plugin_map(units)?;
@@ -487,6 +490,10 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
487490
let dir = self.files().layout(unit.kind).incremental().display();
488491
Ok(vec!["-C".to_string(), format!("incremental={}", dir)])
489492
}
493+
494+
pub fn is_primary_package(&self, unit: &Unit<'a>) -> bool {
495+
self.primary_packages.contains(unit.pkg.package_id())
496+
}
490497
}
491498

492499
#[derive(Default)]

src/cargo/core/compiler/job_queue.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use handle_error;
1616
use util::{internal, profile, CargoResult, CargoResultExt, ProcessBuilder};
1717
use util::{Config, DependencyQueue, Dirty, Fresh, Freshness};
1818
use util::{Progress, ProgressStyle};
19-
use util::diagnostic_server;
19+
use util::diagnostic_server::{self, DiagnosticPrinter};
2020

2121
use super::job::Job;
2222
use super::{BuildContext, BuildPlan, CompileMode, Context, Kind, Unit};
@@ -200,6 +200,7 @@ impl<'a> JobQueue<'a> {
200200
let mut tokens = Vec::new();
201201
let mut queue = Vec::new();
202202
let build_plan = cx.bcx.build_config.build_plan;
203+
let mut print = DiagnosticPrinter::new(cx.bcx.config);
203204
trace!("queue: {:#?}", self.queue);
204205

205206
// Iteratively execute the entire dependency graph. Each turn of the
@@ -311,7 +312,7 @@ impl<'a> JobQueue<'a> {
311312
}
312313
}
313314
Message::FixDiagnostic(msg) => {
314-
msg.print_to(cx.bcx.config)?;
315+
print.print(&msg)?;
315316
}
316317
Message::Finish(key, result) => {
317318
info!("end: {:?}", key);

src/cargo/core/compiler/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,14 @@ fn compile<'a, 'cfg: 'a>(
161161
}
162162

163163
fn rustc<'a, 'cfg>(
164-
mut cx: &mut Context<'a, 'cfg>,
164+
cx: &mut Context<'a, 'cfg>,
165165
unit: &Unit<'a>,
166166
exec: &Arc<Executor>,
167167
) -> CargoResult<Work> {
168168
let mut rustc = prepare_rustc(cx, &unit.target.rustc_crate_types(), unit)?;
169+
if cx.is_primary_package(unit) {
170+
rustc.env("CARGO_PRIMARY_PACKAGE", "1");
171+
}
169172
let build_plan = cx.bcx.build_config.build_plan;
170173

171174
let name = unit.pkg.name().to_string();
@@ -195,7 +198,7 @@ fn rustc<'a, 'cfg>(
195198
} else {
196199
root.join(&cx.files().file_stem(unit))
197200
}.with_extension("d");
198-
let dep_info_loc = fingerprint::dep_info_loc(&mut cx, unit);
201+
let dep_info_loc = fingerprint::dep_info_loc(cx, unit);
199202

200203
rustc.args(&cx.bcx.rustflags_args(unit)?);
201204
let json_messages = cx.bcx.build_config.json_messages();

src/cargo/ops/fix.rs

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::collections::{HashMap, HashSet, BTreeSet};
22
use std::env;
3-
use std::fs::File;
4-
use std::io::Write;
5-
use std::path::Path;
3+
use std::ffi::OsString;
4+
use std::fs;
5+
use std::path::{Path, PathBuf};
66
use std::process::{self, Command, ExitStatus};
77
use std::str;
88

@@ -21,6 +21,7 @@ use util::paths;
2121

2222
const FIX_ENV: &str = "__CARGO_FIX_PLZ";
2323
const BROKEN_CODE_ENV: &str = "__CARGO_FIX_BROKEN_CODE";
24+
const EDITION_ENV: &str = "__CARGO_FIX_EDITION";
2425

2526
pub struct FixOptions<'a> {
2627
pub edition: Option<&'a str>,
@@ -48,9 +49,10 @@ pub fn fix(ws: &Workspace, opts: &mut FixOptions) -> CargoResult<()> {
4849
}
4950

5051
if let Some(edition) = opts.edition {
51-
opts.compile_opts.build_config.extra_rustc_args.push("-W".to_string());
52-
let lint_name = format!("rust-{}-compatibility", edition);
53-
opts.compile_opts.build_config.extra_rustc_args.push(lint_name);
52+
opts.compile_opts.build_config.extra_rustc_env.push((
53+
EDITION_ENV.to_string(),
54+
edition.to_string(),
55+
));
5456
}
5557
opts.compile_opts.build_config.cargo_as_rustc_wrapper = true;
5658
*opts.compile_opts.build_config.rustfix_diagnostic_server.borrow_mut() =
@@ -115,14 +117,8 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
115117
Err(_) => return Ok(false),
116118
};
117119

118-
// Try to figure out what we're compiling by looking for a rust-like file
119-
// that exists.
120-
let filename = env::args()
121-
.skip(1)
122-
.filter(|s| s.ends_with(".rs"))
123-
.find(|s| Path::new(s).exists());
124-
125-
trace!("cargo-fix as rustc got file {:?}", filename);
120+
let args = FixArgs::get();
121+
trace!("cargo-fix as rustc got file {:?}", args.file);
126122
let rustc = env::var_os("RUSTC").expect("failed to find RUSTC env var");
127123

128124
// Our goal is to fix only the crates that the end user is interested in.
@@ -133,10 +129,10 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
133129
// compiling a Rust file and it *doesn't* have an absolute filename. That's
134130
// not the best heuristic but matches what Cargo does today at least.
135131
let mut fixes = FixedCrate::default();
136-
if let Some(path) = filename {
137-
if !Path::new(&path).is_absolute() {
132+
if let Some(path) = &args.file {
133+
if env::var("CARGO_PRIMARY_PACKAGE").is_ok() {
138134
trace!("start rustfixing {:?}", path);
139-
fixes = rustfix_crate(&lock_addr, rustc.as_ref(), &path)?;
135+
fixes = rustfix_crate(&lock_addr, rustc.as_ref(), path, &args)?;
140136
}
141137
}
142138

@@ -148,11 +144,10 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
148144
// If we didn't actually make any changes then we can immediately exec the
149145
// new rustc, and otherwise we capture the output to hide it in the scenario
150146
// that we have to back it all out.
151-
let mut cmd = Command::new(&rustc);
152-
cmd.args(env::args().skip(1));
153-
cmd.arg("--cap-lints=warn");
154-
cmd.arg("--error-format=json");
155147
if !fixes.original_files.is_empty() {
148+
let mut cmd = Command::new(&rustc);
149+
args.apply(&mut cmd);
150+
cmd.arg("--error-format=json");
156151
let output = cmd.output().context("failed to spawn rustc")?;
157152

158153
if output.status.success() {
@@ -173,17 +168,15 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
173168
// below to recompile again.
174169
if !output.status.success() {
175170
for (k, v) in fixes.original_files {
176-
File::create(&k)
177-
.and_then(|mut f| f.write_all(v.as_bytes()))
171+
fs::write(&k, v)
178172
.with_context(|_| format!("failed to write file `{}`", k))?;
179173
}
180174
log_failed_fix(&output.stderr)?;
181175
}
182176
}
183177

184178
let mut cmd = Command::new(&rustc);
185-
cmd.args(env::args().skip(1));
186-
cmd.arg("--cap-lints=warn");
179+
args.apply(&mut cmd);
187180
exit_with(cmd.status().context("failed to spawn rustc")?);
188181
}
189182

@@ -193,9 +186,12 @@ struct FixedCrate {
193186
original_files: HashMap<String, String>,
194187
}
195188

196-
fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
189+
fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
197190
-> Result<FixedCrate, Error>
198191
{
192+
args.verify_not_preparing_for_enabled_edition()?;
193+
args.warn_if_preparing_probably_inert()?;
194+
199195
// If not empty, filter by these lints
200196
//
201197
// TODO: Implement a way to specify this
@@ -210,8 +206,8 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
210206
let _lock = LockServerClient::lock(&lock_addr.parse()?, filename)?;
211207

212208
let mut cmd = Command::new(&rustc);
213-
cmd.args(env::args().skip(1));
214-
cmd.arg("--error-format=json").arg("--cap-lints=warn");
209+
cmd.arg("--error-format=json");
210+
args.apply(&mut cmd);
215211
let output = cmd.output()
216212
.with_context(|_| format!("failed to execute `{}`", rustc.display()))?;
217213

@@ -280,7 +276,8 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
280276

281277
debug!(
282278
"collected {} suggestions for `{}`",
283-
num_suggestion, filename
279+
num_suggestion,
280+
filename.display(),
284281
);
285282

286283
let mut original_files = HashMap::with_capacity(file_map.len());
@@ -311,8 +308,7 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &str)
311308
continue;
312309
}
313310
Ok(new_code) => {
314-
File::create(&file)
315-
.and_then(|mut f| f.write_all(new_code.as_bytes()))
311+
fs::write(&file, new_code)
316312
.with_context(|_| format!("failed to write file `{}`", file))?;
317313
original_files.insert(file, code);
318314
}
@@ -369,3 +365,110 @@ fn log_failed_fix(stderr: &[u8]) -> Result<(), Error> {
369365

370366
Ok(())
371367
}
368+
369+
#[derive(Default)]
370+
struct FixArgs {
371+
file: Option<PathBuf>,
372+
prepare_for_edition: Option<String>,
373+
enabled_edition: Option<String>,
374+
other: Vec<OsString>,
375+
}
376+
377+
impl FixArgs {
378+
fn get() -> FixArgs {
379+
let mut ret = FixArgs::default();
380+
for arg in env::args_os().skip(1) {
381+
let path = PathBuf::from(arg);
382+
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
383+
if path.exists() {
384+
ret.file = Some(path);
385+
continue
386+
}
387+
}
388+
if let Some(s) = path.to_str() {
389+
let prefix = "--edition=";
390+
if s.starts_with(prefix) {
391+
ret.enabled_edition = Some(s[prefix.len()..].to_string());
392+
continue
393+
}
394+
}
395+
ret.other.push(path.into());
396+
}
397+
if let Ok(s) = env::var(EDITION_ENV) {
398+
ret.prepare_for_edition = Some(s);
399+
}
400+
return ret
401+
}
402+
403+
fn apply(&self, cmd: &mut Command) {
404+
if let Some(path) = &self.file {
405+
cmd.arg(path);
406+
}
407+
cmd.args(&self.other)
408+
.arg("--cap-lints=warn");
409+
if let Some(edition) = &self.enabled_edition {
410+
cmd.arg("--edition").arg(edition);
411+
}
412+
if let Some(prepare_for) = &self.prepare_for_edition {
413+
cmd.arg("-W").arg(format!("rust-{}-compatibility", prepare_for));
414+
}
415+
}
416+
417+
/// Verify that we're not both preparing for an enabled edition and enabling
418+
/// the edition.
419+
///
420+
/// This indicates that `cargo fix --prepare-for` is being executed out of
421+
/// order with enabling the edition itself, meaning that we wouldn't
422+
/// actually be able to fix anything! If it looks like this is happening
423+
/// then yield an error to the user, indicating that this is happening.
424+
fn verify_not_preparing_for_enabled_edition(&self) -> CargoResult<()> {
425+
let edition = match &self.prepare_for_edition {
426+
Some(s) => s,
427+
None => return Ok(()),
428+
};
429+
let enabled = match &self.enabled_edition {
430+
Some(s) => s,
431+
None => return Ok(()),
432+
};
433+
if edition != enabled {
434+
return Ok(())
435+
}
436+
let path = match &self.file {
437+
Some(s) => s,
438+
None => return Ok(()),
439+
};
440+
441+
Message::EditionAlreadyEnabled {
442+
file: path.display().to_string(),
443+
edition: edition.to_string(),
444+
}.post()?;
445+
446+
process::exit(1);
447+
}
448+
449+
fn warn_if_preparing_probably_inert(&self) -> CargoResult<()> {
450+
let edition = match &self.prepare_for_edition {
451+
Some(s) => s,
452+
None => return Ok(()),
453+
};
454+
let path = match &self.file {
455+
Some(s) => s,
456+
None => return Ok(()),
457+
};
458+
let contents = match fs::read_to_string(path) {
459+
Ok(s) => s,
460+
Err(_) => return Ok(())
461+
};
462+
463+
let feature_name = format!("rust_{}_preview", edition);
464+
if contents.contains(&feature_name) {
465+
return Ok(())
466+
}
467+
Message::PreviewNotFound {
468+
file: path.display().to_string(),
469+
edition: edition.to_string(),
470+
}.post()?;
471+
472+
Ok(())
473+
}
474+
}

0 commit comments

Comments
 (0)