Skip to main content

typr_cli/
project.rs

1//! Project management utilities for TypR CLI
2//!
3//! Provides functions for:
4//! - Creating new projects
5//! - Building and checking projects
6//! - Running tests
7//! - Package management
8
9use crate::engine::{parse_code, write_std_for_type_checking};
10use crate::io::execute_r_with_path;
11use std::fs;
12use std::fs::File;
13use std::fs::OpenOptions;
14use std::io::Write;
15use std::path::Path;
16use std::path::PathBuf;
17use std::process::Command;
18use typr_core::components::context::config::Environment;
19use typr_core::components::context::Context;
20use typr_core::processes::type_checking::type_checker::TypeChecker;
21use typr_core::typing;
22
23pub fn write_header(context: Context, output_dir: &PathBuf, environment: Environment) -> () {
24    let type_anotations = context.get_type_anotations();
25    let mut app = match environment {
26        Environment::Repl => OpenOptions::new()
27            .append(true)
28            .create(true)
29            .write(true)
30            .open(output_dir.join(".repl.R")),
31        _ => OpenOptions::new().create(true).write(true).open(
32            output_dir
33                .join(context.get_environment().to_base_path())
34                .join("c_types.R"),
35        ),
36    }
37    .unwrap();
38
39    app.write_all(type_anotations.as_bytes()).unwrap();
40
41    let generic_functions = context
42        .get_all_generic_functions()
43        .iter()
44        .map(|(var, _)| var.get_name())
45        .filter(|x| !x.contains("<-"))
46        .map(|fn_name| {
47            format!(
48                "#' @export\n{} <- function(x, ...) UseMethod('{}', x)",
49                fn_name,
50                fn_name.replace("`", "")
51            )
52        })
53        .collect::<Vec<_>>()
54        .join("\n");
55    let mut app = match environment {
56        Environment::Repl => OpenOptions::new()
57            .append(true)
58            .create(true)
59            .write(true)
60            .open(output_dir.join(".repl.R")),
61        _ => OpenOptions::new().create(true).write(true).open(
62            output_dir
63                .join(context.get_environment().to_string())
64                .join("b_generic_functions.R"),
65        ),
66    }
67    .unwrap();
68    app.write_all((generic_functions + "\n").as_bytes())
69        .unwrap();
70}
71
72pub fn write_to_r_lang(
73    content: String,
74    output_dir: &PathBuf,
75    file_name: &str,
76    environment: Environment,
77) -> () {
78    let rstd = include_str!("../configs/src/std.R");
79    let std_path = output_dir.join("a_std.R");
80    let mut rstd_file = File::create(std_path).unwrap();
81    rstd_file.write_all(rstd.as_bytes()).unwrap();
82
83    let app_path = output_dir.join(file_name);
84    let mut app = match environment {
85        Environment::Repl => OpenOptions::new()
86            .append(true)
87            .write(true)
88            .create(true)
89            .open(app_path),
90        _ => File::create(app_path),
91    }
92    .unwrap();
93    let source = match environment {
94        Environment::Project | Environment::Repl | Environment::Wasm => "",
95        Environment::StandAlone => "source('b_generic_functions.R')\nsource('c_types.R')",
96    };
97    app.write_all(format!("{}\n{}", source, content).as_bytes())
98        .unwrap();
99}
100
101pub fn new(name: &str) {
102    println!("Creating the R package '{}'...", name);
103
104    let current_dir = match std::env::current_dir() {
105        Ok(dir) => dir,
106        Err(e) => {
107            eprintln!("Error obtaining current directory: {}", e);
108            std::process::exit(1);
109        }
110    };
111
112    let project_path = current_dir.join(name);
113
114    if let Err(e) = fs::create_dir(&project_path) {
115        eprintln!("Error creating project directory: {}", e);
116        std::process::exit(1);
117    }
118
119    // Classic architecture of a R package
120    let package_folders = vec![
121        "R",         // R code
122        "TypR",      // TypR code
123        "man",       // Documentation
124        "tests",     // Tests
125        "data",      // Data
126        "inst",      // Installed files
127        "src",       // Source code (C++, Fortran, etc.)
128        "vignettes", // Vignettes/tutorials
129    ];
130
131    for folder in package_folders {
132        let folder_path = project_path.join(folder);
133        if let Err(e) = fs::create_dir(&folder_path) {
134            eprintln!(
135                "Warning: Unable to create the folder {}: {}",
136                folder_path.display(),
137                e
138            );
139        }
140    }
141
142    let tests_testthat = project_path.join("tests/testthat");
143    if let Err(e) = fs::create_dir(&tests_testthat) {
144        eprintln!("Warning: Unable to create the tests/testthat folder: {}", e);
145    }
146
147    let package_files = vec![
148        (
149            "DESCRIPTION",
150            include_str!("../configs/DESCRIPTION").replace("{{PACKAGE_NAME}}", name),
151        ),
152        (
153            "NAMESPACE",
154            include_str!("../configs/NAMESPACE").replace("{{PACKAGE_NAME}}", name),
155        ),
156        (
157            ".Rbuildignore",
158            include_str!("../configs/.Rbuildignore").replace("{{PACKAGE_NAME}}", name),
159        ),
160        (
161            ".gitignore",
162            include_str!("../configs/.gitignore").replace("{{PACKAGE_NAME}}", name),
163        ),
164        (
165            "TypR/main.ty",
166            include_str!("../configs/main.ty").replace("{{PACKAGE_NAME}}", name),
167        ),
168        (
169            "R/.gitkeep",
170            include_str!("../configs/.gitkeep").replace("{{PACKAGE_NAME}}", name),
171        ),
172        (
173            "tests/testthat.R",
174            include_str!("../configs/testthat.R").replace("{{PACKAGE_NAME}}", name),
175        ),
176        (
177            "man/.gitkeep",
178            include_str!("../configs/.gitkeep2").replace("{{PACKAGE_NAME}}", name),
179        ),
180        (
181            "README.md",
182            include_str!("../configs/README.md").replace("{{PACKAGE_NAME}}", name),
183        ),
184        (
185            "rproj.Rproj",
186            include_str!("../configs/rproj.Rproj").to_string(),
187        ),
188    ];
189
190    for (file_path, content) in package_files {
191        let full_path = project_path.join(file_path);
192        if let Some(parent) = full_path.parent() {
193            if let Err(e) = fs::create_dir_all(parent) {
194                eprintln!(
195                    "Warning: Unable to create parent directory {}: {}",
196                    parent.display(),
197                    e
198                );
199                continue;
200            }
201        }
202        println!("Writing {} in '{:?}'", content.len(), full_path);
203        if let Err(e) = fs::write(&full_path, content) {
204            eprintln!(
205                "Warning: Unable to create parent directory {}: {}",
206                full_path.display(),
207                e
208            );
209        }
210    }
211
212    println!("Package R '{}' successfully created!", name);
213    let package_structure =
214        include_str!("../configs/package_structure.md").replace("{{PACKAGE_NAME}}", name);
215    println!("{}", package_structure);
216
217    let instructions = include_str!("../configs/instructions.md").replace("{{PACKAGE_NAME}}", name);
218    println!("{}", instructions);
219}
220
221pub fn check_project() {
222    let context = Context::default().set_environment(Environment::Project);
223    let lang = parse_code(&PathBuf::from("TypR/main.ty"), context.get_environment());
224    let _ = typing(&context, &lang);
225    println!("Code verification successful!");
226}
227
228pub fn check_file(path: &PathBuf) {
229    let context = Context::default().set_environment(Environment::Project);
230    let lang = parse_code(path, context.get_environment());
231    let dir = PathBuf::from(".");
232    write_std_for_type_checking(&dir);
233    let type_checker = TypeChecker::new(context.clone()).typing(&lang);
234    if type_checker.has_errors() {
235        std::process::exit(1);
236    }
237    println!("File verification {:?} successful!", path);
238}
239
240pub fn build_project() {
241    let dir = PathBuf::from(".");
242    let context = Context::default().set_environment(Environment::Project);
243    let lang = parse_code(&PathBuf::from("TypR/main.ty"), context.get_environment());
244    let type_checker = TypeChecker::new(context.clone()).typing(&lang);
245
246    let content = type_checker.clone().transpile();
247    write_header(type_checker.get_context(), &dir, Environment::Project);
248    write_to_r_lang(
249        content,
250        &PathBuf::from("R"),
251        "d_main.R",
252        context.get_environment(),
253    );
254    document();
255    println!("R code successfully generated in the R/ folder");
256}
257
258pub fn build_file(path: &PathBuf) {
259    let lang = parse_code(path, Environment::StandAlone);
260    let dir = PathBuf::from(".");
261
262    write_std_for_type_checking(&dir);
263    let context = Context::default();
264    let type_checker = TypeChecker::new(context.clone()).typing(&lang);
265    let r_file_name = path
266        .file_name()
267        .unwrap()
268        .to_str()
269        .unwrap()
270        .replace(".ty", ".R");
271    let content = type_checker.clone().transpile();
272    write_header(type_checker.get_context(), &dir, Environment::StandAlone);
273    write_to_r_lang(content, &dir, &r_file_name, context.get_environment());
274    println!("Generated R code: {:?}", dir.join(&r_file_name));
275}
276
277pub fn run_project() {
278    build_project();
279    execute_r_with_path(&PathBuf::from("R"), "main.R");
280}
281
282pub fn run_file(path: &PathBuf) {
283    let lang = parse_code(path, Environment::StandAlone);
284    let dir = PathBuf::from(".");
285
286    write_std_for_type_checking(&dir);
287    let context = Context::default();
288    let type_checker = TypeChecker::new(context.clone()).typing(&lang);
289    let r_file_name = path
290        .file_name()
291        .unwrap()
292        .to_str()
293        .unwrap()
294        .replace(".ty", ".R");
295    let content = type_checker.clone().transpile();
296    write_header(type_checker.get_context(), &dir, Environment::StandAlone);
297    write_to_r_lang(content, &dir, &r_file_name, context.get_environment());
298    execute_r_with_path(&dir, &r_file_name);
299}
300
301pub fn test() {
302    build_project();
303    let r_command = "devtools::test()".to_string();
304
305    println!("Execution of: R -e \"{}\"", r_command);
306
307    let output = Command::new("R").arg("-e").arg(&r_command).output();
308
309    match output {
310        Ok(output) => {
311            if output.status.success() {
312                if !output.stdout.is_empty() {
313                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
314                }
315            } else {
316                eprintln!("Error while running tests");
317                if !output.stderr.is_empty() {
318                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
319                }
320                std::process::exit(1);
321            }
322        }
323        Err(e) => {
324            eprintln!("Error while executing R command: {}", e);
325            eprintln!("Make sure devtools is installed");
326            std::process::exit(1);
327        }
328    }
329}
330
331pub fn get_package_name() -> Result<String, String> {
332    let description_path = PathBuf::from("DESCRIPTION");
333
334    if !description_path.exists() {
335        return Err("DESCRIPTION file not found. Are you at the project root?".to_string());
336    }
337
338    let content = fs::read_to_string(&description_path)
339        .map_err(|e| format!("Error reading file DESCRIPTION: {}", e))?;
340
341    for line in content.lines() {
342        if line.starts_with("Package:") {
343            let package_name = line.replace("Package:", "").trim().to_string();
344            return Ok(package_name);
345        }
346    }
347
348    Err("Package name not found in the DESCRIPTION file".to_string())
349}
350
351pub fn pkg_install() {
352    println!("Installing the package...");
353
354    let current_dir = match std::env::current_dir() {
355        Ok(dir) => dir,
356        Err(e) => {
357            eprintln!("Error obtaining current directory: {}", e);
358            std::process::exit(1);
359        }
360    };
361
362    let project_path = current_dir.to_str().unwrap();
363    let r_command = format!("devtools::install_local('{}')", project_path);
364    println!("Executing: R -e \"{}\"", r_command);
365
366    let output = Command::new("R").arg("-e").arg(&r_command).output();
367
368    match output {
369        Ok(output) => {
370            if output.status.success() {
371                println!("Package installed successfully!");
372
373                if !output.stdout.is_empty() {
374                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
375                }
376            } else {
377                eprintln!("Error during package installation");
378                if !output.stderr.is_empty() {
379                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
380                }
381
382                std::process::exit(1);
383            }
384        }
385        Err(e) => {
386            eprintln!("Error executing command R: {}", e);
387            eprintln!("Make sure that R and devtools are installed.");
388            std::process::exit(1);
389        }
390    }
391}
392
393pub fn pkg_uninstall() {
394    println!("Uninstalling the package...");
395
396    let package_name = match get_package_name() {
397        Ok(name) => name,
398        Err(e) => {
399            eprintln!("Error: {}", e);
400            std::process::exit(1);
401        }
402    };
403
404    println!("Uninstalling the package '{}'...", package_name);
405    let r_command = format!("remove.packages('{}')", package_name);
406    println!("Executing: R -e \"{}\"", r_command);
407
408    let output = Command::new("R").arg("-e").arg(&r_command).output();
409
410    match output {
411        Ok(output) => {
412            if output.status.success() {
413                println!("Package '{}' successfully uninstalled!", package_name);
414
415                if !output.stdout.is_empty() {
416                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
417                }
418            } else {
419                eprintln!(
420                    "Note: The package '{}' may not have been installed or an error may have occurred",
421                    package_name
422                );
423
424                if !output.stderr.is_empty() {
425                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
426                }
427            }
428        }
429        Err(e) => {
430            eprintln!("Error executing command R: {}", e);
431            eprintln!("Make sure that R is installed.");
432            std::process::exit(1);
433        }
434    }
435}
436
437pub fn document() {
438    println!("Generating package documentation...");
439
440    let current_dir = match std::env::current_dir() {
441        Ok(dir) => dir,
442        Err(e) => {
443            eprintln!("Error obtaining current directory: {}", e);
444            std::process::exit(1);
445        }
446    };
447
448    let project_path = current_dir.to_str().unwrap();
449    let r_command = format!("devtools::document('{}')", project_path);
450
451    let output = Command::new("R").arg("-e").arg(&r_command).output();
452
453    match output {
454        Ok(output) => {
455            if output.status.success() {
456                println!("Documentation successfully generated!");
457
458                if !output.stdout.is_empty() {
459                    println!("")
460                }
461            } else {
462                eprintln!("Error while generating documentation");
463
464                if !output.stderr.is_empty() {
465                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
466                }
467
468                std::process::exit(1);
469            }
470        }
471        Err(e) => {
472            eprintln!("Error while executing the R command : {}", e);
473            eprintln!("Be sure that R et devtools are installed.");
474            std::process::exit(1);
475        }
476    }
477}
478
479pub fn use_package(package_name: &str) {
480    println!("Adding the package '{}' as a dependency...", package_name);
481    let r_command = format!("devtools::use_package('{}')", package_name);
482    println!("Execution of: R -e \"{}\"", r_command);
483
484    let output = Command::new("R").arg("-e").arg(&r_command).output();
485
486    match output {
487        Ok(output) => {
488            if output.status.success() {
489                println!(
490                    "Package '{}' successfully added to dependencies!",
491                    package_name
492                );
493
494                if !output.stdout.is_empty() {
495                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
496                }
497            } else {
498                eprintln!("Error adding package '{}'", package_name);
499
500                if !output.stderr.is_empty() {
501                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
502                }
503
504                std::process::exit(1);
505            }
506        }
507        Err(e) => {
508            eprintln!("Error executing command R: {}", e);
509            eprintln!("Make sure that R and devtools are installed.");
510            std::process::exit(1);
511        }
512    }
513}
514
515pub fn load() {
516    let r_command = "devtools::load_all('.')".to_string();
517
518    println!("Execution of: R -e \"{}\"", r_command);
519
520    let output = Command::new("R").arg("-e").arg(&r_command).output();
521
522    match output {
523        Ok(output) => {
524            if output.status.success() {
525                println!("Elements loaded with success!");
526                if !output.stdout.is_empty() {
527                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
528                }
529            } else {
530                eprintln!("Error while loading elements");
531                if !output.stderr.is_empty() {
532                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
533                }
534
535                std::process::exit(1);
536            }
537        }
538        Err(e) => {
539            eprintln!("Error while executing R command: {}", e);
540            eprintln!("Make sure devtools is installed");
541            std::process::exit(1);
542        }
543    }
544}
545
546pub fn cran() {
547    let r_command = "devtools::check()".to_string();
548    println!("Execution of: R -e \"{}\"", r_command);
549
550    let output = Command::new("R").arg("-e").arg(&r_command).output();
551
552    match output {
553        Ok(output) => {
554            if output.status.success() {
555                println!("Checks passed with success!");
556                if !output.stdout.is_empty() {
557                    println!("\n{}", String::from_utf8_lossy(&output.stdout));
558                }
559            } else {
560                eprintln!("Error while checking the project");
561                if !output.stderr.is_empty() {
562                    eprintln!("\n{}", String::from_utf8_lossy(&output.stderr));
563                }
564
565                std::process::exit(1);
566            }
567        }
568        Err(e) => {
569            eprintln!("Error while executing R command: {}", e);
570            eprintln!("Make sure devtools is installed");
571            std::process::exit(1);
572        }
573    }
574}
575
576pub fn clean() {
577    let folder = Path::new(".");
578    if folder.is_dir() {
579        for entry_result in fs::read_dir(folder).unwrap() {
580            let entry = entry_result.unwrap();
581            let path = entry.path();
582            if let Some(file_name) = path.file_name() {
583                if let Some(str_name) = file_name.to_str() {
584                    if str_name.starts_with(".") {
585                        if path.is_file() {
586                            let _ = fs::remove_file(&path);
587                        }
588                    }
589                }
590            }
591        }
592    };
593}