1use std::{
4 ffi::OsString,
5 fs::File,
6 os::unix::ffi::OsStringExt,
7 path::{Path, PathBuf},
8 process::Command,
9};
10
11use log::{error, trace};
12use serde::Deserialize;
13
14use crate::runlog::RunLog;
15
16#[derive(Debug, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct Spec {
20 pub description: String,
24
25 pub repository_url: String,
29
30 pub git_ref: String,
38
39 pub command: String,
43}
44
45impl Spec {
46 pub fn from_file(filename: &Path) -> anyhow::Result<Self> {
48 let file = File::open(filename)?;
49 let spec: Self = serde_yaml::from_reader(&file)?;
50 Ok(spec)
51 }
52
53 pub fn versions(&self, run_log: &mut RunLog) -> anyhow::Result<()> {
55 RunCmd::new(".", run_log)
56 .arg("rustc")
57 .arg("--version")
58 .run()?;
59 RunCmd::new(".", run_log)
60 .arg("cargo")
61 .arg("--version")
62 .run()?;
63 Ok(())
64 }
65
66 pub fn git_clone(&self, working_dir: &Path, run_log: &mut RunLog) -> anyhow::Result<()> {
68 RunCmd::new(".", run_log)
69 .arg("git")
70 .arg("clone")
71 .arg(&self.repository_url)
72 .path(working_dir)
73 .run()?;
74 Ok(())
75 }
76
77 pub fn git_remote_update(
80 &self,
81 working_dir: &Path,
82 run_log: &mut RunLog,
83 ) -> anyhow::Result<()> {
84 RunCmd::new(working_dir, run_log)
85 .arg("git")
86 .arg("remote")
87 .arg("update")
88 .run()?;
89 Ok(())
90 }
91
92 pub fn git_checkout(
94 &self,
95 working_dir: &Path,
96 committish: &str,
97 run_log: &mut RunLog,
98 ) -> anyhow::Result<()> {
99 RunCmd::new(working_dir, run_log)
100 .arg("git")
101 .arg("checkout")
102 .arg(committish)
103 .run()?;
104 Ok(())
105 }
106
107 pub fn git_head(&self, working_dir: &Path, run_log: &mut RunLog) -> anyhow::Result<String> {
109 let (stdout, _) = RunCmd::new(working_dir, run_log)
110 .arg("git")
111 .arg("rev-parse")
112 .arg("HEAD")
113 .run()?;
114 Ok(stdout.trim().into())
115 }
116
117 pub fn git_commit_date(
119 &self,
120 working_dir: &Path,
121 commit: &str,
122 run_log: &mut RunLog,
123 ) -> String {
124 if let Ok((stdout, _)) = RunCmd::new(working_dir, run_log)
125 .arg("git")
126 .arg("show")
127 .arg("--pretty=fuller")
128 .arg("--date=iso")
129 .arg(commit)
130 .run()
131 {
132 const PREFIX: &str = "CommitDate: ";
133 let x: Vec<String> = stdout
134 .lines()
135 .filter_map(|line| line.strip_prefix(PREFIX).map(|s| s.to_string()))
136 .collect();
137 if x.len() == 1 {
138 return x[0].clone();
139 }
140 }
141 "(unknown date)".into()
142 }
143
144 pub fn run_test_suite(
146 &self,
147 working_dir: &Path,
148 timeout: usize,
149 tmpdir: &Path,
150 run_log: &mut RunLog,
151 ) -> anyhow::Result<(String, bool)> {
152 RunCmd::new(working_dir, run_log)
153 .tmpdir(tmpdir)
154 .arg("timeout")
155 .arg(&format!("{}s", timeout))
156 .arg("bash")
157 .arg("-c")
158 .arg(&self.command)
159 .run()
160 }
161}
162
163#[derive(Debug)]
164struct RunCmd<'a> {
165 argv: Vec<OsString>,
166 cwd: PathBuf,
167 tmpdir: Option<PathBuf>,
168 run_log: &'a mut RunLog,
169}
170
171impl<'a> RunCmd<'a> {
172 fn new<P: AsRef<Path>>(cwd: P, run_log: &'a mut RunLog) -> Self {
173 let cwd = cwd.as_ref();
174 if !cwd.exists() {
175 error!("ERROR: directory {} does not exist", cwd.display());
176 }
177 assert!(cwd.exists());
178 Self {
179 argv: vec![],
180 cwd: cwd.into(),
181 tmpdir: None,
182 run_log,
183 }
184 }
185
186 fn tmpdir(mut self, dirname: &Path) -> Self {
187 self.tmpdir = Some(dirname.into());
188 self
189 }
190
191 fn arg(mut self, arg: &str) -> Self {
192 self.argv.push(OsString::from_vec(arg.as_bytes().to_vec()));
193 self
194 }
195
196 fn path(mut self, arg: &Path) -> Self {
197 self.argv.push(arg.as_os_str().into());
198 self
199 }
200
201 fn run(self) -> anyhow::Result<(String, bool)> {
202 trace!("runcmd: {self:#?}");
203 let tmpdir = self.tmpdir.unwrap_or(PathBuf::from("/tmp"));
204 let output = Command::new("bash")
205 .arg("-c")
206 .arg(r#""$@" 2>&1"#)
207 .arg("--")
208 .args(&self.argv)
209 .current_dir(&self.cwd)
210 .env("TMPDIR", &tmpdir)
211 .output()?;
212 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
213 trace!("runcmd: stdout:\n{stdout}");
214 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
215 trace!("runcmd: stderr:\n{stderr}");
216 trace!("runcmd: success? {}", output.status.success());
217
218 let argv: Vec<&str> = self.argv.iter().map(|os| os.to_str().unwrap()).collect();
219
220 self.run_log
221 .runcmd(&argv, output.status.code().unwrap(), &stdout, &stderr);
222 Ok((stdout, output.status.success()))
223 }
224}