liblingo/package/
mod.rs

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
40/// place where are the build artifacts will be dropped
41pub const OUTPUT_DIRECTORY: &str = "build";
42/// name of the folder inside the `OUTPUT_DIRECTORY` where libraries
43/// will be loaded (cloned, extracted, copied) into for further processing.
44pub const LIBRARY_DIRECTORY: &str = "libraries";
45
46/// default folder for lf executable files
47const DEFAULT_EXECUTABLE_FOLDER: &str = "src";
48
49/// default folder for lf library files
50const 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/// list of apps inside a toml file
59#[derive(Deserialize, Serialize, Clone)]
60pub struct AppVec {
61    pub app: Vec<AppFile>,
62}
63
64/// The Lingo.toml format is defined by this struct
65#[derive(Clone, Deserialize, Serialize)]
66pub struct ConfigFile {
67    /// top level package description
68    pub package: PackageDescription,
69
70    /// list of apps defined inside this package
71    #[serde(rename = "app")]
72    pub apps: Option<Vec<AppFile>>,
73
74    /// library exported by this Lingo Toml
75    #[serde(rename = "lib")]
76    pub library: Option<LibraryFile>,
77
78    /// Dependencies for required to build this Lingua-Franca Project
79    pub dependencies: HashMap<String, PackageDetails>,
80}
81
82/// This struct is used after filling in all the defaults
83#[derive(Clone)]
84pub struct Config {
85    /// top level package description
86    pub package: PackageDescription,
87
88    /// list of apps defined inside this package
89    pub apps: Vec<App>,
90
91    /// library exported by this package
92    pub library: Option<Library>,
93
94    /// Dependencies for required to build this Lingua-Franca Project
95    pub dependencies: HashMap<String, PackageDetails>,
96}
97
98/// The Format inside the Lingo.toml under [lib]
99#[derive(Clone, Deserialize, Serialize)]
100pub struct LibraryFile {
101    /// if not specified will default to value specified in the package description
102    pub name: Option<String>,
103
104    /// if not specified will default to ./lib
105    pub location: Option<PathBuf>,
106
107    /// target language of the library
108    pub target: TargetLanguage,
109
110    /// platform of this project
111    pub platform: Option<Platform>,
112
113    /// target properties of that lingua-franca app
114    pub properties: LibraryTargetPropertiesFile,
115}
116
117#[derive(Clone)]
118pub struct Library {
119    /// if not specified will default to value specified in the package description
120    pub name: String,
121
122    /// if not specified will default to ./src
123    pub location: PathBuf,
124
125    /// target of the app
126    pub target: TargetLanguage,
127
128    /// platform of this project
129    pub platform: Platform,
130
131    /// target properties of that lingua-franca app
132    pub properties: LibraryTargetProperties,
133
134    /// Root directory where to place src-gen and other compilation-specifics stuff.
135    pub output_root: PathBuf,
136}
137
138/// Schema of the configuration parsed from the Lingo.toml
139#[derive(Clone, Deserialize, Serialize)]
140pub struct AppFile {
141    /// if not specified will default to value specified in the package description
142    pub name: Option<String>,
143
144    /// if not specified will default to main.lf
145    pub main: Option<PathBuf>,
146
147    /// target of the app
148    pub target: TargetLanguage,
149
150    /// platform of this project
151    pub platform: Option<Platform>,
152
153    /// target properties of that lingua-franca app
154    pub properties: AppTargetPropertiesFile,
155}
156
157#[derive(Clone)]
158pub struct App {
159    /// Absolute path to the directory where the Lingo.toml file is located.
160    pub root_path: PathBuf,
161    /// Name of the app (and the final binary).
162    pub name: String,
163    /// Root directory where to place src-gen and other compilation-specifics stuff.
164    pub output_root: PathBuf,
165    /// Absolute path to the main reactor file.
166    pub main_reactor: PathBuf,
167    /// main reactor name
168    pub main_reactor_name: String,
169    /// target language of this lf program
170    pub target: TargetLanguage,
171    /// platform for which this program should be compiled
172    pub platform: Platform,
173    /// target properties of that lingua-franca app
174    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    // Sets up a standard LF project for "native" development and deployment
395    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"), //FIXME: Add support for Rust.
403        };
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 the cloned template repo into the project directory
427        copy_recursively(tmp_path, Path::new("."))?;
428        // Remove temporary folder
429        dir.close()?;
430        Ok(())
431    }
432
433    // Sets up a LF project with Zephyr as the target platform.
434    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    /// The `path` is the path to the directory containing the Lingo.toml file.
494    pub fn to_config(self, path: &Path) -> Config {
495        let package_name = &self.package.name;
496
497        Config {
498            //properties: self.properties,
499            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}