cargo_v5/commands/migrate/
mod.rs1use std::{
2 borrow::Cow,
3 env::{self, home_dir},
4 fmt::Display,
5 io::ErrorKind,
6 path::{Path, PathBuf},
7};
8
9use fs_err::tokio as fs;
10use miette::Diagnostic;
11use semver::Version;
12use supports_color::Stream;
13use thiserror::Error;
14use tokio::{process::Command, task::block_in_place};
15use toml_edit::{Document, DocumentMut, Item, Table, Value, table};
16
17use crate::errors::CliError;
18
19mod source_code;
20mod vfs;
21
22pub async fn migrate_workspace(root: &Path) -> Result<(), CliError> {
24 let metadata_task = block_in_place(|| {
25 cargo_metadata::MetadataCommand::new()
26 .current_dir(root)
27 .exec()
28 .ok()
29 });
30
31 let Some(metadata) = metadata_task else {
32 return Err(MigrateError::Metadata.into());
33 };
34
35 let mut ctx = ChangesCtx::new(&metadata.workspace_root);
36
37 update_vexide(&mut ctx).await?;
38 update_rust(&mut ctx).await?;
39 update_cargo_config(&mut ctx).await?;
40 source_code::update_targets(&mut ctx, &metadata).await?;
41
42 let highlight = supports_color::on_cached(Stream::Stdout).is_some();
44
45 println!(
46 "The upgrade tool will now update your project configuration to the vexide 0.8.0 recommended defaults."
47 );
48 println!(
49 "After applying these changes, make sure to check out the upgrade guide on the vexide website"
50 );
51 println!("for instructions on how to update your project's code!");
52 println!("Changes Summary:");
53 for desc in &ctx.description {
54 println!(" - {desc}");
55 }
56 if ctx.description.is_empty() {
57 println!(" - (No changes)");
58 println!();
59 return Ok(());
60 }
61 println!();
62
63 loop {
64 let confirmation: inquire::Select<'_, ConfirmOptions> = inquire::Select::new(
65 "Apply changes?",
66 vec![
67 ConfirmOptions::Confirm,
68 ConfirmOptions::ViewDiff,
69 ConfirmOptions::Abort,
70 ],
71 );
72
73 let reply = block_in_place(|| confirmation.prompt_skippable())?.unwrap_or_default();
74
75 match reply {
76 ConfirmOptions::Confirm => {
77 ctx.apply().await?;
78 break;
79 }
80 ConfirmOptions::ViewDiff => println!("{}", ctx.fs.display(true, highlight).await),
81 ConfirmOptions::Abort => {
82 break;
83 }
84 }
85 }
86
87 Ok(())
88}
89
90#[derive(Default)]
91enum ConfirmOptions {
92 Confirm,
93 ViewDiff,
94 #[default]
95 Abort,
96}
97
98impl Display for ConfirmOptions {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(match self {
101 ConfirmOptions::Confirm => "Confirm",
102 ConfirmOptions::ViewDiff => "View Changes",
103 ConfirmOptions::Abort => "Abort",
104 })
105 }
106}
107
108async fn update_rust(ctx: &mut ChangesCtx) -> Result<(), CliError> {
109 ctx.edit_toml("rust-toolchain.toml", |mut ctx| {
110 let latest = "nightly-2025-11-26";
111
112 let toolchain = ctx.document.table("toolchain");
113 toolchain["channel"] = latest.into();
114 ctx.explain_change(format!("Updated to Rust {}", latest));
115 })
116 .await?;
117
118 let has_override = rustup_has_override_for_path(ctx.fs.root())
119 .await
120 .unwrap_or(false);
121 if has_override {
122 ctx.will_disable_rustup_override = has_override;
123 ctx.describe("Disabled the Rustup override for this project.");
124 }
125
126 Ok(())
127}
128
129async fn rustup_has_override_for_path(path: &Path) -> Option<bool> {
130 let absolute_path = fs::canonicalize(path).await.ok()?;
131
132 let mut rustup_home = env::var("RUSTUP_HOME").ok().map(PathBuf::from);
133 if rustup_home.is_none() {
134 rustup_home = home_dir().map(|dir| dir.join(".rustup"));
135 }
136
137 let settings_path = rustup_home?.join("settings.toml");
138 let contents = fs::read_to_string(settings_path).await.ok()?;
139
140 let settings = Document::parse(contents).ok()?;
141
142 let overrides = settings.get("overrides")?.as_table()?;
143
144 let has_override_for_path = overrides.contains_key(absolute_path.to_str()?);
145
146 Some(has_override_for_path)
147}
148
149async fn update_cargo_config(ctx: &mut ChangesCtx) -> Result<(), CliError> {
152 ctx.edit_toml(".cargo/config.toml", |mut ctx| {
153 let build = ctx.document.table("build");
155 build.remove("target");
156 ctx.explain_change("Enabled desktop unit testing");
157
158 let rustflags = vec!["-Clink-arg=-Tvexide.ld"];
161
162 let build = ctx.document.table("build");
163 if let Some(old_rustflags) = build.get_mut("rustflags")
164 && let Some(flag_array) = old_rustflags.as_array_mut()
165 {
166 #[rustfmt::skip]
170 flag_array.retain(|item| {
171 let is_vexide_flag = rustflags.iter().any(|&vexide_flag| {
174 item.as_str().is_some_and(|flag| flag == vexide_flag)
175 });
176
177 !is_vexide_flag
178 });
179
180 if flag_array.is_empty() {
181 build.remove("rustflags");
182 }
183 }
184
185 let target = ctx.document.table("target");
187 target.set_position(-1); let this_target = target.table(r#"cfg(target_os = "vexos")"#);
190 this_target["rustflags"] = Value::from_iter(rustflags).into();
191
192 ctx.explain_change("Enabled the vexide v0.8.0 memory layout");
193
194 let unstable = ctx.document.table("unstable");
196 unstable["build-std"] = Value::from_iter(vec!["std", "panic_abort"]).into();
197 unstable["build-std-features"] = Value::from_iter(vec!["compiler-builtins-mem"]).into();
198 ctx.explain_change("Added the Rust Standard Library as a dependency");
199 })
200 .await?;
201
202 ctx.fs.delete_if_exists("armv7a-vex-v5.json").await?;
203
204 Ok(())
205}
206
207async fn update_vexide(ctx: &mut ChangesCtx) -> Result<(), CliError> {
208 let latest = "0.8.0";
209
210 ctx.edit_toml("Cargo.toml", |mut ctx| {
211 _ = ctx
213 .document
214 .table("package")
215 .insert("edition", "2024".to_string().into());
216 ctx.explain_change("Updated to Rust 2024 edition");
217
218 let old_entry = ctx
219 .document
220 .get("dependencies")
221 .and_then(|d| d.get("vexide"));
222
223 let old_version = old_entry
224 .and_then(|v| v.get("version"))
225 .and_then(|d| d.as_str());
226
227 if let Some(old_version) = old_version
228 && let Ok(current) = Version::parse(old_version)
229 {
230 let supported_by_tool = Version::new(0, 7, 0);
231 let latest = Version::parse(latest).unwrap();
232
233 let is_eligible = current < latest && current >= supported_by_tool;
234 println!("eligible for upgrade: {is_eligible}");
235 if !is_eligible {
236 log::warn!("vexide v{current} not eligible for upgrade");
237 return;
238 }
239 }
240
241 let old_features_array = old_entry
242 .and_then(|v| v.get("features"))
243 .and_then(|d| d.as_array());
244
245 let default_features = old_entry
246 .and_then(|v| v.get("default-features"))
247 .and_then(|d| d.as_bool())
248 .unwrap_or(true);
249
250 let mut features = Vec::<Value>::new();
251 let mut use_default_sdk = default_features;
252
253 if default_features {
254 features.push("full".into());
255 }
256
257 if let Some(old_features_array) = old_features_array {
260 for item in old_features_array {
261 let Some(mut feature) = item.as_str() else {
262 continue;
263 };
264
265 feature = match feature {
267 "dangerous_motor_tuning" => "dangerous-motor-tuning",
268 "backtraces" => "backtrace",
269 "macro" => "macros",
270 "display_panics" => "panic-hook",
271 "force_rust_libm" | "smart_leds_trait" | "panic" => continue, other => other,
273 };
274
275 if feature == "startup" {
276 use_default_sdk = true;
277 }
278
279 features.push(feature.into());
280 }
281 }
282
283 if use_default_sdk {
284 features.retain(|f| f.as_str().is_none_or(|s| !s.starts_with("vex-sdk")));
286 features.push("default-sdk".into());
287 }
288
289 features.dedup_by(|l_feature, r_feature| {
291 l_feature
292 .as_str()
293 .is_some_and(|l| r_feature.as_str() == Some(l))
294 });
295
296 let dependencies = ctx.document.table("dependencies");
297
298 dependencies.remove("vexide");
299
300 let mut vexide = Table::new();
301
302 println!("new version: {latest}");
303 vexide["version"] = latest.into();
304 vexide["features"] = Value::from_iter(features).into();
305 if !default_features {
306 vexide["default-features"] = default_features.into();
307 }
308
309 dependencies["vexide"] = vexide.into_inline_table().into();
310
311 ctx.explain_change(format!("Updated to vexide {latest}"));
312 })
313 .await
314}
315
316#[derive(Debug, Error, Diagnostic)]
317pub enum MigrateError {
318 #[error("failed to parse toml file")]
319 #[diagnostic(code(cargo_v5::upgrade::invalid_toml_file))]
320 TomlParse(#[from] toml_edit::TomlError),
321 #[error("Cannot determine the current Cargo workspace")]
322 #[diagnostic(code(cargo_v5::upgrade::no_metadata))]
323 Metadata,
324}
325
326struct ChangesCtx {
327 fs: vfs::FileOperationStore,
328 will_disable_rustup_override: bool,
329 description: Vec<String>,
330}
331
332impl ChangesCtx {
333 pub fn new(root: impl Into<PathBuf>) -> Self {
334 Self {
335 fs: vfs::FileOperationStore::new(root),
336 will_disable_rustup_override: false,
337 description: vec![],
338 }
339 }
340
341 pub async fn edit_toml(
342 &mut self,
343 path: impl AsRef<Path>,
344 editor: impl for<'a> FnOnce(EditTomlCtx<'a>),
345 ) -> Result<(), CliError> {
346 let path = path.as_ref();
347 let (mut doc, old_contents) = open_or_create_toml(&mut self.fs, path).await?;
348
349 let ctx = EditTomlCtx {
350 changes: self,
351 document: &mut doc,
352 previous_version: Cow::Borrowed(old_contents.as_deref().unwrap_or_default()),
353 };
354 editor(ctx);
355
356 let new_file = doc.to_string();
357 if old_contents.as_ref() == Some(&new_file) {
358 return Ok(()); }
360
361 self.fs.write(path, new_file).await?;
362
363 Ok(())
364 }
365
366 pub fn describe(&mut self, change: impl Into<String>) {
367 self.description.push(change.into());
368 }
369
370 pub async fn apply(&mut self) -> Result<(), CliError> {
371 self.fs.apply().await?;
372
373 if self.will_disable_rustup_override {
374 let mut cmd = Command::new("rustup");
375
376 cmd.arg("override")
377 .arg("unset")
378 .arg("--path")
379 .arg(self.fs.root());
380
381 let status = cmd.spawn()?.wait().await?;
382 if !status.success() {
383 log::warn!(
384 "Disabling the rustup override for the project directory was unsuccessful"
385 );
386 }
387 }
388
389 Ok(())
390 }
391}
392
393struct EditTomlCtx<'a> {
394 pub changes: &'a mut ChangesCtx,
395 pub document: &'a mut DocumentMut,
396 previous_version: Cow<'a, str>,
397}
398
399impl EditTomlCtx<'_> {
400 pub fn explain_change(&mut self, change: impl Into<String>) {
405 let new_version = self.document.to_string();
406
407 if self.previous_version == new_version {
408 return; }
410
411 self.changes.describe(change);
412 self.previous_version = Cow::Owned(new_version);
413 }
414}
415
416trait TableExt {
417 fn table(&mut self, key: &str) -> &mut Table;
418}
419
420impl TableExt for Table {
421 fn table(&mut self, key: &str) -> &mut Table {
422 let value = self.entry(key).or_insert_with(table);
423
424 *value = std::mem::take(value)
426 .into_table()
427 .unwrap_or_default()
428 .into();
429
430 let table_ref = value.as_table_mut().unwrap();
431 table_ref.set_implicit(true);
432 table_ref
433 }
434}
435
436async fn open_or_create_toml(
437 files: &mut vfs::FileOperationStore,
438 path: &Path,
439) -> Result<(DocumentMut, Option<String>), CliError> {
440 let file = files.read_to_string(&path).await;
441
442 let doc = match file {
444 Ok(contents) => {
445 let toml = contents
446 .parse::<DocumentMut>()
447 .map_err(MigrateError::from)?;
448 (toml, Some(contents))
449 }
450 Err(err) if err.kind() == ErrorKind::NotFound => (DocumentMut::new(), None),
451 Err(other) => return Err(other)?,
452 };
453
454 Ok(doc)
455}
456
457#[allow(unused)]
458fn toml_item_eq_strings(toml: Option<&Item>, strings: &[&str]) -> bool {
459 toml.and_then(|f| f.as_array())
460 .map(|array| {
461 array
462 .into_iter()
463 .map(|f| f.as_str().unwrap_or_default())
464 .eq(strings.iter().cloned())
465 })
466 .unwrap_or_default()
467}