From 36a8e3ac0ee4f59ed587725e3257a79129a981e2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:29:41 +0100 Subject: [PATCH] Replace rust-project.json with Cargo.toml --- src/init.rs | 75 ++++++++++++++++++++++++++++++++++ src/main.rs | 108 +++++++++++++++++++++---------------------------- src/project.rs | 83 ------------------------------------- 3 files changed, 122 insertions(+), 144 deletions(-) create mode 100644 src/init.rs delete mode 100644 src/project.rs diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 00000000..66535354 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,75 @@ +use anyhow::{bail, Context, Result}; +use std::{ + env::set_current_dir, + fs::{create_dir, OpenOptions}, + io::{self, ErrorKind, Write}, + path::Path, +}; + +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; + +fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { + let mut cargo_toml = Vec::with_capacity(1 << 13); + cargo_toml.extend_from_slice( + br#"[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false +"#, + ); + for exercise in exercises { + cargo_toml.extend_from_slice(b"\n[[bin]]\nname = \""); + cargo_toml.extend_from_slice(exercise.name.as_bytes()); + cargo_toml.extend_from_slice(b"\"\npath = \""); + cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); + cargo_toml.extend_from_slice(b"\"\n"); + } + OpenOptions::new() + .create_new(true) + .write(true) + .open("Cargo.toml")? + .write_all(&cargo_toml) +} + +fn create_vscode_dir() -> Result<()> { + create_dir(".vscode").context("Failed to create the directory `.vscode`")?; + let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + OpenOptions::new() + .create_new(true) + .write(true) + .open(".vscode/extensions.json")? + .write_all(vs_code_extensions_json)?; + + Ok(()) +} + +pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { + let rustlings_path = Path::new("rustlings"); + if let Err(e) = create_dir(rustlings_path) { + if e.kind() == ErrorKind::AlreadyExists { + bail!( + "A directory with the name `rustligs` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again" + ); + } + return Err(e.into()); + } + + set_current_dir("rustlings") + .context("Failed to change the current directory to `rustlings`")?; + + EMBEDDED_FILES + .init_exercises_dir() + .context("Failed to initialize the `rustlings/exercises` directory")?; + + create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + println!("\nDone initialization!\n"); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 822cd1ad..36c36b54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use crate::exercise::{Exercise, ExerciseList}; -use crate::project::write_project_json; use crate::run::{reset, run}; use crate::verify::verify; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; use embedded::EMBEDDED_FILES; @@ -10,7 +9,7 @@ use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::io::{self, prelude::*, stdin, stdout}; +use std::io::{self, prelude::*}; use std::path::Path; use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -24,7 +23,7 @@ mod ui; mod embedded; mod exercise; -mod project; +mod init; mod run; mod verify; @@ -41,6 +40,8 @@ struct Args { #[derive(Subcommand)] enum Subcommands { + /// Initialize Rustlings + Init, /// Verify all exercises according to the recommended order Verify, /// Rerun `verify` when files were edited @@ -88,40 +89,6 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - .unwrap() - .exercises; - - if !Path::new("exercises").is_dir() { - let mut stdout = stdout().lock(); - write!( - stdout, - "The `exercises` directory wasn't found in the current directory. -Do you want to initialize Rustlings in the current directory (y/n)? " - )?; - stdout.flush()?; - let mut answer = String::new(); - stdin().read_line(&mut answer)?; - answer.make_ascii_lowercase(); - if answer.trim() != "y" { - exit(1); - } - - EMBEDDED_FILES.init_exercises_dir()?; - if let Err(e) = write_project_json(&exercises) { - writeln!( - stdout, - "Failed to write rust-project.json to disk for rust-analyzer: {e}" - )?; - } else { - writeln!(stdout, "Successfully generated rust-project.json")?; - writeln!( - stdout, - "rust-analyzer will now parse exercises, restart your language server or editor" - )?; - } - } - if args.command.is_none() { println!("\n{WELCOME}\n"); } @@ -133,14 +100,32 @@ Do you want to initialize Rustlings in the current directory (y/n)? " std::process::exit(1); } - let verbose = args.nocapture; + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + .unwrap() + .exercises; + if matches!(args.command, Some(Subcommands::Init)) { + init::init_rustlings(&exercises).context("Initialization failed")?; + println!("{DEFAULT_OUT}\n"); + return Ok(()); + } else if !Path::new("exercises").is_dir() { + println!( + "\nThe `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings and want to initialize it, +run the command `rustlings init`" + ); + exit(1); + } + + let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); std::process::exit(0); }); match command { + // `Init` is handled above. + Subcommands::Init => (), Subcommands::List { paths, names, @@ -421,9 +406,16 @@ fn watch( } } -const DEFAULT_OUT: &str = "Thanks for installing Rustlings! +const WELCOME: &str = r" welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/"; -Is this your first time? Don't worry, Rustlings was made for beginners! We are +const DEFAULT_OUT: &str = + "Is this your first time? Don't worry, Rustlings was made for beginners! We are going to teach you a lot of things about Rust, but before we can get started, here's a couple of notes about how Rustlings operates: @@ -446,8 +438,20 @@ started, here's a couple of notes about how Rustlings operates: 5. If you want to use `rust-analyzer` with exercises, which provides features like autocompletion, run the command `rustlings lsp`. -Got all that? Great! To get started, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"; +Got all that? Great! To get started, go into the new directory `rustlings` by +running `cd rustlings`. +Then, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the new `rustlings` directory!"; + +const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: + hint - prints the current exercise's hint + clear - clears the screen + quit - quits watch mode + ! - executes a command, like `!rustc --explain E0381` + help - displays this help message + +Watch mode automatically re-evaluates the current exercise +when you edit a file's contents."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | @@ -475,21 +479,3 @@ You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; - -const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: - hint - prints the current exercise's hint - clear - clears the screen - quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; diff --git a/src/project.rs b/src/project.rs deleted file mode 100644 index bb6caa58..00000000 --- a/src/project.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::env; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -use crate::exercise::Exercise; - -/// Contains the structure of resulting rust-project.json file -/// and functions to build the data required to create the file -#[derive(Serialize)] -struct RustAnalyzerProject<'a> { - sysroot_src: PathBuf, - crates: Vec>, -} - -#[derive(Serialize)] -struct Crate<'a> { - root_module: &'a Path, - edition: &'static str, - // Not used, but required in the JSON file. - deps: Vec<()>, - // Only `test` is used for all crates. - // Therefore, an array is used instead of a `Vec`. - cfg: [&'static str; 1], -} - -impl<'a> RustAnalyzerProject<'a> { - fn build(exercises: &'a [Exercise]) -> Result { - let crates = exercises - .iter() - .map(|exercise| Crate { - root_module: &exercise.path, - edition: "2021", - deps: Vec::new(), - // This allows rust_analyzer to work inside `#[test]` blocks - cfg: ["test"], - }) - .collect(); - - if let Some(path) = env::var_os("RUST_SRC_PATH") { - return Ok(Self { - sysroot_src: PathBuf::from(path), - crates, - }); - } - - let toolchain = Command::new("rustc") - .arg("--print") - .arg("sysroot") - .stderr(Stdio::inherit()) - .output() - .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? - .stdout; - - let toolchain = - String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; - let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); - - let mut sysroot_src = PathBuf::with_capacity(256); - sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - - Ok(Self { - sysroot_src, - crates, - }) - } -} - -/// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: &[Exercise]) -> Result<()> { - let content = RustAnalyzerProject::build(exercises)?; - - // Using the capacity 2^14 since the file length in bytes is higher than 2^13. - // The final length is not known exactly because it depends on the user's sysroot path, - // the current number of exercises etc. - let mut buf = Vec::with_capacity(1 << 14); - serde_json::to_writer(&mut buf, &content)?; - std::fs::write("rust-project.json", buf)?; - - Ok(()) -}