1pub mod lock;
2pub mod management;
3pub mod tree;
4
5pub mod target_properties;
6
7use serde::de::{Error, Visitor};
8use serde::{Deserializer, Serializer};
9use serde_derive::{Deserialize, Serialize};
10use std::collections::HashMap;
11use tempfile::tempdir;
12use versions::Versioning;
13
14use std::fs::{remove_dir_all, remove_file, write};
15use std::io::ErrorKind;
16use std::path::{Path, PathBuf};
17use std::str::FromStr;
18use std::{env, fmt, io};
19
20use crate::args::TargetLanguage::UC;
21use crate::args::{
22 BuildSystem,
23 BuildSystem::{CMake, LFC},
24 InitArgs, Platform, TargetLanguage,
25};
26use crate::package::tree::GitLock;
27use crate::package::{
28 target_properties::{
29 AppTargetProperties, AppTargetPropertiesFile, LibraryTargetProperties,
30 LibraryTargetPropertiesFile,
31 },
32 tree::PackageDetails,
33};
34use crate::util::{
35 analyzer, copy_recursively,
36 errors::{BuildResult, LingoError},
37};
38use crate::{FsReadCapability, GitCloneAndCheckoutCap, GitUrl, WhichCapability};
39
40pub const OUTPUT_DIRECTORY: &str = "build";
42pub const LIBRARY_DIRECTORY: &str = "libraries";
45
46const DEFAULT_EXECUTABLE_FOLDER: &str = "src";
48
49const DEFAULT_LIBRARY_FOLDER: &str = "src/lib";
51
52fn is_valid_location_for_project(path: &std::path::Path) -> bool {
53 !path.join(DEFAULT_EXECUTABLE_FOLDER).exists()
54 && !path.join(".git").exists()
55 && !path.join(DEFAULT_LIBRARY_FOLDER).exists()
56}
57
58#[derive(Deserialize, Serialize, Clone)]
60pub struct AppVec {
61 pub app: Vec<AppFile>,
62}
63
64#[derive(Clone, Deserialize, Serialize)]
66pub struct ConfigFile {
67 pub package: PackageDescription,
69
70 #[serde(rename = "app")]
72 pub apps: Option<Vec<AppFile>>,
73
74 #[serde(rename = "lib")]
76 pub library: Option<LibraryFile>,
77
78 pub dependencies: HashMap<String, PackageDetails>,
80}
81
82#[derive(Clone)]
84pub struct Config {
85 pub package: PackageDescription,
87
88 pub apps: Vec<App>,
90
91 pub library: Option<Library>,
93
94 pub dependencies: HashMap<String, PackageDetails>,
96}
97
98#[derive(Clone, Deserialize, Serialize)]
100pub struct LibraryFile {
101 pub name: Option<String>,
103
104 pub location: Option<PathBuf>,
106
107 pub target: TargetLanguage,
109
110 pub platform: Option<Platform>,
112
113 pub properties: LibraryTargetPropertiesFile,
115}
116
117#[derive(Clone)]
118pub struct Library {
119 pub name: String,
121
122 pub location: PathBuf,
124
125 pub target: TargetLanguage,
127
128 pub platform: Platform,
130
131 pub properties: LibraryTargetProperties,
133
134 pub output_root: PathBuf,
136}
137
138#[derive(Clone, Deserialize, Serialize)]
140pub struct AppFile {
141 pub name: Option<String>,
143
144 pub main: Option<PathBuf>,
146
147 pub target: TargetLanguage,
149
150 pub platform: Option<Platform>,
152
153 pub properties: AppTargetPropertiesFile,
155}
156
157#[derive(Clone)]
158pub struct App {
159 pub root_path: PathBuf,
161 pub name: String,
163 pub output_root: PathBuf,
165 pub main_reactor: PathBuf,
167 pub main_reactor_name: String,
169 pub target: TargetLanguage,
171 pub platform: Platform,
173 pub properties: AppTargetProperties,
175}
176
177impl AppFile {
178 const DEFAULT_MAIN_REACTOR_RELPATH: &'static str = "src/Main.lf";
179 pub fn convert(self, package_name: &str, path: &Path) -> App {
180 let file_name: Option<String> = match self.main.clone() {
181 Some(path) => path
182 .file_stem()
183 .to_owned()
184 .and_then(|x| x.to_str())
185 .map(|x| x.to_string()),
186 None => None,
187 };
188 let name = self
189 .name
190 .unwrap_or(file_name.unwrap_or(package_name.to_string()).to_string());
191
192 let mut abs = path.to_path_buf();
193 abs.push(
194 self.main
195 .unwrap_or(Self::DEFAULT_MAIN_REACTOR_RELPATH.into()),
196 );
197
198 let temp = abs
199 .clone()
200 .file_name()
201 .expect("cannot extract file name")
202 .to_str()
203 .expect("cannot convert path to string")
204 .to_string();
205 let main_reactor_name = &temp[..temp.len() - 3];
206
207 App {
208 root_path: path.to_path_buf(),
209 name,
210 output_root: path.join(OUTPUT_DIRECTORY),
211 main_reactor: abs,
212 main_reactor_name: main_reactor_name.to_string(),
213 target: self.target,
214 platform: self.platform.unwrap_or(Platform::Native),
215 properties: self.properties.from(path),
216 }
217 }
218}
219
220impl LibraryFile {
221 pub fn convert(self, package_name: &str, path: &Path) -> Library {
222 let file_name: Option<String> = match self.location.clone() {
223 Some(path) => path
224 .file_stem()
225 .to_owned()
226 .and_then(|x| x.to_str())
227 .map(|x| x.to_string()),
228 None => None,
229 };
230 let name = self
231 .name
232 .unwrap_or(file_name.unwrap_or(package_name.to_string()).to_string());
233
234 Library {
235 name,
236 location: {
237 let mut abs = path.to_path_buf();
238 abs.push(self.location.unwrap_or(DEFAULT_LIBRARY_FOLDER.into()));
239 abs
240 },
241 target: self.target,
242 platform: self.platform.unwrap_or(Platform::Native),
243 properties: self.properties.from(path),
244 output_root: path.join(OUTPUT_DIRECTORY),
245 }
246 }
247}
248
249impl App {
250 pub fn build_system(&self, which: &WhichCapability) -> BuildSystem {
251 match self.target {
252 TargetLanguage::C => CMake,
253 TargetLanguage::Cpp => CMake,
254 TargetLanguage::TypeScript => {
255 if which("pnpm").is_ok() {
256 BuildSystem::Pnpm
257 } else {
258 BuildSystem::Npm
259 }
260 }
261 _ => LFC,
262 }
263 }
264 pub fn src_gen_dir(&self) -> PathBuf {
265 self.output_root.join("src-gen")
266 }
267 pub fn executable_path(&self) -> PathBuf {
268 let mut p = self.output_root.join("bin");
269 if self.target == TargetLanguage::TypeScript {
270 p.push(self.name.clone() + ".js")
271 } else {
272 p.push(&self.name);
273 }
274 p
275 }
276
277 pub fn src_dir_path(&self) -> Option<PathBuf> {
278 for path in self.main_reactor.ancestors() {
279 if path.ends_with("src") {
280 return Some(path.to_path_buf());
281 }
282 }
283 None
284 }
285}
286
287fn serialize_version<S>(version: &Versioning, serializer: S) -> Result<S::Ok, S::Error>
288where
289 S: Serializer,
290{
291 serializer.serialize_str(&version.to_string())
292}
293
294struct VersioningVisitor;
295
296impl<'de> Visitor<'de> for VersioningVisitor {
297 type Value = Versioning;
298
299 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
300 formatter.write_str("an valid semantic version")
301 }
302
303 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
304 where
305 E: Error,
306 {
307 Versioning::from_str(v).map_err(|_| E::custom("not a valid version"))
308 }
309}
310
311fn deserialize_version<'de, D>(deserializer: D) -> Result<Versioning, D::Error>
312where
313 D: Deserializer<'de>,
314{
315 deserializer.deserialize_str(VersioningVisitor)
316}
317
318#[derive(Deserialize, Serialize, Clone)]
319pub struct PackageDescription {
320 pub name: String,
321 #[serde(
322 serialize_with = "serialize_version",
323 deserialize_with = "deserialize_version"
324 )]
325 pub version: Versioning,
326 pub authors: Option<Vec<String>>,
327 pub website: Option<String>,
328 pub license: Option<String>,
329 pub description: Option<String>,
330}
331
332impl ConfigFile {
333 pub fn new_for_init_task(init_args: &InitArgs) -> io::Result<ConfigFile> {
334 let src_path = Path::new(DEFAULT_EXECUTABLE_FOLDER);
335 let main_reactors = if src_path.exists() {
336 analyzer::find_main_reactors(src_path)?
337 } else {
338 vec![analyzer::MainReactorSpec {
339 name: "Main".into(),
340 path: src_path.join("Main.lf"),
341 target: init_args.get_target_language(),
342 }]
343 };
344 let app_specs = main_reactors
345 .into_iter()
346 .map(|spec| AppFile {
347 name: Some(spec.name),
348 main: Some(spec.path),
349 target: spec.target,
350 platform: Some(init_args.platform),
351 properties: Default::default(),
352 })
353 .collect::<Vec<_>>();
354
355 let result = ConfigFile {
356 package: PackageDescription {
357 name: std::env::current_dir()
358 .expect("error while reading current directory")
359 .as_path()
360 .file_name()
361 .expect("cannot get file name")
362 .to_string_lossy()
363 .to_string(),
364 version: Versioning::from_str("0.1.0").unwrap(),
365 authors: None,
366 website: None,
367 license: None,
368 description: None,
369 },
370 dependencies: HashMap::default(),
371 apps: Some(app_specs),
372 library: Option::default(),
373 };
374 Ok(result)
375 }
376
377 pub fn write(&self, path: &Path) -> io::Result<()> {
378 let toml_string = toml::to_string(&self).expect("cannot serialize toml");
379 write(path, toml_string)
380 }
381
382 pub fn from(path: &Path, fsr: FsReadCapability) -> io::Result<ConfigFile> {
383 let contents = fsr(path);
384 contents.and_then(|contents| {
385 toml::from_str(&contents).map_err(|e| {
386 io::Error::new(
387 ErrorKind::InvalidData,
388 format!("failed to convert string to toml: {}", e),
389 )
390 })
391 })
392 }
393
394 pub fn setup_native(&self, target_language: TargetLanguage) -> BuildResult {
396 std::fs::create_dir_all("./src")?;
397 let hello_world_code: &'static str = match target_language {
398 TargetLanguage::Cpp => include_str!("../../defaults/HelloCpp.lf"),
399 TargetLanguage::C => include_str!("../../defaults/HelloC.lf"),
400 TargetLanguage::Python => include_str!("../../defaults/HelloPy.lf"),
401 TargetLanguage::TypeScript => include_str!("../../defaults/HelloTS.lf"),
402 _ => panic!("Target langauge not supported yet"), };
404
405 write(Path::new("./src/Main.lf"), hello_world_code)?;
406 Ok(())
407 }
408
409 fn setup_template_repo(
410 &self,
411 url: &str,
412 target_language: TargetLanguage,
413 clone: &GitCloneAndCheckoutCap,
414 ) -> BuildResult {
415 let dir = tempdir()?;
416 let tmp_path = dir.path();
417
418 let git_rev = if target_language == UC {
419 Some(GitLock::Branch("origin/reactor-uc".to_string()))
420 } else {
421 None
422 };
423
424 clone(GitUrl::from(url), tmp_path, git_rev)?;
425
426 copy_recursively(tmp_path, Path::new("."))?;
428 dir.close()?;
430 Ok(())
431 }
432
433 fn clone_and_clean(
435 &self,
436 url: &str,
437 target_language: TargetLanguage,
438 clone: &GitCloneAndCheckoutCap,
439 ) -> BuildResult {
440 self.setup_template_repo(url, target_language, clone)?;
441 remove_file(".gitignore")?;
442 remove_dir_all(Path::new(".git"))?;
443 Ok(())
444 }
445
446 pub fn setup_example(
447 &self,
448 platform: Platform,
449 target_language: TargetLanguage,
450 git_clone_capability: &GitCloneAndCheckoutCap,
451 ) -> BuildResult {
452 if is_valid_location_for_project(Path::new(".")) {
453 match platform {
454 Platform::Native => self.setup_native(target_language),
455 Platform::Zephyr => self.clone_and_clean(
456 "https://github.com/lf-lang/lf-west-template",
457 target_language,
458 git_clone_capability,
459 ),
460 Platform::RP2040 => self.clone_and_clean(
461 "https://github.com/lf-lang/lf-pico-template",
462 target_language,
463 git_clone_capability,
464 ),
465 Platform::LF3PI => self.clone_and_clean(
466 "https://github.com/lf-lang/lf-3pi-template",
467 target_language,
468 git_clone_capability,
469 ),
470 Platform::FlexPRET => self.clone_and_clean(
471 "https://github.com/lf-lang/lf-flexpret-template",
472 target_language,
473 git_clone_capability,
474 ),
475 Platform::Patmos => self.clone_and_clean(
476 "https://github.com/lf-lang/lf-patmos-template",
477 target_language,
478 git_clone_capability,
479 ),
480 Platform::RIOT => self.clone_and_clean(
481 "https://github.com/lf-lang/lf-riot-template",
482 target_language,
483 git_clone_capability,
484 ),
485 }
486 } else {
487 Err(Box::new(LingoError::InvalidProjectLocation(
488 env::current_dir().expect("cannot fetch current working directory"),
489 )))
490 }
491 }
492
493 pub fn to_config(self, path: &Path) -> Config {
495 let package_name = &self.package.name;
496
497 Config {
498 apps: self
500 .apps
501 .unwrap_or_default()
502 .into_iter()
503 .map(|app_file| app_file.convert(package_name, path))
504 .collect(),
505 package: self.package.clone(),
506 library: self.library.map(|lib| lib.convert(package_name, path)),
507 dependencies: self.dependencies,
508 }
509 }
510}