1use serde::Deserialize;
2use std::{fs, path::Path};
3use toml::Table;
4
5use indexmap::{indexmap, IndexMap};
6use path_clean::PathClean;
7use smol_str::SmolStr;
8use walkdir::WalkDir;
9
10use crate::utils::dirs::get_fe_deps;
11use crate::utils::git;
12
13const FE_TOML: &str = "fe.toml";
14
15pub enum FileLoader {
16 Static(Vec<(&'static str, &'static str)>),
17 Fs,
18}
19
20impl FileLoader {
21 pub fn canonicalize_path(&self, path: &str) -> Result<SmolStr, String> {
22 match self {
23 FileLoader::Static(_) => Ok(SmolStr::new(
24 Path::new(path).clean().to_str().expect("path clean failed"),
25 )),
26 FileLoader::Fs => Ok(SmolStr::new(
27 fs::canonicalize(path)
28 .map_err(|err| {
29 format!("unable to canonicalize root project path {path}.\n{err}")
30 })?
31 .to_str()
32 .expect("could not convert path to string"),
33 )),
34 }
35 }
36
37 pub fn fe_files(&self, path: &str) -> Result<Vec<(String, String)>, String> {
38 match self {
39 FileLoader::Static(files) => Ok(files
40 .iter()
41 .filter_map(|(file_path, content)| {
42 if file_path.starts_with(path) && file_path.ends_with(".fe") {
43 Some((file_path.to_string(), content.to_string()))
44 } else {
45 None
46 }
47 })
48 .collect()),
49 FileLoader::Fs => {
50 let entries = WalkDir::new(path);
51 let mut files = vec![];
52
53 for entry in entries.into_iter() {
54 let entry =
55 entry.map_err(|err| format!("Error loading source files.\n{err}"))?;
56 let path = entry.path();
57
58 if path.is_file()
59 && path.extension().and_then(std::ffi::OsStr::to_str) == Some("fe")
60 {
61 let content = std::fs::read_to_string(path)
62 .map_err(|err| format!("Unable to read src file.\n{err}"))?;
63 files.push((path.to_string_lossy().to_string(), content));
64 }
65 }
66
67 Ok(files)
68 }
69 }
70 }
71
72 pub fn file_content(&self, path: &str) -> Result<String, String> {
73 match self {
74 FileLoader::Static(files) => {
75 match files.iter().find(|(file_path, _)| file_path == &path) {
76 Some((_, content)) => Ok(content.to_string()),
77 None => Err(format!("could not load static file {}", path)),
78 }
79 }
80 FileLoader::Fs => {
81 std::fs::read_to_string(path).map_err(|err| format!("Unable to read file.\n{err}"))
82 }
83 }
84 }
85}
86
87pub struct BuildFiles {
88 pub root_project_path: SmolStr,
89 pub project_files: IndexMap<SmolStr, ProjectFiles>,
90}
91
92impl BuildFiles {
93 pub fn root_project_mode(&self) -> ProjectMode {
94 self.project_files[&self.root_project_path].mode
95 }
96
97 pub fn load_fs(root_path: &str) -> Result<Self, String> {
99 Self::load(&FileLoader::Fs, root_path)
100 }
101
102 pub fn load_static(
104 files: Vec<(&'static str, &'static str)>,
105 root_path: &str,
106 ) -> Result<Self, String> {
107 Self::load(&FileLoader::Static(files), root_path)
108 }
109
110 fn load(loader: &FileLoader, root_project_path: &str) -> Result<Self, String> {
111 let root_project_path = loader.canonicalize_path(root_project_path)?;
112
113 let mut project_files = indexmap! {
115 root_project_path.clone() => ProjectFiles::load(loader, &root_project_path)?
116 };
117
118 let mut unresolved_projects = vec![root_project_path.clone()];
120
121 while let Some(unresolved_project_path) = unresolved_projects.pop() {
122 for dependency in project_files[&unresolved_project_path].dependencies.clone() {
124 if !project_files.contains_key(&dependency.canonicalized_path) {
125 let dep_project = dependency.resolve(loader)?;
127 project_files.insert(dependency.canonicalized_path.clone(), dep_project);
128 unresolved_projects.push(dependency.canonicalized_path);
129 };
130 }
131 }
132
133 Ok(Self {
134 root_project_path,
135 project_files,
136 })
137 }
138}
139
140pub struct ProjectFiles {
141 pub name: SmolStr,
142 pub version: SmolStr,
143 pub mode: ProjectMode,
144 pub dependencies: Vec<Dependency>,
145 pub src: Vec<(String, String)>,
146}
147
148impl ProjectFiles {
149 fn load(loader: &FileLoader, path: &str) -> Result<Self, String> {
150 let manifest_path = Path::new(path)
151 .join(FE_TOML)
152 .to_str()
153 .expect("unable to convert path to &str")
154 .to_owned();
155 let manifest = Manifest::load(loader, &manifest_path)?;
156 let name = manifest.name;
157 let version = manifest.version;
158
159 let mut dependencies = vec![];
160 let mut errors = vec![];
161
162 if let Some(deps) = &manifest.dependencies {
163 for (name, value) in deps {
164 match Dependency::new(loader, name, path, value) {
165 Ok(dep) => dependencies.push(dep),
166 Err(dep_err) => {
167 errors.push(format!("Misconfigured dependency {name}:\n{dep_err}"))
168 }
169 }
170 }
171 }
172
173 if !errors.is_empty() {
174 return Err(errors.join("\n"));
175 }
176
177 let src_path = Path::new(path)
178 .join("src")
179 .to_str()
180 .expect("unable to convert path to &str")
181 .to_owned();
182 let src = loader.fe_files(&src_path)?;
183
184 let mode = if src
185 .iter()
186 .any(|(file_path, _)| file_path.ends_with("main.fe"))
187 {
188 ProjectMode::Main
189 } else if src
190 .iter()
191 .any(|(file_path, _)| file_path.ends_with("lib.fe"))
192 {
193 ProjectMode::Lib
194 } else {
195 return Err(format!("Unable to determine mode of {}. Consider adding src/main.fe or src/lib.fe file to the project.", name));
196 };
197
198 Ok(Self {
199 name,
200 version,
201 mode,
202 dependencies,
203 src,
204 })
205 }
206}
207
208#[derive(Clone, Copy, PartialEq, Eq)]
209pub enum ProjectMode {
210 Main,
211 Lib,
212}
213
214#[derive(Clone)]
215pub struct Dependency {
216 pub name: SmolStr,
217 pub version: Option<SmolStr>,
218 pub canonicalized_path: SmolStr,
219 pub kind: DependencyKind,
220}
221
222pub trait DependencyResolver {
223 fn resolve(dep: &Dependency, loader: &FileLoader) -> Result<ProjectFiles, String>;
224}
225
226#[derive(Clone)]
227pub struct LocalDependency;
228
229impl DependencyResolver for LocalDependency {
230 fn resolve(dep: &Dependency, loader: &FileLoader) -> Result<ProjectFiles, String> {
231 let project = ProjectFiles::load(loader, &dep.canonicalized_path)?;
232
233 let mut errors = vec![];
234
235 if project.mode == ProjectMode::Main {
236 errors.push(format!("{} is not a library", project.name));
237 }
238
239 if project.name != dep.name {
240 errors.push(format!("Name mismatch: {} =/= {}", project.name, dep.name));
241 }
242
243 if let Some(version) = &dep.version {
244 if version != &project.version {
245 errors.push(format!(
246 "Version mismatch: {} =/= {}",
247 project.version, version
248 ));
249 }
250 }
251
252 if errors.is_empty() {
253 Ok(project)
254 } else {
255 Err(format!(
256 "Unable to resolve {} at {} due to the following errors.\n{}",
257 dep.name,
258 dep.canonicalized_path,
259 errors.join("\n")
260 ))
261 }
262 }
263}
264
265#[derive(Clone)]
266pub struct GitDependency {
267 source: String,
268 rev: String,
269}
270
271impl DependencyResolver for GitDependency {
272 fn resolve(dep: &Dependency, loader: &FileLoader) -> Result<ProjectFiles, String> {
273 if let DependencyKind::Git(GitDependency { source, rev }) = &dep.kind {
274 if let Err(e) = git::fetch_and_checkout(source, dep.canonicalized_path.as_str(), rev) {
275 return Err(format!(
276 "Unable to clone git dependency {}.\n{}",
277 dep.name, e
278 ));
279 }
280
281 return LocalDependency::resolve(dep, loader);
283 }
284 Err(format!("Could not resolve git dependency {}", dep.name))
285 }
286}
287
288#[derive(Clone)]
289pub enum DependencyKind {
290 Local(LocalDependency),
291 Git(GitDependency),
292}
293
294impl Dependency {
295 fn new(
296 loader: &FileLoader,
297 name: &str,
298 orig_path: &str,
299 value: &toml::Value,
300 ) -> Result<Self, String> {
301 let join_path = |path: &str| {
302 loader.canonicalize_path(
303 Path::new(orig_path)
304 .join(path)
305 .to_str()
306 .expect("unable to convert path to &str"),
307 )
308 };
309
310 match value {
311 toml::Value::String(dep_path) => Ok(Dependency {
312 name: name.into(),
313 version: None,
314 kind: DependencyKind::Local(LocalDependency),
315 canonicalized_path: join_path(dep_path)?,
316 }),
317 toml::Value::Table(table) => {
318 let version = table
319 .get("version")
320 .map(|version| version.as_str().unwrap().into());
321
322 let return_local_dep = |path| {
323 Ok::<Dependency, String>(Dependency {
324 name: name.into(),
325 version: version.clone(),
326 canonicalized_path: join_path(path)?,
327
328 kind: DependencyKind::Local(LocalDependency),
329 })
330 };
331
332 match (table.get("source"), table.get("rev"), table.get("path")) {
333 (Some(toml::Value::String(source)), Some(toml::Value::String(rev)), None) => {
334 let dep_path = get_fe_deps().join(format!("{}-{}", name, rev));
335 if dep_path.exists() {
336 return_local_dep(dep_path.to_str().unwrap())
339 } else {
340 fs::create_dir_all(&dep_path).unwrap();
341 Ok(Dependency {
342 name: name.into(),
343 version: version.clone(),
344 canonicalized_path: loader.canonicalize_path(
345 Path::new(orig_path)
346 .join(dep_path)
347 .to_str()
348 .expect("unable to convert path to &str"),
349 )?,
350 kind: DependencyKind::Git(GitDependency {
351 source: source.into(),
352 rev: rev.into(),
353 }),
354 })
355 }
356 }
357 (Some(_), Some(_), Some(_)) => {
358 Err("`path` can not be used together with `rev` and `source`".into())
359 }
360 (Some(_), None, _) => Err("`source` specified but no `rev` given".into()),
361 (None, Some(_), _) => Err("`rev` specified but no `source` given".into()),
362 (None, None, Some(toml::Value::String(path))) => return_local_dep(path),
363 _ => Err("Dependency isn't well formed".into()),
364 }
365 }
366 _ => Err("unsupported toml type".into()),
367 }
368 }
369
370 fn resolve(&self, loader: &FileLoader) -> Result<ProjectFiles, String> {
371 match &self.kind {
372 DependencyKind::Local(_) => LocalDependency::resolve(self, loader),
373 DependencyKind::Git(_) => GitDependency::resolve(self, loader),
374 }
375 }
376}
377
378#[derive(Deserialize)]
379struct Manifest {
380 pub name: SmolStr,
381 pub version: SmolStr,
382 dependencies: Option<Table>,
383}
384
385impl Manifest {
386 pub fn load(loader: &FileLoader, path: &str) -> Result<Self, String> {
387 let content = loader
388 .file_content(path)
389 .map_err(|err| format!("Failed to load manifest content from {path}.\n{err}"))?;
390 let manifest: Manifest = toml::from_str(&content)
391 .map_err(|err| format!("Failed to parse the content of {path}.\n{err}"))?;
392
393 Ok(manifest)
394 }
395}
396
397pub fn get_project_root() -> Option<String> {
399 let current_dir = std::env::current_dir().expect("Unable to get current directory");
400
401 let mut current_path = current_dir.clone();
402 loop {
403 let fe_toml_path = current_path.join(FE_TOML);
404 if fe_toml_path.is_file() {
405 return fe_toml_path
406 .parent()
407 .map(|val| val.to_string_lossy().to_string());
408 }
409
410 if !current_path.pop() {
411 break;
412 }
413 }
414
415 None
416}