1
pub mod resources;
2

            
3
use crate::cli::input::TemplateValues;
4
use crate::project_model::compiler::CppCompiler;
5
use crate::utils;
6
use color_eyre::eyre::{bail, Context};
7
use color_eyre::{Report, Result};
8
use std::path::Path;
9
use std::process::Command;
10

            
11
/// Generates a new C++ standarized empty base project
12
/// with a pre-designed structure to organize the
13
/// user code in a modern fashion way.
14
///
15
/// Base template for the project files and folders:
16
///    - ./ifc
17
///        - math.<extension>
18
///    - ./src
19
///       - math.<extension -> .cpp, .cc, ...>
20
///       - math2.<extension -> .cpp, .cc, ...>
21
///    - main.cpp
22
///    - test/
23
///    - dependencies/
24
///
25
/// Note that this template is just a pnemonic. Any `C++` project can adhere to
26
/// whatever they feel that suits them better. Even tho, take in consideration
27
/// that legacy projects (headers + sources) can differ structurally to better reflect
28
/// what kind of responsabilities a translation unit has in the `C++` modules world.
29
3
pub fn create_templated_project(
30
    base_path: &Path,
31
    project_name: &str,
32
    git: bool,
33
    compiler: CppCompiler,
34
    template: TemplateValues,
35
) -> std::result::Result<(), Report> {
36
3
    let project_root = base_path.join(project_name);
37

            
38
3
    let path_ifc = project_root.join("ifc");
39
3
    let path_src = project_root.join("src");
40
3
    let path_test = project_root.join("test");
41
3
    let path_dependencies = project_root.join("deps");
42

            
43
3
    check_project_root_available(&project_root)?;
44

            
45
2
    utils::fs::create_directory(&project_root)?;
46
2
    utils::fs::create_directory(&path_ifc)?;
47
2
    utils::fs::create_directory(&path_src)?;
48
2
    utils::fs::create_directory(&path_test)?;
49
2
    utils::fs::create_directory(&path_dependencies)?;
50

            
51
2
    utils::fs::create_file(
52
2
        &path_ifc,
53
2
        &format!("{}.{}", "math", compiler.get_default_module_extension()),
54
2
        resources::IFC_MOD_FILE.as_bytes(),
55
2
    )?;
56

            
57
2
    match template {
58
        TemplateValues::BASIC => {
59
2
            utils::fs::create_file(&project_root, "main.cpp", resources::MAIN_BASIC.as_bytes())?;
60
        }
61
        TemplateValues::PARTITIONS => {
62
            utils::fs::create_file(
63
                &path_ifc,
64
                &format!(
65
                    "{}.{}",
66
                    "partitions",
67
                    compiler.get_default_module_extension()
68
                ),
69
                resources::IFC_PART_FILE.as_bytes(),
70
            )?;
71
            utils::fs::create_file(
72
                &path_ifc,
73
                &format!(
74
                    "{}.{}",
75
                    "interface_partition",
76
                    compiler.get_default_module_extension()
77
                ),
78
                resources::IFC_PART_PARTITION_FILE.as_bytes(),
79
            )?;
80
            utils::fs::create_file(
81
                &path_ifc,
82
                &format!("{}.{}", "internal_partition", "cpp"),
83
                resources::PARTITIONS_INTERNAL_PARTITION_FILE.as_bytes(),
84
            )?;
85
            utils::fs::create_file(&project_root, "main.cpp", resources::MAIN.as_bytes())?;
86
        }
87
    }
88

            
89
2
    utils::fs::create_file(&path_src, "math.cpp", resources::SRC_MOD_FILE.as_bytes())?;
90
2
    utils::fs::create_file(&path_src, "math2.cpp", resources::SRC_MOD_FILE_2.as_bytes())?;
91

            
92
4
    let template = match compiler {
93
        CppCompiler::MSVC => match template {
94
            TemplateValues::BASIC => resources::CONFIG_FILE_BASIC_MSVC,
95
            TemplateValues::PARTITIONS => resources::CONFIG_FILE_MSVC,
96
        },
97
2
        CppCompiler::CLANG => match template {
98
1
            TemplateValues::BASIC => resources::CONFIG_FILE_BASIC,
99
            TemplateValues::PARTITIONS => resources::CONFIG_FILE,
100
1
        },
101
2
        CppCompiler::GCC => match template {
102
1
            TemplateValues::BASIC => resources::CONFIG_FILE_BASIC_GCC,
103
            TemplateValues::PARTITIONS => resources::CONFIG_FILE_GCC,
104
1
        },
105
    }
106
    .replace("<project_name>", project_name);
107

            
108
2
    utils::fs::create_file(
109
2
        &project_root,
110
4
        &format!(
111
            "{}_{}.{}",
112
            utils::constants::CONFIG_FILE_NAME,
113
2
            compiler.as_ref(),
114
            utils::constants::CONFIG_FILE_EXT
115
        ),
116
2
        template.as_bytes(),
117
2
    )?;
118

            
119
2
    if git {
120
3
        initialize_git_repository(&project_root)?
121
    }
122

            
123
2
    Ok(())
124
3
}
125

            
126
3
fn check_project_root_available(project_root: &Path) -> Result<()> {
127
3
    if !project_root.exists() {
128
        // if it doesn't exist, there is nothing that would be overwritten
129
2
        return Ok(());
130
    }
131

            
132
1
    if !is_empty_directory(project_root)? {
133
1
        bail!("Directory {project_root:?} is not empty")
134
    }
135

            
136
    Ok(())
137
3
}
138

            
139
1
fn is_empty_directory(path: &Path) -> Result<bool> {
140
1
    if !path.is_dir() {
141
        return Ok(false);
142
    }
143

            
144
1
    let is_empty = path
145
        .read_dir()
146
        .with_context(|| format!("Directory {path:?} is not readable"))?
147
        .next()
148
1
        .is_none();
149

            
150
1
    Ok(is_empty)
151
1
}
152

            
153
fn initialize_git_repository(project_root: &Path) -> Result<()> {
154
    let exit_status = Command::new("git")
155
        .current_dir(project_root)
156
        .arg("init")
157
        .spawn()
158
        .with_context(|| "Could not run \"git init\"")?
159
        .wait()
160
        .with_context(|| "An error occurred while waiting for \"git init\" to finish")?;
161

            
162
    match exit_status.code() {
163
        Some(0) => {}
164
        None => bail!("Process \"git init\" was terminated by external signal"),
165
        Some(error_code) => bail!("Process \"git init\" returned {}", error_code),
166
    };
167

            
168
    Ok(())
169
}
170

            
171
#[cfg(test)]
172
mod tests {
173
    use color_eyre::Result;
174
    use tempfile::tempdir;
175

            
176
    use super::*;
177

            
178
    #[test]
179
2
    fn test_create_if_root_not_empty() -> Result<()> {
180
1
        let temp = tempdir()?;
181

            
182
        const PROJECT_NAME: &str = "example";
183

            
184
1
        let project_path = temp.path().join(PROJECT_NAME);
185
1
        let dummy_path = project_path.join("dummy.txt");
186

            
187
1
        std::fs::create_dir(project_path)?;
188
1
        std::fs::File::create(dummy_path)?;
189

            
190
1
        let result = create_templated_project(
191
1
            temp.path(),
192
            PROJECT_NAME,
193
            false,
194
1
            CppCompiler::CLANG,
195
1
            TemplateValues::BASIC,
196
        );
197
        assert!(
198
1
            result.is_err(),
199
            "The project was created, even though the project root is not empty"
200
        );
201

            
202
1
        Ok(())
203
2
    }
204
}