echo_library/common/
composer.rs

1use anyhow::{anyhow, Ok};
2use composer_primitives::types::SourceFiles;
3use rayon::prelude::*;
4use starlark::environment::FrozenModule;
5use starlark::eval::ReturnFileLoader;
6use std::fs::OpenOptions;
7use std::io::Write;
8use std::path::Path;
9use boilerplate::*;
10
11use super::*;
12
13// Hardcoded boilerplate for release
14// const COMMON: &str = include_str!("../boilerplate/src/common.rs");
15// const LIB: &str = include_str!("../boilerplate/src/lib.rs");
16// const TRAIT: &str = include_str!("../boilerplate/src/traits.rs");
17// const MACROS: &str = include_str!("../boilerplate/src/macros.rs");
18// const CARGO: &str = include_str!("../boilerplate/Cargo.toml");
19
20#[derive(Debug, ProvidesStaticType, Default)]
21pub struct Composer {
22    pub config_files: Vec<String>,
23    pub workflows: RefCell<Vec<Workflow>>,
24    pub custom_types: RefCell<HashMap<String, String>>,
25}
26
27impl Composer {
28    /// Adds config file to the composer
29    /// This method is called by the user
30    ///
31    /// # Arguments
32    ///
33    /// * `config` - A string slice that holds the of the config file along with its name
34    ///
35    /// # Example
36    ///
37    /// ```
38    /// use echo_library::Composer;
39    /// let mut composer = Composer::default();
40    /// composer.add_config("config/path/config_file_name_here");
41    /// ```
42    pub fn add_config(&mut self, config: &str) {
43        self.config_files.push(config.to_string());
44    }
45
46    /// Adds a new workflow to the composer.
47    /// This method is invoked by the workflows function inside the starlark_module.
48    ///
49    /// # Arguments
50    ///
51    /// * `name` - Name of the workflow to be added
52    /// * `version` - Version of the workflow
53    /// * `tasks` - HashMap of tasks associated with the workflow
54    /// * `custom_types` - Optional vector of custom types names that are created within config
55    ///   for the workflow.
56    ///
57    /// # Returns
58    ///
59    /// * `Result<(), Error>` - Result indicating success if the workflow is added successfully,
60    ///   or an error if the workflow name is empty or if there is a duplicate workflow name.
61    ///
62    pub fn add_workflow(
63        &self,
64        name: String,
65        version: String,
66        tasks: HashMap<String, Task>,
67    ) -> Result<(), Error> {
68        for workflow in self.workflows.borrow().iter() {
69            if workflow.name == name {
70                return Err(Error::msg("Workflows should not have same name"));
71            }
72        }
73        if name.is_empty() {
74            Err(Error::msg("Workflow name should not be empty"))
75        } else {
76            self.workflows.borrow_mut().push(Workflow {
77                name,
78                version,
79                tasks,
80            });
81            Ok(())
82        }
83    }
84
85    pub fn build(verbose: bool, temp_dir: &Path) -> Result<(), Error> {
86        if verbose {
87            Command::new("rustup")
88                .current_dir(temp_dir.join("boilerplate"))
89                .args(["target", "add", "wasm32-wasi"])
90                .status()?;
91
92            Command::new("cargo")
93                .current_dir(temp_dir.join("boilerplate"))
94                .args(["build", "--release", "--target", "wasm32-wasi"])
95                .status()?;
96        } else {
97            Command::new("cargo")
98                .current_dir(temp_dir.join("boilerplate"))
99                .args(["build", "--release", "--target", "wasm32-wasi", "--quiet"])
100                .status()?;
101        }
102        Ok(())
103    }
104
105    fn copy_boilerplate(
106        temp_dir: &Path,
107        types_rs: String,
108        workflow_name: String,
109        workflow: &Workflow,
110    ) -> Result<PathBuf, Error> {
111        let temp_dir = temp_dir.join(workflow_name);
112        let curr = temp_dir.join("boilerplate");
113
114        std::fs::create_dir_all(curr.clone().join("src"))?;
115
116        let src_curr = temp_dir.join("boilerplate/src");
117        let temp_path = src_curr.as_path().join("common.rs");
118
119        std::fs::write(temp_path, COMMON)?;
120
121        let temp_path = src_curr.as_path().join("lib.rs");
122        std::fs::write(temp_path.clone(), LIB)?;
123
124        let mut lib = OpenOptions::new()
125            .write(true)
126            .append(true)
127            .open(temp_path)?;
128
129        let library = get_struct_stake_ledger(workflow);
130        writeln!(lib, "{library}").expect("could not able to add struct to lib");
131
132        let temp_path = src_curr.as_path().join("types.rs");
133        std::fs::write(temp_path, types_rs)?;
134
135        let temp_path = src_curr.as_path().join("traits.rs");
136        std::fs::write(temp_path, TRAIT)?;
137
138        let temp_path = src_curr.as_path().join("macros.rs");
139        std::fs::write(temp_path, MACROS)?;
140
141        let cargo_path = curr.join("Cargo.toml");
142        std::fs::write(cargo_path.clone(), CARGO)?;
143
144        let mut cargo_toml = OpenOptions::new()
145            .write(true)
146            .append(true)
147            .open(cargo_path)?;
148
149        let dependencies = generate_cargo_toml_dependencies(workflow);
150        writeln!(cargo_toml, "{dependencies}")
151            .expect("could not able to add dependencies to the Cargo.toml");
152
153        Ok(temp_dir)
154    }
155}
156
157impl Composer {
158    pub fn compile(
159        &self,
160        module: &str,
161        files: &SourceFiles,
162        loader: &mut HashMap<String, FrozenModule>,
163    ) -> Result<FrozenModule, Error> {
164        let ast: AstModule = AstModule::parse_file(
165            files
166                .files()
167                .get(&PathBuf::from(format!(
168                    "{}/{}",
169                    files.base().display(),
170                    module
171                )))
172                .ok_or_else(|| {
173                    Error::msg(format!(
174                        "FileNotFound at {}/{}",
175                        files.base().display(),
176                        module
177                    ))
178                })?,
179            &Dialect::Extended,
180        )
181        .map_err(|err| Error::msg(format!("Error parsing file: {}", err)))?;
182
183        for load in ast.loads() {
184            if loader.get(load.module_id).is_none() {
185                let frozen_module = Self::compile(self, load.module_id, files, loader)?;
186                loader.insert(load.module_id.to_owned(), frozen_module);
187            };
188        }
189
190        let modules = loader.iter().map(|(a, b)| (a.as_str(), b)).collect();
191        let loader = ReturnFileLoader { modules: &modules };
192
193        // We build our globals by adding some functions we wrote
194        let globals = GlobalsBuilder::extended_by(&[
195            StructType, RecordType, EnumType, Map, Filter, Partial, Debug, Print, Pprint,
196            Breakpoint, Json, Typing, Internal, CallStack,
197        ])
198        .with(starlark_workflow_module)
199        .with(starlark_datatype_module)
200        .with_struct("Operation", starlark_operation_module)
201        .build();
202
203        let module = Module::new();
204
205        let int = module.heap().alloc(RustType::Int);
206        module.set("Int", int);
207        let uint = module.heap().alloc(RustType::Uint);
208        module.set("Uint", uint);
209        let int = module.heap().alloc(RustType::Float);
210        module.set("Float", int);
211        let int = module.heap().alloc(RustType::String);
212        module.set("String", int);
213        let int = module.heap().alloc(RustType::Boolean);
214        module.set("Bool", int);
215
216        {
217            let result = {
218                let mut eval = Evaluator::new(&module);
219                // We add a reference to our store
220                eval.set_loader(&loader);
221                eval.extra = Some(self);
222                eval.eval_module(ast, &globals)
223            };
224
225            result.map_err(|err| Error::msg(format!("Evaluation error: {}", err)))?;
226        }
227
228        if self.workflows.borrow().is_empty() {
229            return Err(Error::msg("Empty workflow detected!!!"));
230        }
231        Ok(module.freeze()?)
232    }
233
234    pub fn build_directory(
235        &self,
236        build_path: &Path,
237        out_path: &Path,
238        quiet: bool,
239    ) -> anyhow::Result<(), Error> {
240        let composer_custom_types = self.custom_types.take();
241
242        let workflows = self.workflows.take();
243
244        let results: Vec<Result<(), Error>> = workflows
245            .par_iter()
246            .enumerate()
247            .map(|workflow: (usize, &Workflow)| {
248                if workflow.1.tasks.is_empty() {
249                    return Ok(());
250                }
251
252                let workflow_name = format!("{}_{}", workflow.1.name, workflow.1.version);
253
254                let types_rs =
255                    generate_types_rs_file_code(&workflows[workflow.0], &composer_custom_types)
256                        .map_err(|err| {
257                            anyhow!(
258                                "{}: Failed to generate types.rs file: {}",
259                                workflow.1.name,
260                                err
261                            )
262                        })?;
263
264                let temp_dir =
265                    Self::copy_boilerplate(build_path, types_rs, workflow_name.clone(), workflow.1)
266                        .map_err(|err| {
267                            anyhow!("{}: Failed to copy boilerplate: {}", workflow.1.name, err)
268                        })?;
269
270                Self::build(quiet, &temp_dir)
271                    .map_err(|err| anyhow!("{}: Failed to build: {}", workflow.1.name, err))?;
272
273                let wasm_path = format!(
274                    "{}/boilerplate/target/wasm32-wasi/release/boilerplate.wasm",
275                    temp_dir.display()
276                );
277
278                fs::create_dir_all(out_path.join("output")).map_err(|err| {
279                    anyhow!(
280                        "{}: Failed to create output directory: {}",
281                        workflow.1.name,
282                        err
283                    )
284                })?;
285
286                fs::copy(
287                    wasm_path,
288                    out_path.join(format!("output/{workflow_name}.wasm")),
289                )
290                .map_err(|err| anyhow!("{}: Failed to copy wasm: {}", workflow.1.name, err))?;
291
292                fs::remove_dir_all(temp_dir).map_err(|err| {
293                    anyhow!("{}: Failed to remove temp dir: {}", workflow.1.name, err)
294                })?;
295
296                Ok(())
297            })
298            .filter(|result| result.is_err())
299            .collect::<Vec<_>>()
300            .into_iter()
301            .collect();
302
303        if !results.is_empty() {
304            return Err(Error::msg(format!(
305                "Failed to build the following workflows: {:?}",
306                results
307            )));
308        }
309
310        Ok(())
311    }
312}