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}