liblingo/backends/
mod.rs

1use log::error;
2use rayon::prelude::*;
3
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use crate::args::{BuildSystem, Platform, TargetLanguage};
9use crate::package::{
10    management::DependencyManager, target_properties::MergeTargetProperties, App, Config,
11    OUTPUT_DIRECTORY,
12};
13use crate::util::errors::{AnyError, BuildResult, LingoError};
14use crate::{GitCloneAndCheckoutCap, WhichCapability};
15
16pub mod cmake_c;
17pub mod cmake_cpp;
18pub mod lfc;
19pub mod npm;
20pub mod pnpm;
21
22#[allow(clippy::single_match)] // there more options will be added to this match block
23pub fn execute_command<'a>(
24    command: &CommandSpec,
25    config: &'a mut Config,
26    which: WhichCapability,
27    clone: GitCloneAndCheckoutCap,
28) -> BatchBuildResults<'a> {
29    let mut result = BatchBuildResults::new();
30    let dependencies = Vec::from_iter(config.dependencies.clone());
31
32    match command {
33        CommandSpec::Build(_build) => {
34            let manager = match DependencyManager::from_dependencies(
35                dependencies.clone(),
36                &PathBuf::from(OUTPUT_DIRECTORY),
37                &clone,
38            ) {
39                Ok(value) => value,
40                Err(e) => {
41                    error!("failed to create dependency manager because of {e}");
42                    return result;
43                }
44            };
45
46            // enriching the apps with the target properties from the libraries
47            let library_properties = manager.get_target_properties().expect("lib properties");
48
49            // merging app with library target properties
50            for app in &mut config.apps {
51                if let Err(e) = app.properties.merge(&library_properties) {
52                    error!("cannot merge properties from the libraries with the app. error: {e}");
53                    return result;
54                }
55            }
56        }
57        _ => {}
58    }
59
60    // Group apps by build system
61    let mut by_build_system = HashMap::<(BuildSystem, TargetLanguage), Vec<&App>>::new();
62    for app in &config.apps {
63        by_build_system
64            .entry((app.build_system(&which), app.target))
65            .or_default()
66            .push(app);
67    }
68
69    for (build_system, apps) in by_build_system {
70        let mut sub_res = BatchBuildResults::for_apps(&apps);
71
72        sub_res.map(|app| {
73            // TODO: Support using lingo as a thin wrapper around west
74            if app.platform == Platform::Zephyr {
75                Err(Box::new(LingoError::UseWestBuildToBuildApp))
76            } else {
77                Ok(())
78            }
79        });
80
81        match build_system {
82            (BuildSystem::CMake, TargetLanguage::Cpp) => {
83                cmake_cpp::CmakeCpp.execute_command(command, &mut sub_res)
84            }
85            (BuildSystem::CMake, TargetLanguage::C) => {
86                cmake_c::CmakeC.execute_command(command, &mut sub_res)
87            }
88            (BuildSystem::Npm, TargetLanguage::TypeScript) => {
89                npm::Npm.execute_command(command, &mut sub_res)
90            }
91            (BuildSystem::Pnpm, TargetLanguage::TypeScript) => {
92                pnpm::Pnpm.execute_command(command, &mut sub_res)
93            }
94            (BuildSystem::LFC, _) => lfc::LFC.execute_command(command, &mut sub_res),
95            (BuildSystem::Cargo, _) => todo!(),
96            _ => {
97                error!("invalid combination of target and platform!");
98                todo!()
99            }
100        };
101        result.append(sub_res);
102    }
103    result
104}
105
106#[derive(Copy, Clone, Eq, PartialEq)]
107pub enum BuildProfile {
108    /// Compile with optimizations.
109    Release,
110    /// Compile with debug info.
111    Debug,
112}
113
114pub struct BuildCommandOptions {
115    /// Build profile, mostly relevant for target compilation.
116    pub profile: BuildProfile,
117    /// Whether to compile the target code.
118    pub compile_target_code: bool,
119    /// Path to the LFC executable.
120    pub lfc_exec_path: PathBuf,
121    /// Max threads to use for compilation. A value of zero means
122    /// that the number will be automatically determined.
123    /// A value of one effectively disables parallel builds.
124    pub max_threads: usize,
125    /// if compilation should continue if one of the apps fails building
126    pub keep_going: bool,
127}
128
129/// Description of a lingo command
130pub enum CommandSpec {
131    /// Compile generated code with the target compiler.
132    Build(BuildCommandOptions),
133    /// Update dependencies
134    Update,
135    /// Clean build artifacts
136    Clean,
137}
138
139/// Implemented by specific build strategies, eg for specific build tools.
140pub trait BatchBackend {
141    /// Build all apps, possibly in parallel.
142    fn execute_command(&mut self, command: &CommandSpec, results: &mut BatchBuildResults);
143}
144
145/// Collects build results by app.
146pub struct BatchBuildResults<'a> {
147    results: Vec<(&'a App, BuildResult)>,
148    keep_going: bool,
149}
150
151impl<'a> BatchBuildResults<'a> {
152    /// Create an empty result, only for use with `append`.
153    fn new() -> Self {
154        Self {
155            results: Vec::new(),
156            keep_going: false,
157        }
158    }
159
160    /// Create a result with an entry for each app. This can
161    /// then be used by combinators like map and such.
162    fn for_apps(apps: &[&'a App]) -> Self {
163        Self {
164            results: apps.iter().map(|&a| (a, Ok(()))).collect(),
165            keep_going: false,
166        }
167    }
168    /// sets the keep going value
169    fn keep_going(&mut self, value: bool) {
170        self.keep_going = value
171    }
172
173    /// Print this result collection to standard output.
174    pub fn print_results(&self) {
175        for (app, b) in &self.results {
176            match b {
177                Ok(()) => {
178                    log::info!("- {}: Success", &app.name);
179                }
180                Err(e) => {
181                    log::error!("- {}: Error: {}", &app.name, e);
182                }
183            }
184        }
185    }
186
187    /// Absorb some results into this vector. Apps are not deduplicated, so this
188    /// is only ok if the other is disjoint from this result.
189    fn append(&mut self, mut other: BatchBuildResults<'a>) {
190        self.results.append(&mut other.results);
191        self.results.sort_by_key(|(app, _)| &app.name);
192    }
193
194    // Note: the duplication of the bodies of the following functions is benign, and
195    // allows the sequential map to be bounded more loosely than if we were to extract
196    // a function to get rid of the dup.
197
198    /// Map results sequentially. Apps that already have a failing result recorded
199    /// are not fed to the mapping function.
200    pub fn map<F>(&mut self, f: F) -> &mut Self
201    where
202        F: Fn(&'a App) -> BuildResult,
203    {
204        self.results.iter_mut().for_each(|(app, res)| {
205            if let Ok(()) = res {
206                *res = f(app);
207
208                if (*res).is_err() && !self.keep_going {
209                    panic!(
210                        "build step failed because of {} with main reactor {}!",
211                        &app.name,
212                        &app.main_reactor.display()
213                    );
214                }
215            }
216        });
217        self
218    }
219
220    /// Map results in parallel. Apps that already have a failing result recorded
221    /// are not fed to the mapping function.
222    pub fn par_map<F>(&mut self, f: F) -> &mut Self
223    where
224        F: Fn(&'a App) -> BuildResult + Send + Sync,
225    {
226        self.results.par_iter_mut().for_each(|(app, res)| {
227            if let Ok(()) = res {
228                *res = f(app);
229
230                if (*res).is_err() && !self.keep_going {
231                    panic!(
232                        "build step failed with error {:?} because of app {} with main reactor {}!",
233                        &res,
234                        &app.name,
235                        &app.main_reactor.display()
236                    );
237                }
238            }
239        });
240        self
241    }
242
243    /// Execute a function of all the apps that have not failed.
244    /// The returned result is set to all apps, ie, either they
245    /// all succeed, or they all fail for the same reason.
246    pub fn gather<F>(&mut self, f: F) -> &mut Self
247    where
248        F: FnOnce(&Vec<&'a App>) -> BuildResult,
249    {
250        // collect all apps that have not yet failed.
251        let vec: Vec<&'a App> = self
252            .results
253            .iter()
254            .filter_map(|&(app, ref res)| res.as_ref().ok().map(|()| app))
255            .collect();
256
257        if vec.is_empty() {
258            return self;
259        }
260
261        match f(&vec) {
262            Ok(()) => { /* Do nothing, all apps have succeeded. */ }
263            Err(e) => {
264                if !self.keep_going {
265                    panic!("build step failed!");
266                }
267
268                // Mark all as failed for the same reason.
269                let shared: Arc<AnyError> = e.into();
270                for (_app, res) in &mut self.results {
271                    if let Ok(()) = res {
272                        *res = Err(Box::new(LingoError::Shared(shared.clone())));
273                    }
274                }
275            }
276        }
277        self
278    }
279}