diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..4b014006 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --manifest-path xtask/Cargo.toml --" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..cd3d347a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,228 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "color-eyre", + "eyre", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..8da4b657 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = [ + "xtask", +] +exclude = ["example-code/"] + +resolver = "2" + diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..8c33bbb0 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +color-eyre = "0.6.3" +eyre = "0.6.12" diff --git a/xtask/README.md b/xtask/README.md new file mode 100644 index 00000000..470c7bf2 --- /dev/null +++ b/xtask/README.md @@ -0,0 +1,79 @@ +# Cheatsheets + +## Why + +Teaching Rust can be hard, and it's harder when trainees we teach come from different programming language backgrounds. + +A person in Python might have more questions about memory safety in general, since the GC allows them not to worry about that, but a person from C++ would be confused by the keyword `move` in a closure. + +To alleviate this, we've created some cheatsheets of the form + +``` +# Applied Rust + +## Methods and Traits +... +## Rust I/O Traits +... +## Generics +... +``` + +per programming language that match each slide we have in our normal syllabus with an entry on the second header (`## Methods and Traits`, for example) level. + +## How + +As `training-material` grows and changes, maintenance could be a nightmare. We basically don't want to be in the business of remembering that a certain slide got reordered or moved from one section to another and thus needs changing in all the cheatsheets as well. Therefore, this tool seeks to alleviate that with the following workflow: + +* call `cargo xtask make-cheatsheet python` at the root folder +* scrape Markdown headers in `SUMMARY.md` and segment topics by `Rust Fundamentals`, `Applied Rust` and `Advanced Rust` +* write out to `src/python-cheatsheet.md` if it doesn't exist +* if it does exist, check that it in sync: all headers in `python-cheatsheet.md` are in the appropriate sections, in order, and none are missing. + +Specifically, `make-cheatsheet` and `test-cheatsheet` are defined in `xtask/src/tasks.rs` with utility functions to take our `SUMMARY.md` + +``` +# Rust Fundamentals + * [Overview](./src/overview.md) + * [Installation](.src/installation.md) + * [Basic Types](./src/basic-types.md) +... +# Applied Rust + * [Methods and Traits](./src/methods-and-traits.md) + * [Rust I/O Traits](./src/rust-io-traits.md) + * [Generics](./src/generics.md) +``` + +and convert it into a `Vec`: + +```rust + vec![SlideSection {header: "Rust Fundamentals", + slide_titles: vec!["Overview", "Installation", "Basic Types"]}, + SlideSection {header: "Applied Rust", + slide_titles: vec!["Methods and Traits", "Rust I/O Traits", "Generics"]}] +``` + +From there we can + +* create the cheatsheet for Python and have it written out to `training-slides/src/python-cheatsheet.md` by just iterating over `Vec` and prefixing with the appropriate header level before printing +* test that the cheathseet is in sync by scraping for all the lines that start with `#` in `python-cheatsheet.md` and check that they match, in order, those we scraped from `SUMMARY.md`. + +Note: some languages will warrant some special entries - any headers after the last `SlideSection` header will be ignored, +so that we can add additional relevant information without having to conform to the slides. + +Concretely, this is allowed: + +```md + +# Applied Rust +## Methods and Traits +## Rust I/O Traits +## Generics +# FAQ +## How to do... +# Syntax Clashes +## Operator overloading +... +``` + +but the code will signal if `# FAQ` or `# Syntax Clashes` appear before `# Applied Rust`. diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..aa86bb6c --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,45 @@ +#![deny(warnings)] + +mod tasks; + +use std::env; + +static HELP_TEXT: &'static str = "cargo xtask + +USAGE: + cargo xtask [COMMAND] + +COMMANDS: + make-cheatsheet [LANG] make LANG cheatsheet by scraping slides names in `SUMMARY.md` + test-cheatsheet [LANG] test LANG's cheatsheet (all `SUMMARY.md` items are in sheet and viceversa) + +LANG: + Valid values are `python, go, cpp, swift, java, julia, c` +"; + +// Code adapted from the xtask workflow in rust-exercises +fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + + // first arg is the name of the executable; skip it + let unprocessed_args = env::args().skip(1).collect::>(); + let args: Vec<&str> = unprocessed_args.iter().map(|arg| &arg[..]).collect::>(); + + let langs = ["python", "go", "cpp", "swift", "java", "julia", "c"]; + + if !langs.contains(&args[1]) { + let panic_text = format!("{} {}\n{}\n", args[1], "is not a valid language name. \n\nExpected one of:", langs.join("\n")); + panic!("{panic_text}"); + } + + match &args[..] { + ["make-cheatsheet", lang] => tasks::make_cheatsheet(lang), + ["test-cheatsheet", lang] => tasks::test_cheatsheet(lang), + _ => { + eprintln!("{HELP_TEXT}"); + + Ok(()) + } + } +} + diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs new file mode 100644 index 00000000..0f424b9e --- /dev/null +++ b/xtask/src/tasks.rs @@ -0,0 +1,268 @@ +use std::{ + fs::{read_to_string, File}, + io::Write, + path::Path, +}; + +#[derive(Debug, Eq, PartialEq)] +struct SlidesSection { + header: String, + slide_titles: Vec, +} + +fn get_slide_name(line: &str) -> String { + assert!(line.starts_with("* [")); + assert!(line.ends_with(".md)")); + // SAFETY + // This line should be a well formed mdbook entry: `* [TEXT](./foo.md)` + let top = line + .rfind(']') + .expect("the markdown file entry did not have a ']'"); + let bot = line + .find('[') + .expect("the markdown file entry did not have a '['"); + String::from(&line[bot + 1..top]) +} + +const INITIAL_HEADER: &str = "# Rust Fundamentals"; +const LAST_HEADER: &str = "# No-Std Rust"; + +fn focus_regions(text: &str) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut current_section: Vec = Vec::new(); + + if !text.contains(INITIAL_HEADER) { + panic!("Your INITIAL_HEADER is not part of the input. Check your `SUMMARY.md` for {INITIAL_HEADER}"); + } + if !text.contains(LAST_HEADER) { + panic!("YOUR LAST_HEADER is not part of the text input. CHECK your `SUMMARY.md` for {LAST_HEADER}"); + } + + let Some(first_header) = text.find(INITIAL_HEADER) else { + panic!("Could not find the initial header {INITIAL_HEADER:?}. Check your `SUMMARY.md`.") + }; + let Some(last_header) = text.rfind(LAST_HEADER) else { + panic!("Could not find the last header {LAST_HEADER:?}. Check your `SUMMARY.md`.") + }; + + let text = &text[first_header..last_header]; + + for line in text.lines() { + let trimmed_line = line.trim(); + if trimmed_line.is_empty() + || (!trimmed_line.starts_with('*') && !trimmed_line.starts_with('#')) + { + continue; + } + + if trimmed_line.starts_with("# ") && !current_section.is_empty() { + result.push(current_section); + current_section = Vec::new(); + } + current_section.push(trimmed_line.to_string()); + } + + if !current_section.is_empty() { + result.push(current_section); + } + + result +} + +fn extract_slides(chunk: Vec) -> SlidesSection { + assert!(chunk.len() > 2); + // # Rust Fundamentals + // ^ 3rd character in title + let header = String::from(&chunk[0][2..]); + + let slide_titles = chunk[1..] + .iter() + .map(|l| get_slide_name(l)) + .collect::>(); + + SlidesSection { + header, + slide_titles, + } +} + +pub fn make_cheatsheet(lang: &str) -> Result<(), eyre::Report> { + // Collect slide sections, chunked by header + let text = read_to_string("./training-slides/src/SUMMARY.md").expect("SUMMARY.md not found"); + let slide_texts = focus_regions(&text); + let slide_sections: Vec = slide_texts + .iter() + .map(|l| extract_slides(l.clone())) + .collect(); + + // Check to see if a file exists + let file_str = format!("./training-slides/src/{lang}-cheatsheet.md"); + let new_file = Path::new(&file_str); + + // If lang-cheatsheet.md exists, check if any headers are missing + // Otherwise, create the lang-cheatsheet.md + match File::create_new(new_file) { + Ok(mut f) => { + let result_text = write_cheatsheet(slide_sections); + drop(f.write_all(result_text.as_bytes())); + eprintln!("Cheatsheat for {lang} written at {file_str}"); + eprintln!("Make sure to add it to SUMMARY.md!") + } + Err(_) => { + eprintln!("File {lang}-cheatsheet.md already exists - checking it's in sync"); + drop(test_cheatsheet(lang)); + } + } + Ok(()) +} + +pub fn test_cheatsheet(lang: &str) -> Result<(), eyre::Report> { + // Collect Vec + let text = read_to_string("./training-slides/src/SUMMARY.md") + .expect("could not read_to_string - SUMMARY.md not found"); + let slide_texts = focus_regions(&text); + let slide_sections: Vec = slide_texts + .iter() + .map(|l| extract_slides(l.clone())) + .collect(); + + // Collect SlideSections and slide titles + let file_name = format!("./training-slides/src/{lang}-cheatsheet.md"); + let cheatsheet_text = read_to_string(file_name).expect("lang-cheatsheet.md not found"); + let cheatsheet_lines = cheatsheet_text + .lines() + // Tricky: We only care about entries that start with '#', so filtering for them is enough to get only the + // interesting lines! + .filter(|l| l.starts_with("#")) + .map(|l| l.to_string()) + .collect::>(); + + let mut missing_files = false; + let mut idx = 0; + for line in cheatsheet_lines.iter() { + if line.starts_with("# ") { + if line != cheatsheet_lines.first().unwrap() { + idx += 1; + // Check if people have added extra headers - leave them alone + // so that lang - specific advice doesn't have to correlate to slides + // if it goes at the end + if idx == slide_sections.len() { + eprintln!("Neat! {lang}-cheatsheet.md is in sync AND contains some extra info at the end"); + return Ok(()); + } + } + let header = line.strip_prefix("# ").unwrap(); + if header != slide_sections[idx].header { + eprintln!("{} header should be {}", line, slide_sections[idx].header); + missing_files = true; + } + } + if line.starts_with("## ") { + let slide_title = line + .strip_prefix("## ") + .expect("Expected the line to start with `## `"); + if !(slide_sections[idx].slide_titles).contains(&slide_title.to_string()) { + eprintln!( + "{} is not in {lang}-cheathseet.md under expected header {}", + slide_title, slide_sections[idx].header + ); + missing_files = true; + } + } + } + if missing_files { + panic!("You have missing slides"); + } else { + eprintln!("Neat! {lang}-cheatsheet.md is in sync"); + Ok(()) + } +} + +fn write_cheatsheet(slide_sections: Vec) -> String { + let mut res = String::new(); + for slide in slide_sections.iter() { + let mut section_str_buf = format!("# {}\n", slide.header); + for entry in slide.slide_titles.iter() { + let slide_title = format!("## {entry}\n"); + section_str_buf.push_str(&slide_title); + } + section_str_buf.push('\n'); + res.push_str(§ion_str_buf); + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_slide_name() { + let test = "* [Methods and Traits](./methods-traits.md)"; + let res = "Methods and Traits"; + assert_eq!(res, get_slide_name(test)); + + let test2 = "* [Shared Mutability (Cell, RefCell)](./shared-mutability.md)"; + let res2 = "Shared Mutability (Cell, RefCell)"; + assert_eq!(res2, get_slide_name(test2)); + } + + #[test] + fn test_focus_regions() { + let test = "# Summary + +[Start Here](./start_here.md) + +# Rust Fundamentals + +* [Overview](./overview.md) + +# Applied Rust + +Using Rust on Windows/macOS/Linux. Requires [Rust Fundamentals](#rust-fundamentals). + +* [Methods and Traits](./methods-traits.md) + +# Advanced Rust + +Topics that go beyond [Applied Rust](#applied-rust). + +* [Advanced Strings](./advanced-strings.md) + +# No-Std Rust + +Rust for the Linux Kernel and other no-std environments with an pre-existing C API. Requires [Applied Rust](#applied-rust). +"; + let res = vec![ + vec![ + "# Rust Fundamentals".to_owned(), + "* [Overview](./overview.md)".to_owned(), + ], + vec![ + "# Applied Rust".to_owned(), + "* [Methods and Traits](./methods-traits.md)".to_owned(), + ], + vec![ + "# Advanced Rust".to_owned(), + "* [Advanced Strings](./advanced-strings.md)".to_owned(), + ], + ]; + assert_eq!(focus_regions(test), res); + } + #[test] + fn test_extract_slides() { + let test = "# Rust Fundamentals +* [Rust I/O Traits](./io.md) +* [Generics](./generics.md) +# No-Std Rust"; + let header = String::from("Rust Fundamentals"); + let slide_titles = vec![String::from("Rust I/O Traits"), String::from("Generics")]; + let res = SlidesSection { + header, + slide_titles, + }; + let region = focus_regions(test); + assert_eq!(extract_slides(region[0].clone()), res); + assert!(true); + } +}