Move logic into main binary

This commit is contained in:
Jack Clayton 2022-01-12 12:11:06 +08:00
parent f0bcb4a816
commit a831aa7982
7 changed files with 185 additions and 76 deletions

View File

@ -21,10 +21,6 @@ home = "0.5.3"
name = "rustlings" name = "rustlings"
path = "src/main.rs" path = "src/main.rs"
[[bin]]
name = "fix-rust-analyzer"
path = "src/fix-rust-analyzer.rs"
[dev-dependencies] [dev-dependencies]
assert_cmd = "0.11.0" assert_cmd = "0.11.0"
predicates = "1.0.1" predicates = "1.0.1"

View File

@ -1,72 +0,0 @@
use glob::glob;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct RustProject {
sysroot_src: String,
crates: Vec<Crate>,
}
#[derive(Serialize, Deserialize)]
struct Crate {
root_module: String,
edition: String,
deps: Vec<String>,
}
impl RustProject {
fn new() -> RustProject {
RustProject {
sysroot_src: RustProject::get_sysroot_src(),
crates: Vec::new(),
}
}
fn get_sysroot_src() -> String {
let mut sysroot_src = home::rustup_home()
.expect("Can't find Rustup... aborting")
.to_string_lossy()
.to_string();
use std::process::Command;
let output = Command::new("rustup")
.arg("default")
.output()
.expect("Failed to get rustup default toolchain");
let toolchain = String::from_utf8_lossy(&output.stdout).to_string();
sysroot_src += "/toolchains/";
sysroot_src += toolchain
.split_once(' ')
.expect("Malformed default toolchain path")
.0;
sysroot_src += "/lib/rustlib/src/rust/library";
println!("{}", sysroot_src);
sysroot_src
}
}
fn main() {
let mut project = RustProject::new();
for e in glob("./exercises/**/*").expect("Glob failed to read pattern") {
let path = e
.expect("Unable to extract path")
.to_string_lossy()
.to_string();
if let Some((_, ext)) = path.split_once(".") {
if ext == "rs" {
project.crates.push(Crate {
deps: Vec::new(),
edition: "2021".to_string(),
root_module: path,
})
}
}
}
std::fs::write(
"./rust-project.json",
serde_json::to_vec(&project).expect("Failed to serialize to JSON"),
)
.expect("Failed to write file");
}

150
src/fix_rust_analyzer.rs Normal file
View File

@ -0,0 +1,150 @@
/// because `rustlings` is a special type of project where we don't have a
/// cargo.toml linking to each exercise, we need a way to tell `rust-analyzer`
/// how to parse the exercises. This functionality is built into rust-analyzer
/// by putting a `rust-project.json` at the root of the repository. This module generates
/// that file by finding the default toolchain used and looping through each exercise
/// to build the configuration in a way that allows rust-analyzer to work with the exercises.
use glob::glob;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
use std::process::Command;
/// Custom error to check if io error or rust-analyzer just doesn't exist
/// if rust-analyzer doesn't exist don't want to panic, want to print
/// message to console and continue
pub enum RustAnalyzerError {
IoError(std::io::Error),
NoRustAnalyzerError,
}
impl fmt::Display for RustAnalyzerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid first item to double")
}
}
impl RustAnalyzerError {
fn from_io(err: std::io::Error) -> RustAnalyzerError {
RustAnalyzerError::IoError(err)
}
}
/// Contains the structure of resulting rust-project.json file
/// and functions to build the data required to create the file
#[derive(Serialize, Deserialize)]
pub struct RustAnalyzerProject {
sysroot_src: String,
crates: Vec<Crate>,
}
#[derive(Serialize, Deserialize)]
struct Crate {
root_module: String,
edition: String,
deps: Vec<String>,
}
impl RustAnalyzerProject {
pub fn new() -> RustAnalyzerProject {
RustAnalyzerProject {
sysroot_src: String::new(),
crates: Vec::new(),
}
}
/// If path contains .rs extension, add a crate to `rust-project.json`
fn path_to_json(&mut self, path: String) {
if let Some((_, ext)) = path.split_once(".") {
if ext == "rs" {
self.crates.push(Crate {
root_module: path,
edition: "2021".to_string(),
deps: Vec::new(),
})
}
}
}
/// Write rust-project.json to disk
pub fn write_to_disk(&self) -> Result<(), std::io::Error> {
std::fs::write(
"./rust-project.json",
serde_json::to_vec(&self).expect("Failed to serialize to JSON"),
)?;
Ok(())
}
/// Parse the exercises folder for .rs files, any matches will create
/// a new `crate` in rust-project.json which allows rust-analyzer to
/// treat it like a normal binary
pub fn exercies_to_json(&mut self) -> Result<(), Box<dyn Error>> {
let glob = glob("./exercises/**/*")?;
for e in glob {
let path = e?.to_string_lossy().to_string();
self.path_to_json(path);
}
Ok(())
}
/// Run `rust-analyzer --version` to check if rust analyzer exists, if it doesn't
/// then return custom error
pub fn check_rust_analyzer_exists(&self) -> Result<(), RustAnalyzerError> {
match Command::new("rust-analyzer").arg("--version").output() {
Ok(out) => {
if out.stderr.len() > 0 {
return Err(RustAnalyzerError::NoRustAnalyzerError);
} else {
return Ok(());
}
}
Err(err) => Err(RustAnalyzerError::from_io(err)),
}
}
/// Use `rustup` command to determine the default toolchain, if it exists
/// it will be put in RustAnalyzerProject.sysroot_src, otherwise an error will be returned
pub fn get_sysroot_src(&mut self) -> Result<(), Box<dyn Error>> {
let mut sysroot_src = home::rustup_home()?.to_string_lossy().to_string();
let output = Command::new("rustup").arg("default").output()?;
let toolchain = String::from_utf8_lossy(&output.stdout).to_string();
sysroot_src += "/toolchains/";
sysroot_src += toolchain
.split_once(' ')
.ok_or("Unable to determine toolchain")?
.0;
sysroot_src += "/lib/rustlib/src/rust/library";
println!(
"Determined toolchain to use with Rustlings: {}",
sysroot_src
);
self.sysroot_src = sysroot_src;
Ok(())
}
}
#[test]
fn parses_exercises() {
let mut rust_project = RustAnalyzerProject::new();
rust_project
.exercies_to_json()
.expect("Failed to parse exercises");
assert_eq!(rust_project.crates.len() > 0, true);
}
#[test]
fn check_exists() {
let rust_project = RustAnalyzerProject::new();
match rust_project.check_rust_analyzer_exists() {
Ok(_) => (),
Err(err) => match err {
RustAnalyzerError::NoRustAnalyzerError => {
println!("Correctly identifying rust-analyzer doesn't exist")
}
RustAnalyzerError::IoError(err) => panic!("io error: {}", err),
},
}
}

View File

@ -1,4 +1,5 @@
use crate::exercise::{Exercise, ExerciseList}; use crate::exercise::{Exercise, ExerciseList};
use crate::fix_rust_analyzer::{RustAnalyzerError, RustAnalyzerProject};
use crate::run::run; use crate::run::run;
use crate::verify::verify; use crate::verify::verify;
use argh::FromArgs; use argh::FromArgs;
@ -20,6 +21,7 @@ use std::time::Duration;
mod ui; mod ui;
mod exercise; mod exercise;
mod fix_rust_analyzer;
mod run; mod run;
mod verify; mod verify;
@ -37,6 +39,9 @@ struct Args {
version: bool, version: bool,
#[argh(subcommand)] #[argh(subcommand)]
nested: Option<Subcommands>, nested: Option<Subcommands>,
/// skip rust-analyzer fix check
#[argh(switch, short = 'x')]
skipfix: bool,
} }
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
@ -128,6 +133,10 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
if !Path::new("rust-project.json").exists() && !args.skipfix {
fix_rust_analyzer();
}
if !rustc_exists() { if !rustc_exists() {
println!("We cannot find `rustc`."); println!("We cannot find `rustc`.");
println!("Try running `rustc --version` to diagnose your problem."); println!("Try running `rustc --version` to diagnose your problem.");
@ -404,3 +413,26 @@ fn rustc_exists() -> bool {
.map(|status| status.success()) .map(|status| status.success())
.unwrap_or(false) .unwrap_or(false)
} }
fn fix_rust_analyzer() {
let mut rust_project = RustAnalyzerProject::new();
if let Err(err) = rust_project.check_rust_analyzer_exists() {
match err {
RustAnalyzerError::NoRustAnalyzerError => {
println!("rust-analyzer doesn't exist, skipping fix")
}
RustAnalyzerError::IoError(err) => {
println!("error trying to find rust-analyzer: {}", err)
}
}
} else {
println!("rust-analyzer exists, fixing to work with rustlings");
if let Err(err) = rust_project.get_sysroot_src() {
println!("Error getting toolchain path: {}\nContinuing... rust-analyzer won't work with rustlings", &err)
}
if let Err(err) = rust_project.exercies_to_json() {
println!("Error parsing exercises and converting to json: {}\nContinuing... rust-analyzer won't work with rustlings", &err)
}
rust_project.write_to_disk();
}
}

View File

@ -0,0 +1 @@
{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]}

View File

@ -0,0 +1 @@
{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]}

View File

@ -0,0 +1 @@
{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]}