Skip to main content

opal/executor/
script.rs

1use crate::model::JobSpec;
2use crate::naming::job_name_slug;
3use anyhow::{Context, Result};
4use std::fs::{self, File};
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8pub fn write_job_script(
9    scripts_dir: &Path,
10    container_workdir: &Path,
11    job: &JobSpec,
12    commands: &[String],
13    verbose: bool,
14) -> Result<PathBuf> {
15    let slug = job_name_slug(&job.name);
16    let script_path = scripts_dir.join(format!("{slug}.sh"));
17    if let Some(parent) = script_path.parent() {
18        fs::create_dir_all(parent).with_context(|| format!("failed to create dir {:?}", parent))?;
19    }
20
21    let mut file = File::create(&script_path)
22        .with_context(|| format!("failed to create script for {}", job.name))?;
23    writeln!(file, "#!/usr/bin/env sh")?;
24    if verbose {
25        writeln!(file, "set -ex")?;
26    } else {
27        writeln!(file, "set -e")?;
28    }
29    writeln!(file, "cd {}", container_workdir.display())?;
30    writeln!(file)?;
31
32    for line in commands {
33        if line.trim().is_empty() {
34            continue;
35        }
36        writeln!(
37            file,
38            "printf '%s\\n' {}",
39            shell_quote(&format!("$ {}", line))
40        )?;
41        writeln!(file, "{}", line)?;
42        writeln!(file)?;
43    }
44
45    Ok(script_path)
46}
47
48fn shell_quote(value: &str) -> String {
49    format!("'{}'", value.replace('\'', "'\"'\"'"))
50}
51
52#[cfg(test)]
53mod tests {
54    use super::write_job_script;
55    use crate::model::{ArtifactSpec, JobSpec, RetryPolicySpec};
56    use std::collections::HashMap;
57    use std::fs;
58    use tempfile::tempdir;
59
60    #[test]
61    fn writes_non_verbose_script_without_nounset() {
62        let dir = tempdir().expect("tempdir");
63        let script_path = write_job_script(
64            dir.path(),
65            Path::new("/builds/project"),
66            &job(),
67            &["echo hello".to_string()],
68            false,
69        )
70        .expect("write script");
71        let script = fs::read_to_string(script_path).expect("read script");
72        assert!(script.contains("set -e"));
73        assert!(!script.contains("set -eu"));
74    }
75
76    #[test]
77    fn writes_verbose_script_without_nounset() {
78        let dir = tempdir().expect("tempdir");
79        let script_path = write_job_script(
80            dir.path(),
81            Path::new("/builds/project"),
82            &job(),
83            &["echo hello".to_string()],
84            true,
85        )
86        .expect("write script");
87        let script = fs::read_to_string(script_path).expect("read script");
88        assert!(script.contains("set -ex"));
89        assert!(!script.contains("set -eux"));
90    }
91
92    #[test]
93    fn writes_script_with_command_tracing_lines() {
94        let dir = tempdir().expect("tempdir");
95        let script_path = write_job_script(
96            dir.path(),
97            Path::new("/builds/project"),
98            &job(),
99            &["test -n \"$QUAY_USERNAME\"".to_string()],
100            false,
101        )
102        .expect("write script");
103        let script = fs::read_to_string(script_path).expect("read script");
104        assert!(script.contains("printf '%s\\n' '$ test -n \"$QUAY_USERNAME\"'"));
105        assert!(script.contains("test -n \"$QUAY_USERNAME\""));
106    }
107
108    fn job() -> JobSpec {
109        JobSpec {
110            name: "test".into(),
111            stage: "build".into(),
112            commands: vec!["echo hello".into()],
113            needs: Vec::new(),
114            explicit_needs: false,
115            dependencies: Vec::new(),
116            before_script: None,
117            after_script: None,
118            inherit_default_before_script: true,
119            inherit_default_after_script: true,
120            inherit_default_image: true,
121            inherit_default_cache: true,
122            inherit_default_services: true,
123            inherit_default_timeout: true,
124            inherit_default_retry: true,
125            inherit_default_interruptible: true,
126            when: None,
127            rules: Vec::new(),
128            only: Vec::new(),
129            except: Vec::new(),
130            artifacts: ArtifactSpec::default(),
131            cache: Vec::new(),
132            image: None,
133            variables: HashMap::new(),
134            services: Vec::new(),
135            timeout: None,
136            retry: RetryPolicySpec::default(),
137            interruptible: false,
138            resource_group: None,
139            parallel: None,
140            tags: Vec::new(),
141            environment: None,
142        }
143    }
144
145    use std::path::Path;
146}