cairo_lang_compiler/
project.rs

1use std::ffi::OsStr;
2use std::path::Path;
3
4use cairo_lang_defs::ids::ModuleId;
5use cairo_lang_filesystem::db::{
6    CORELIB_CRATE_NAME, CrateConfiguration, CrateIdentifier, CrateSettings, FilesGroupEx,
7};
8use cairo_lang_filesystem::ids::{CrateId, CrateLongId, Directory};
9pub use cairo_lang_project::*;
10use cairo_lang_semantic::db::SemanticGroup;
11use cairo_lang_utils::Intern;
12
13#[derive(thiserror::Error, Debug)]
14pub enum ProjectError {
15    #[error("Only files with .cairo extension can be compiled.")]
16    BadFileExtension,
17    #[error("Couldn't read {path}: No such file.")]
18    NoSuchFile { path: String },
19    #[error("Couldn't handle {path}: Not a legal path.")]
20    BadPath { path: String },
21    #[error("Failed to load project config: {0}")]
22    LoadProjectError(DeserializationError),
23}
24
25/// Setup to 'db' to compile the file at the given path.
26/// Returns the id of the generated crate.
27pub fn setup_single_file_project(
28    db: &mut dyn SemanticGroup,
29    path: &Path,
30) -> Result<CrateId, ProjectError> {
31    match path.extension().and_then(OsStr::to_str) {
32        Some("cairo") => (),
33        _ => {
34            return Err(ProjectError::BadFileExtension);
35        }
36    }
37    if !path.exists() {
38        return Err(ProjectError::NoSuchFile { path: path.to_string_lossy().to_string() });
39    }
40    let bad_path_err = || ProjectError::BadPath { path: path.to_string_lossy().to_string() };
41    let canonical = path.canonicalize().map_err(|_| bad_path_err())?;
42    let file_dir = canonical.parent().ok_or_else(bad_path_err)?;
43    let file_stem = path.file_stem().and_then(OsStr::to_str).ok_or_else(bad_path_err)?;
44    if file_stem == "lib" {
45        let crate_name = file_dir.to_str().ok_or_else(bad_path_err)?;
46        let crate_id = CrateId::plain(db, crate_name);
47        db.set_crate_config(
48            crate_id,
49            Some(CrateConfiguration::default_for_root(Directory::Real(file_dir.to_path_buf()))),
50        );
51        Ok(crate_id)
52    } else {
53        // If file_stem is not lib, create a fake lib file.
54        let crate_id = CrateId::plain(db, file_stem);
55        db.set_crate_config(
56            crate_id,
57            Some(CrateConfiguration::default_for_root(Directory::Real(file_dir.to_path_buf()))),
58        );
59
60        let module_id = ModuleId::CrateRoot(crate_id);
61        let file_id = db.module_main_file(module_id).unwrap();
62        db.as_files_group_mut()
63            .override_file_content(file_id, Some(format!("mod {file_stem};").into()));
64        Ok(crate_id)
65    }
66}
67
68/// Updates the crate roots from a ProjectConfig object.
69pub fn update_crate_roots_from_project_config(db: &mut dyn SemanticGroup, config: &ProjectConfig) {
70    for (crate_identifier, directory_path) in config.content.crate_roots.iter() {
71        let root = Directory::Real(config.absolute_crate_root(directory_path));
72        update_crate_root(db, config, crate_identifier, root);
73    }
74}
75
76/// Updates a single crate root from a ProjectConfig.
77/// If the crate defines settings in the config, it will be used.
78/// Crate is identified by name and the root directory.
79pub fn update_crate_root(
80    db: &mut dyn SemanticGroup,
81    config: &ProjectConfig,
82    crate_identifier: &CrateIdentifier,
83    root: Directory,
84) {
85    let (crate_id, crate_settings) = get_crate_id_and_settings(db, crate_identifier, config);
86    db.set_crate_config(
87        crate_id,
88        Some(CrateConfiguration { root, settings: crate_settings.clone() }),
89    );
90}
91
92/// Setup the 'db' to compile the project in the given path.
93/// The path can be either a directory with cairo project file or a .cairo file.
94/// Returns the ids of the project crates.
95pub fn setup_project(
96    db: &mut dyn SemanticGroup,
97    path: &Path,
98) -> Result<Vec<CrateId>, ProjectError> {
99    if path.is_dir() {
100        let config = ProjectConfig::from_directory(path).map_err(ProjectError::LoadProjectError)?;
101        let main_crate_ids = get_main_crate_ids_from_project(db, &config);
102        update_crate_roots_from_project_config(db, &config);
103        Ok(main_crate_ids)
104    } else {
105        Ok(vec![setup_single_file_project(db, path)?])
106    }
107}
108
109/// Checks that the given path is a valid compiler path.
110pub fn check_compiler_path(single_file: bool, path: &Path) -> anyhow::Result<()> {
111    if path.is_file() {
112        if !single_file {
113            anyhow::bail!("The given path is a file, but --single-file was not supplied.");
114        }
115    } else if path.is_dir() {
116        if single_file {
117            anyhow::bail!("The given path is a directory, but --single-file was supplied.");
118        }
119    } else {
120        anyhow::bail!("The given path does not exist.");
121    }
122    Ok(())
123}
124
125pub fn get_main_crate_ids_from_project(
126    db: &mut dyn SemanticGroup,
127    config: &ProjectConfig,
128) -> Vec<CrateId> {
129    config
130        .content
131        .crate_roots
132        .keys()
133        .map(|crate_identifier| get_crate_id_and_settings(db, crate_identifier, config).0)
134        .collect()
135}
136
137fn get_crate_id_and_settings<'a>(
138    db: &mut dyn SemanticGroup,
139    crate_identifier: &CrateIdentifier,
140    config: &'a ProjectConfig,
141) -> (CrateId, &'a CrateSettings) {
142    let crate_settings = config.content.crates_config.get(crate_identifier);
143    let name = crate_settings.name.clone().unwrap_or_else(|| crate_identifier.clone().into());
144    // It has to be done due to how `CrateId::core` works.
145    let discriminator =
146        if name == CORELIB_CRATE_NAME { None } else { Some(crate_identifier.clone().into()) };
147
148    let crate_id = CrateLongId::Real { name, discriminator }.intern(db);
149
150    (crate_id, crate_settings)
151}