1use std::{
2 collections::HashMap,
3 fs::File,
4 io::Result,
5 io::{Cursor, Error, Write},
6 path::{Path, PathBuf},
7 process::{Child, Command},
8};
9
10use grass_ir::GrassIR;
11use serde::Deserialize;
12use sha::{sha256::Sha256, utils::DigestExt};
13use tempfile::TempDir;
14
15use crate::{
16 cache::CacheState,
17 dependency::{Dependency, DependencySource},
18};
19
20use crate::return_true;
21
22#[derive(Deserialize)]
23pub enum BuildFlavor {
24 Debug,
25 Release,
26 ReleaseWithDebugInfo,
27}
28
29impl Default for BuildFlavor {
30 fn default() -> Self {
31 Self::Release
32 }
33}
34
35#[derive(Deserialize)]
36#[allow(unused)]
37pub struct JobDefinition {
38 ir: Vec<GrassIR>,
39
40 working_dir: PathBuf,
42 #[serde(default)]
43 cmdline_args: Vec<String>,
44 #[serde(default)]
45 env_vars: HashMap<String, String>,
46 #[serde(default)]
47 const_bag_types: Vec<String>,
48
49 #[serde(default = "default_runtime")]
51 runtime_package: String,
52 #[serde(default = "default_macro")]
53 macro_package: String,
54
55 runtime_source: DependencySource,
56 #[serde(default = "latest_grass_runtime")]
57 runtime_version: String,
58
59 macro_source: DependencySource,
60 #[serde(default = "latest_grass_runtime")]
61 macro_version: String,
62
63 #[serde(default)]
65 deps: Vec<Dependency>,
66 #[serde(default = "default_version")]
67 version: String,
68 #[serde(default)]
69 build_flavor: BuildFlavor,
70 #[serde(default)]
71 temp_dir: Option<PathBuf>,
72
73 #[serde(default = "return_true")]
75 use_cache: bool,
76 #[serde(default = "return_true")]
77 update_cache: bool,
78 #[serde(default = "default_cache_root")]
79 cache_root: PathBuf,
80
81 #[serde(default)]
83 tool_chain_path: Option<PathBuf>,
84 #[serde(skip)]
85 compilation_dir: Option<TempDir>,
86 #[serde(skip)]
87 ir_hash: String,
88 #[serde(skip)]
89 artifact_path: Option<PathBuf>,
90}
91
92fn default_version() -> String {
93 "0.0.1".to_string()
94}
95
96fn default_runtime() -> String {
97 "grass-runtime".to_string()
98}
99
100fn default_macro() -> String {
101 "grass-macro".to_string()
102}
103
104fn latest_grass_runtime() -> String {
105 "*".to_string()
106}
107
108fn default_cache_root() -> PathBuf {
109 let mut ret: PathBuf = std::env::var("HOME")
110 .unwrap_or_else(|_| "/".to_string())
111 .into();
112 ret.push(".grass-ql");
113 ret.push("cache");
114 ret
115}
116
117impl JobDefinition {
118 pub fn get_stderr_log(&mut self) -> Result<File> {
119 File::open(self.get_compilation_dir()?.join("stderr.log"))
120 }
121 pub fn cargo(&mut self, args: &[&str], capture_stdout: bool) -> Result<Child> {
122 let mut cmd = Command::new("cargo");
123
124 if let Some(tool_chain_path) = self.tool_chain_path.as_ref() {
125 cmd.env("PATH", tool_chain_path);
126 }
127
128 cmd.args(args);
129
130 let comp_dir = self.get_compilation_dir()?;
131 cmd.current_dir(comp_dir);
132
133 let err_log = File::create(comp_dir.join("stderr.log"))?;
134 cmd.stderr(err_log);
135
136 if capture_stdout {
137 let out_log = File::create(comp_dir.join("stdout.txt"))?;
138 cmd.stdout(out_log);
139 }
140
141 cmd.spawn()
142 }
143 fn populate_ir_hash(&mut self) -> Result<()> {
144 let mut buffer = Vec::new();
145 {
146 let mut buffer_writer = Cursor::new(&mut buffer);
147 serde_json::to_writer(&mut buffer_writer, &self.ir)?;
148
149 let mut deps = self.deps.clone();
150 deps.sort_by_key(|x| x.name.clone());
151
152 serde_json::to_writer(&mut buffer_writer, &deps)?;
153
154 serde_json::to_writer(&mut buffer_writer, &self.macro_package)?;
155 serde_json::to_writer(&mut buffer_writer, &self.macro_version)?;
156 serde_json::to_writer(&mut buffer_writer, &self.macro_source)?;
157
158 serde_json::to_writer(&mut buffer_writer, &self.runtime_package)?;
159 serde_json::to_writer(&mut buffer_writer, &self.runtime_version)?;
160 serde_json::to_writer(&mut buffer_writer, &self.runtime_source)?;
161 }
162 let input_str = String::from_utf8(buffer).unwrap();
163 log::debug!("Job hasher input: {}", input_str);
164 let mut hasher = Sha256::default();
165
166 hasher.write_all(input_str.as_bytes())?;
167
168 hasher.flush()?;
169
170 let ir_hash = hasher.to_hex();
171
172 self.ir_hash = ir_hash;
173 Ok(())
174 }
175 fn get_artifact_hash(&mut self) -> Result<&str> {
176 if self.ir_hash.len() == 0 {
177 self.populate_ir_hash()?;
178 }
179 Ok(self.ir_hash.as_str())
180 }
181 fn get_artifact_name(&mut self) -> Result<String> {
182 Ok(format!("grass-artifact-{}", self.get_artifact_hash()?))
183 }
184 fn get_artifact_path(&mut self) -> Result<PathBuf> {
185 let mut ret = self.get_compilation_dir()?.to_path_buf();
186 ret.push("target");
187 match self.build_flavor {
188 BuildFlavor::Debug => ret.push("debug"),
189 BuildFlavor::Release | BuildFlavor::ReleaseWithDebugInfo => ret.push("release"),
190 }
191 ret.push(self.get_artifact_name()?);
192 Ok(ret)
193 }
194 fn write_manifest_file(&mut self, root: &Path) -> Result<()> {
195 let manifest_path = root.join("Cargo.toml");
196 let mut manifest_file = File::create(&manifest_path)?;
197 writeln!(&mut manifest_file, "[package]")?;
198 writeln!(
199 &mut manifest_file,
200 "name = \"{}\"",
201 self.get_artifact_name()?
202 )?;
203 writeln!(&mut manifest_file, "version = \"{}\"", self.version)?;
204 writeln!(&mut manifest_file, "edition = \"2021\"")?;
205 writeln!(&mut manifest_file, "[dependencies]")?;
206 for dep in self.deps.iter().chain([
207 &Dependency::create_grass_dep(
208 &self.runtime_package,
209 &self.runtime_source,
210 &self.runtime_version,
211 ),
212 &Dependency::create_grass_dep(
213 &self.macro_package,
214 &self.macro_source,
215 &self.macro_version,
216 ),
217 ]) {
218 dep.write_dependency_line(&mut manifest_file)?;
219 }
220 match self.build_flavor {
221 BuildFlavor::Release => {
222 writeln!(&mut manifest_file, "[profile.release]")?;
223 writeln!(&mut manifest_file, "strip = true")?;
224 }
225 BuildFlavor::ReleaseWithDebugInfo => {
226 writeln!(&mut manifest_file, "[profile.release]")?;
227 writeln!(&mut manifest_file, "debug = true")?;
228 }
229 _ => (),
230 }
231 Ok(())
232 }
233 fn write_source_code(&self, root: &Path) -> Result<()> {
234 let source_dir = root.join("src");
235 std::fs::create_dir(&source_dir)?;
236
237 let source_path = source_dir.join("main.rs");
238 let mut source_file = File::create(source_path)?;
239
240 writeln!(&mut source_file, "#[allow(unused_imports)]")?;
241 writeln!(
242 &mut source_file,
243 "use grass_runtime::const_bag::{{ConstBagRef, ConstBagType}};"
244 )?;
245
246 for (id, t) in self.const_bag_types.iter().enumerate() {
247 let ty = match t.as_str() {
248 "str" => "String",
249 "i64" => "i64",
250 "f64" => "f64",
251 _ => {
252 panic!("Unsupported const bag type: {}", t);
253 }
254 };
255 writeln!(
256 &mut source_file,
257 "const __CONST_BAG_VALUE_{id} : ConstBagRef<{ty}> = ConstBagRef::<{ty}>::new({id});",
258 id = id,
259 ty = ty,
260 )?;
261 }
262
263 for (id, ir) in self.ir.iter().enumerate() {
264 let ir_path = source_dir.as_path().join(format!("grass_ir_{}.json", id));
265 let ir_file = File::create(&ir_path)?;
266 serde_json::to_writer(ir_file, ir)?;
267
268 writeln!(
269 &mut source_file,
270 "fn grass_query_{id}(cmd_args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {{",
271 id = id
272 )?;
273 writeln!(
274 &mut source_file,
275 " grass_macro::import_grass_ir_from_file!(\"{ir_file}\");",
276 ir_file = ir_path.as_os_str().to_string_lossy()
277 )?;
278 writeln!(&mut source_file, " Ok(())")?;
279 writeln!(&mut source_file, "}}")?;
280 }
281
282 writeln!(
283 &mut source_file,
284 "fn main() -> Result<(), Box<dyn std::error::Error>> {{"
285 )?;
286
287 writeln!(
288 &mut source_file,
289 " let owned_cmd_args: Vec<_> = std::env::args().collect();"
290 )?;
291
292 writeln!(
293 &mut source_file,
294 " let cmd_args: Vec<_> = owned_cmd_args.iter().map(|a| a.as_str()).collect();"
295 )?;
296
297 for id in 0..self.ir.len() {
298 writeln!(
299 &mut source_file,
300 " grass_query_{id}(&cmd_args)?;",
301 id = id
302 )?;
303 }
304 writeln!(&mut source_file, " Ok(())")?;
305 writeln!(&mut source_file, "}}")?;
306
307 Ok(())
308 }
309 pub fn get_compilation_dir(&mut self) -> Result<&Path> {
310 if self.compilation_dir.is_some() {
311 Ok(self.compilation_dir.as_ref().unwrap().as_ref())
312 } else {
313 let mut root_dir = tempfile::Builder::new();
314
315 root_dir.prefix("grass-workspace-");
316 root_dir.rand_bytes(5);
317 let compilation_dir = if let Some(temp_dir) = &self.temp_dir {
318 root_dir.tempdir_in(temp_dir.as_path())?
319 } else {
320 root_dir.tempdir()?
321 };
322
323 self.write_manifest_file(compilation_dir.path())?;
324 self.write_source_code(compilation_dir.path())?;
325
326 self.compilation_dir = Some(compilation_dir);
327 Ok(self.compilation_dir.as_ref().unwrap().path())
328 }
329 }
330 pub fn build_artifact(&mut self) -> Result<&Path> {
331 log::info!("Building artifact {}", self.get_artifact_name()?);
332 let mut child = match self.build_flavor {
333 BuildFlavor::Debug => self.cargo(&["build"], true),
334 BuildFlavor::Release | BuildFlavor::ReleaseWithDebugInfo => {
335 self.cargo(&["build", "--release"], true)
336 }
337 }?;
338 let status = child.wait()?;
339 if status.success() {
340 self.artifact_path = Some(self.get_artifact_path()?);
341 return self.get_artifact();
342 }
343
344 Err(Error::new(
345 std::io::ErrorKind::Other,
346 format!(
347 "Cargo exited with error code {}",
348 status.code().unwrap_or(0)
349 ),
350 ))
351 }
352 pub fn get_artifact(&mut self) -> Result<&Path> {
353 if self.artifact_path.is_some() {
354 return Ok(self.artifact_path.as_ref().unwrap());
355 }
356
357 let mut cache = if self.use_cache || self.update_cache {
358 Some(CacheState::load_cache(&self.cache_root)?)
359 } else {
360 None
361 };
362
363 if self.use_cache {
364 log::info!(
365 "Checking binary cache for binary {}",
366 self.get_artifact_hash()?
367 );
368 let mut cached_artifact = PathBuf::new();
369 if cache
370 .as_mut()
371 .unwrap()
372 .query_cache_entry(self.get_artifact_hash()?, &mut cached_artifact)?
373 {
374 log::info!(
375 "Found cached binary at {}",
376 cached_artifact.to_str().unwrap()
377 );
378 self.artifact_path = Some(cached_artifact);
379 return self.get_artifact();
380 }
381 }
382
383 if self.update_cache {
384 let hash = self.get_artifact_hash()?.to_string();
385 let mut cached_path = PathBuf::new();
386 cache.as_mut().unwrap().update_cache(
387 &hash,
388 |buf| {
389 let path = self.build_artifact()?;
390 *buf = path.to_path_buf();
391 Ok(())
392 },
393 &mut cached_path,
394 )?;
395 self.artifact_path = Some(cached_path);
396 return self.get_artifact();
397 }
398 self.build_artifact()
399 }
400
401 pub fn execute_artifact(&mut self) -> Result<Child> {
402 let working_dir = self.working_dir.clone();
403 let environment = self.env_vars.clone();
404 let artifact_path = self.get_artifact()?;
405 log::info!(
406 "Launching artifact {}",
407 artifact_path.as_os_str().to_string_lossy()
408 );
409 log::info!("Working directory = {:?}", working_dir);
410 log::info!("Environment vars: {:?}", environment);
411 Ok(Command::new(artifact_path)
412 .current_dir(&self.working_dir)
413 .envs(&self.env_vars)
414 .args(&self.cmdline_args)
415 .spawn()?)
416 }
417 pub fn print_expanded_code(&mut self) -> Result<()> {
418 self.cargo(&["expand"], false)?.wait()?;
419 Ok(())
420 }
421}