diff --git a/Cargo.lock b/Cargo.lock index 65d22162..4c95ca85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,15 +311,6 @@ dependencies = [ "libc", ] -[[package]] -name = "gen-dev-cargo-toml" -version = "0.0.0" -dependencies = [ - "anyhow", - "serde", - "toml_edit", -] - [[package]] name = "hashbrown" version = "0.14.3" diff --git a/Cargo.toml b/Cargo.toml index 06fbbf70..cde41823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,6 @@ exclude = [ "tests/fixture/success", "dev", ] -members = [ - "gen-dev-cargo-toml", -] [workspace.package] version = "6.0.0-alpha.0" @@ -20,11 +17,6 @@ authors = [ license = "MIT" edition = "2021" -[workspace.dependencies] -anyhow = "1.0.82" -serde = { version = "1.0.197", features = ["derive"] } -toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } - [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" @@ -42,15 +34,15 @@ include = [ ] [dependencies] -anyhow.workspace = true +anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } -serde.workspace = true -toml_edit.workspace = true +serde = { version = "1.0.197", features = ["derive"] } +toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } which = "6.0.1" [dev-dependencies] diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 1d230ebb..e66973e0 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,6 +1,4 @@ -# This file is a hack to allow using `cargo run` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. - +# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update` bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, diff --git a/gen-dev-cargo-toml/Cargo.toml b/gen-dev-cargo-toml/Cargo.toml deleted file mode 100644 index 8922ae8c..00000000 --- a/gen-dev-cargo-toml/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "gen-dev-cargo-toml" -publish = false -license.workspace = true -edition.workspace = true - -[dependencies] -anyhow.workspace = true -serde.workspace = true -toml_edit.workspace = true diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs deleted file mode 100644 index 43b4ebd7..00000000 --- a/gen-dev-cargo-toml/src/main.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Generates `dev/Cargo.toml` such that it is synced with `info.toml`. -// `dev/Cargo.toml` is a hack to allow using `cargo run` to test `rustlings` -// during development. - -use anyhow::{bail, Context, Result}; -use serde::Deserialize; -use std::{ - fs::{self, create_dir}, - io::ErrorKind, -}; - -#[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: Option, -} - -#[derive(Deserialize)] -struct InfoFile { - exercises: Vec, -} - -fn main() -> Result<()> { - let exercise_infos = toml_edit::de::from_str::( - &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, - ) - .context("Failed to deserialize `info.toml`")? - .exercises; - - let mut buf = Vec::with_capacity(1 << 14); - - buf.extend_from_slice( - b"# This file is a hack to allow using `cargo run` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. - -bin = [\n", - ); - - for exercise_info in exercise_infos { - buf.extend_from_slice(b" { name = \""); - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b"\", path = \"../exercises/"); - if let Some(dir) = &exercise_info.dir { - buf.extend_from_slice(dir.as_bytes()); - buf.push(b'/'); - } - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b".rs\" },\n"); - } - - buf.extend_from_slice( - br#"] - -[package] -name = "rustlings-dev" -edition = "2021" -publish = false -"#, - ); - - if let Err(e) = create_dir("dev") { - if e.kind() != ErrorKind::AlreadyExists { - bail!("Failed to create the `dev` directory: {e}"); - } - } - - fs::write("dev/Cargo.toml", buf).context("Failed to write `dev/Cargo.toml`") -} diff --git a/src/dev.rs b/src/dev.rs index 7905f387..feca99e5 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,22 +1,23 @@ use anyhow::{Context, Result}; use clap::Subcommand; -use crate::info_file::InfoFile; - mod check; mod init; +mod update; #[derive(Subcommand)] pub enum DevCommands { Init, Check, + Update, } impl DevCommands { - pub fn run(self, info_file: InfoFile) -> Result<()> { + pub fn run(self) -> Result<()> { match self { DevCommands::Init => init::init().context(INIT_ERR), - DevCommands::Check => check::check(info_file), + DevCommands::Check => check::check(), + DevCommands::Update => update::update(), } } } diff --git a/src/dev/check.rs b/src/dev/check.rs index 5910a75a..bc8e4592 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,16 +1,83 @@ +use anyhow::{bail, Context, Result}; use std::fs; -use anyhow::{Context, Result}; +use crate::{ + info_file::{ExerciseInfo, InfoFile}, + DEVELOPING_OFFIFICAL_RUSTLINGS, +}; -use crate::{info_file::InfoFile, init::cargo_toml}; +pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { + let start_ind = cargo_toml + .find("bin = [") + .context("Failed to find the start of the `bin` list (`bin = [`)")? + + 7; + let end_ind = start_ind + + cargo_toml + .get(start_ind..) + .and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']')) + .context("Failed to find the end of the `bin` list (`]`)")?; + + Ok((start_ind, end_ind)) +} + +pub fn append_bins( + buf: &mut Vec, + exercise_infos: &[ExerciseInfo], + exercise_path_prefix: &[u8], +) { + buf.push(b'\n'); + for exercise_info in exercise_infos { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"exercises/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); + } +} + +fn check_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; + let mut new_bins = Vec::with_capacity(1 << 13); + append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + + if old_bins != new_bins { + bail!("`Cargo.toml` is outdated. Run `rustlings dev update` to update it"); + } + + Ok(()) +} + +pub fn check() -> Result<()> { + let info_file = InfoFile::parse()?; -pub fn check(info_file: InfoFile) -> Result<()> { // TODO: Add checks - // TODO: Keep dependencies! - fs::write("Cargo.toml", cargo_toml(&info_file.exercises)) - .context("Failed to update the file `Cargo.toml`")?; - println!("Updated `Cargo.toml`"); + if DEVELOPING_OFFIFICAL_RUSTLINGS { + check_cargo_toml( + &info_file.exercises, + include_str!("../../dev/Cargo.toml"), + b"../", + ) + .context("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it")?; + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"").context( + "The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it", + )?; + } println!("\nEverything looks fine!"); diff --git a/src/dev/init.rs b/src/dev/init.rs index 09935220..3ce5055c 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -1,6 +1,5 @@ -use std::fs::{self, create_dir}; - use anyhow::{Context, Result}; +use std::fs::{self, create_dir}; use crate::CURRENT_FORMAT_VERSION; @@ -19,11 +18,8 @@ pub fn init() -> Result<()> { ) .context("Failed to create the file `rustlings/info.toml`")?; - fs::write( - "rustlings/Cargo.toml", - format!("{CARGO_TOML_COMMENT}{}", crate::init::CARGO_TOML_PACKAGE), - ) - .context("Failed to create the file `rustlings/Cargo.toml`")?; + fs::write("rustlings/Cargo.toml", CARGO_TOML) + .context("Failed to create the file `rustlings/Cargo.toml`")?; fs::write("rustlings/.gitignore", crate::init::GITIGNORE) .context("Failed to create the file `rustlings/.gitignore`")?; @@ -80,10 +76,17 @@ mode = "test" hint = """???""" "#; -const CARGO_TOML_COMMENT: &str = - "# You shouldn't edit this file manually! It is updated by `rustlings dev check` +const CARGO_TOML: &[u8] = + br#"# Don't edit the `bin` list manually! It is updated by `rustlings dev update` +bin = [] -"; +[package] +name = "rustlings" +edition = "2021" +publish = false + +[dependencies] +"#; const README: &str = "# Rustlings 🦀 diff --git a/src/dev/update.rs b/src/dev/update.rs new file mode 100644 index 00000000..981934d6 --- /dev/null +++ b/src/dev/update.rs @@ -0,0 +1,53 @@ +use std::fs; + +use anyhow::{Context, Result}; + +use crate::{ + info_file::{ExerciseInfo, InfoFile}, + DEVELOPING_OFFIFICAL_RUSTLINGS, +}; + +use super::check::{append_bins, bins_start_end_ind}; + +fn update_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + cargo_toml_path: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let mut new_cargo_toml = Vec::with_capacity(1 << 13); + new_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); + append_bins(&mut new_cargo_toml, exercise_infos, exercise_path_prefix); + new_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); + + fs::write(cargo_toml_path, new_cargo_toml).context("Failed to write the `Cargo.toml` file")?; + + Ok(()) +} + +pub fn update() -> Result<()> { + let info_file = InfoFile::parse()?; + + if DEVELOPING_OFFIFICAL_RUSTLINGS { + update_cargo_toml( + &info_file.exercises, + include_str!("../../dev/Cargo.toml"), + "dev/Cargo.toml", + b"../", + ) + .context("Failed to update the file `dev/Cargo.toml`")?; + + println!("Updated `dev/Cargo.toml`"); + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, "Cargo.toml", b"") + .context("Failed to update the file `Cargo.toml`")?; + + println!("Updated `Cargo.toml`"); + } + + Ok(()) +} diff --git a/src/exercise.rs b/src/exercise.rs index 8bdf399f..c4df9999 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -9,6 +9,7 @@ use std::{ use crate::{ embedded::{WriteStrategy, EMBEDDED_FILES}, info_file::Mode, + DEVELOPING_OFFIFICAL_RUSTLINGS, }; pub struct TerminalFileLink<'a> { @@ -50,9 +51,7 @@ impl Exercise { cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - // Use `dev/Cargo.toml` when in the directory of the repository. - #[cfg(debug_assertions)] - if std::path::Path::new("tests").exists() { + if DEVELOPING_OFFIFICAL_RUSTLINGS { cmd.arg("--manifest-path").arg("dev/Cargo.toml"); } diff --git a/src/init.rs b/src/init.rs index 5fa44d40..52315e24 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,30 +6,19 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; +use crate::embedded::EMBEDDED_FILES; -pub fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec { - let mut cargo_toml = Vec::with_capacity(1 << 13); - cargo_toml.extend_from_slice(b"bin = [\n"); - for exercise_info in exercise_infos { - cargo_toml.extend_from_slice(b" { name = \""); - cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); - cargo_toml.extend_from_slice(b"\", path = \"exercises/"); - if let Some(dir) = &exercise_info.dir { - cargo_toml.extend_from_slice(dir.as_bytes()); - cargo_toml.push(b'/'); - } - cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); - cargo_toml.extend_from_slice(b".rs\" },\n"); +const CARGO_TOML: &[u8] = { + let cargo_toml = include_bytes!("../dev/Cargo.toml"); + // Skip the first line (comment). + let mut start_ind = 0; + while cargo_toml[start_ind] != b'\n' { + start_ind += 1; } + cargo_toml.split_at(start_ind + 1).1 +}; - cargo_toml.extend_from_slice(b"]\n\n"); - cargo_toml.extend_from_slice(CARGO_TOML_PACKAGE.as_bytes()); - - cargo_toml -} - -pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { +pub fn init() -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } @@ -49,7 +38,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - fs::write("Cargo.toml", cargo_toml(exercise_infos)) + fs::write("Cargo.toml", CARGO_TOML) .context("Failed to create the file `rustlings/Cargo.toml`")?; fs::write(".gitignore", GITIGNORE) @@ -64,12 +53,6 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { Ok(()) } -pub const CARGO_TOML_PACKAGE: &str = r#"[package] -name = "rustlings" -edition = "2021" -publish = false -"#; - pub const GITIGNORE: &[u8] = b"Cargo.lock .rustlings-state.txt target diff --git a/src/main.rs b/src/main.rs index 8b3f28f9..ea5f7c92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,17 @@ mod watch; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; const CURRENT_FORMAT_VERSION: u8 = 1; +const DEVELOPING_OFFIFICAL_RUSTLINGS: bool = { + #[allow(unused_assignments, unused_mut)] + let mut debug_profile = false; + + #[cfg(debug_assertions)] + { + debug_profile = true; + } + + debug_profile +}; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] @@ -66,17 +77,11 @@ fn main() -> Result<()> { which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; - let info_file = InfoFile::parse()?; - - if info_file.format_version > CURRENT_FORMAT_VERSION { - bail!(FORMAT_VERSION_HIGHER_ERR); - } - match args.command { Some(Subcommands::Init) => { - return init::init(&info_file.exercises).context("Initialization failed"); + return init::init().context("Initialization failed"); } - Some(Subcommands::Dev(dev_command)) => return dev_command.run(info_file), + Some(Subcommands::Dev(dev_command)) => return dev_command.run(), _ => (), } @@ -85,6 +90,12 @@ fn main() -> Result<()> { exit(1); } + let info_file = InfoFile::parse()?; + + if info_file.format_version > CURRENT_FORMAT_VERSION { + bail!(FORMAT_VERSION_HIGHER_ERR); + } + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs deleted file mode 100644 index 81f48b1e..00000000 --- a/tests/dev_cargo_bins.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Makes sure that `dev/Cargo.toml` is synced with `info.toml`. -// When this test fails, you just need to run `cargo run -p gen-dev-cargo-toml`. - -use serde::Deserialize; -use std::fs; - -#[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: Option, -} - -#[derive(Deserialize)] -struct InfoFile { - exercises: Vec, -} - -#[test] -fn dev_cargo_bins() { - let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap(); - - let exercise_infos = - toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) - .unwrap() - .exercises; - - let mut start_ind = 0; - for exercise_info in exercise_infos { - let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1; - let name_end = name_start + cargo_toml[name_start..].find('"').unwrap(); - assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]); - - let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2; - let path_end = path_start + cargo_toml[path_start..].find('"').unwrap(); - let expected_path = if let Some(dir) = exercise_info.dir { - format!("../exercises/{dir}/{}.rs", exercise_info.name) - } else { - format!("../exercises/{}.rs", exercise_info.name) - }; - assert_eq!(expected_path, &cargo_toml[path_start..path_end]); - - start_ind = path_end + 1; - } -}