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#[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 pub fn add_config(&mut self, config: &str) {
43 self.config_files.push(config.to_string());
44 }
45
46 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 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 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}