summaryrefslogtreecommitdiff
path: root/src/init.rs
blob: 94551c4c6962cdb7ccdffb0193b001498c8a90b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use anyhow::{bail, Context, Result};
use ratatui::crossterm::style::Stylize;
use std::{
    env::set_current_dir,
    fs::{self, create_dir},
    io::{self, Write},
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

use crate::{
    cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile,
    term::press_enter_prompt,
};

pub fn init() -> Result<()> {
    let rustlings_dir = Path::new("rustlings");
    if rustlings_dir.exists() {
        bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
    }

    let mut stdout = io::stdout().lock();
    let mut init_git = true;

    let manifest_path = Command::new("cargo")
        .args(["locate-project", "--message-format=plain"])
        .output()?;
    if manifest_path.status.success() {
        let manifest_path: PathBuf = String::from_utf8_lossy(&manifest_path.stdout).trim().into();

        if Path::new("exercises").exists() && Path::new("solutions").exists() {
            bail!(IN_INITIALIZED_DIR_ERR);
        }
        if fs::read_to_string(manifest_path)?.contains("[workspace]") {
            // make sure "rustlings" is added to `workspace.members` by making
            // cargo initialize a new project
            let output = Command::new("cargo").args(["new", "rustlings"]).output()?;
            if !output.status.success() {
                bail!("Failed to initilize new workspace member");
            }
            fs::remove_dir_all("rustlings")?;
            init_git = false;
        } else {
            bail!(IN_NON_WORKSPACE_CARGO_PROJECT_ERR);
        }
    }

    stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
    press_enter_prompt(&mut stdout)?;

    create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?;
    set_current_dir(rustlings_dir)
        .context("Failed to change the current directory to `rustlings/`")?;

    let info_file = InfoFile::parse()?;
    EMBEDDED_FILES
        .init_exercises_dir(&info_file.exercises)
        .context("Failed to initialize the `rustlings/exercises` directory")?;

    create_dir("solutions").context("Failed to create the `solutions/` directory")?;
    fs::write(
        "solutions/README.md",
        include_bytes!("../solutions/README.md"),
    )
    .context("Failed to create the file rustlings/solutions/README.md")?;
    for dir in EMBEDDED_FILES.exercise_dirs {
        let mut dir_path = String::with_capacity(10 + dir.name.len());
        dir_path.push_str("solutions/");
        dir_path.push_str(dir.name);
        create_dir(&dir_path)
            .with_context(|| format!("Failed to create the directory {dir_path}"))?;
    }
    for exercise_info in &info_file.exercises {
        let solution_path = exercise_info.sol_path();
        fs::write(&solution_path, INIT_SOLUTION_FILE)
            .with_context(|| format!("Failed to create the file {solution_path}"))?;
    }

    let current_cargo_toml = include_str!("../dev-Cargo.toml");
    // Skip the first line (comment).
    let newline_ind = current_cargo_toml
        .as_bytes()
        .iter()
        .position(|c| *c == b'\n')
        .context("The embedded `Cargo.toml` is empty or contains only one line")?;
    let current_cargo_toml = current_cargo_toml
        .get(newline_ind + 1..)
        .context("The embedded `Cargo.toml` contains only one line")?;
    let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"")
        .context("Failed to generate `Cargo.toml`")?;
    fs::write("Cargo.toml", updated_cargo_toml)
        .context("Failed to create the file `rustlings/Cargo.toml`")?;

    fs::write(".gitignore", GITIGNORE)
        .context("Failed to create the file `rustlings/.gitignore`")?;

    create_dir(".vscode").context("Failed to create the directory `rustlings/.vscode`")?;
    fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
        .context("Failed to create the file `rustlings/.vscode/extensions.json`")?;

    if init_git {
        // Ignore any Git error because Git initialization is not required.
        let _ = Command::new("git")
            .arg("init")
            .stdin(Stdio::null())
            .stderr(Stdio::null())
            .status();
    }

    writeln!(
        stdout,
        "\n{}\n\n{}",
        "Initialization done ✓".green(),
        POST_INIT_MSG.bold(),
    )?;

    Ok(())
}

const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
    // DON'T EDIT THIS SOLUTION FILE!
    // It will be automatically filled after you finish the exercise.
}
";

const GITIGNORE: &[u8] = b"Cargo.lock
target/
.vscode/
";

pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;

const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.

If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
Otherwise, please run `rustlings init` again in another directory.";

const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str =
    "A directory with the name `rustlings` already exists in the current directory.
You probably already initialized Rustlings.
Run `cd rustlings`
Then run `rustlings` again";

const IN_NON_WORKSPACE_CARGO_PROJECT_ERR: &str = "\
The current directory is already part of a cargo project.
Please initialize rustlings in a different directory.";

const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started.";