1use crate::config::Config;
2use crate::core::error::Result;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6pub struct Context {
8 pub config: Config,
10
11 pub workspace_root: PathBuf,
13
14 pub target_dir: PathBuf,
16
17 pub executable: PathBuf,
19
20 pub is_test: bool,
22
23 pub cache_dir: PathBuf,
25
26 pub output_dir: PathBuf,
28
29 pub template_vars: HashMap<String, String>,
31
32 pub cli_extra_args: Vec<String>,
34
35 pub env_extra_args: Vec<String>,
37}
38
39impl Context {
40 pub fn new(config: Config, workspace_root: PathBuf, executable: PathBuf) -> Result<Self> {
42 let target_dir = workspace_root.join("target").join("image-runner");
43 let cache_dir = target_dir.join("cache");
44 let output_dir = target_dir.join("output");
45
46 std::fs::create_dir_all(&cache_dir)?;
48 std::fs::create_dir_all(&output_dir)?;
49
50 let mut ctx = Self {
51 config,
52 workspace_root: workspace_root.clone(),
53 target_dir,
54 executable: executable.clone(),
55 is_test: false,
56 cache_dir,
57 output_dir,
58 template_vars: HashMap::new(),
59 cli_extra_args: Vec::new(),
60 env_extra_args: Vec::new(),
61 };
62
63 ctx.detect_test();
65
66 ctx.init_template_vars();
68
69 Ok(ctx)
70 }
71
72 pub fn detect_test(&mut self) {
78 if let Some(stem) = self.executable.file_stem().and_then(|n| n.to_str()) {
79 if stem.contains('-') {
82 if let Some(suffix) = stem.rsplit('-').next() {
83 if suffix.len() >= 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
85 self.is_test = true;
86 }
87 }
88 }
89 }
90 }
91
92 fn init_template_vars(&mut self) {
99 self.template_vars = self.config.variables.clone();
101
102 for (key, value) in crate::config::env::collect_env_variables() {
104 self.template_vars.insert(key, value);
105 }
106
107 self.template_vars.insert(
109 "EXECUTABLE".to_string(),
110 self.executable.display().to_string(),
111 );
112
113 if let Some(exe_name) = self.executable.file_name().and_then(|n| n.to_str()) {
114 self.template_vars
115 .insert("EXECUTABLE_NAME".to_string(), exe_name.to_string());
116 }
117
118 self.template_vars.insert(
119 "WORKSPACE_ROOT".to_string(),
120 self.workspace_root.display().to_string(),
121 );
122
123 self.template_vars.insert(
124 "OUTPUT_DIR".to_string(),
125 self.output_dir.display().to_string(),
126 );
127
128 self.template_vars.insert(
129 "IS_TEST".to_string(),
130 if self.is_test { "1" } else { "0" }.to_string(),
131 );
132
133 self.template_vars
136 .insert("ARGS".to_string(), String::new());
137 }
138
139 pub fn get_extra_args(&self) -> &[String] {
141 if self.is_test {
142 &self.config.test.extra_args
143 } else {
144 &self.config.run.extra_args
145 }
146 }
147
148 pub fn test_success_exit_code(&self) -> Option<i32> {
150 self.config.test.success_exit_code
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::config::Config;
158
159 fn make_context(workspace: &std::path::Path, exe: &std::path::Path) -> Context {
160 Context::new(Config::default(), workspace.to_path_buf(), exe.to_path_buf()).unwrap()
161 }
162
163 #[test]
164 fn test_context_paths() {
165 let dir = tempfile::tempdir().unwrap();
166 let exe = dir.path().join("my-kernel");
167 std::fs::write(&exe, b"fake").unwrap();
168
169 let ctx = make_context(dir.path(), &exe);
170 assert_eq!(ctx.target_dir, dir.path().join("target/image-runner"));
171 assert_eq!(ctx.cache_dir, dir.path().join("target/image-runner/cache"));
172 assert_eq!(
173 ctx.output_dir,
174 dir.path().join("target/image-runner/output")
175 );
176 }
177
178 #[test]
179 fn test_builtin_template_vars() {
180 let dir = tempfile::tempdir().unwrap();
181 let exe = dir.path().join("my-kernel");
182 std::fs::write(&exe, b"fake").unwrap();
183
184 let ctx = make_context(dir.path(), &exe);
185 assert_eq!(
186 ctx.template_vars.get("EXECUTABLE").unwrap(),
187 &exe.display().to_string()
188 );
189 assert_eq!(
190 ctx.template_vars.get("EXECUTABLE_NAME").unwrap(),
191 "my-kernel"
192 );
193 assert_eq!(
194 ctx.template_vars.get("WORKSPACE_ROOT").unwrap(),
195 &dir.path().display().to_string()
196 );
197 assert_eq!(
198 ctx.template_vars.get("OUTPUT_DIR").unwrap(),
199 &ctx.output_dir.display().to_string()
200 );
201 assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "0");
202 }
203
204 #[test]
205 fn test_user_variables_included() {
206 let dir = tempfile::tempdir().unwrap();
207 let exe = dir.path().join("my-kernel");
208 std::fs::write(&exe, b"fake").unwrap();
209
210 let mut config = Config::default();
211 config
212 .variables
213 .insert("MY_VAR".to_string(), "hello".to_string());
214
215 let ctx =
216 Context::new(config, dir.path().to_path_buf(), exe).unwrap();
217 assert_eq!(ctx.template_vars.get("MY_VAR").unwrap(), "hello");
218 }
219
220 #[test]
221 fn test_detect_test_with_hash_suffix() {
222 let dir = tempfile::tempdir().unwrap();
223 let exe = dir.path().join("my-test-a1b2c3d4e5f6a7b8");
224 std::fs::write(&exe, b"fake").unwrap();
225
226 let ctx = make_context(dir.path(), &exe);
227 assert!(ctx.is_test);
228 assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "1");
229 }
230
231 #[test]
232 fn test_detect_test_with_efi_extension() {
233 let dir = tempfile::tempdir().unwrap();
234 let exe = dir.path().join("basic_boot-edba05eea98a559f.efi");
235 std::fs::write(&exe, b"fake").unwrap();
236
237 let ctx = make_context(dir.path(), &exe);
238 assert!(ctx.is_test);
239 assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "1");
240 }
241
242 #[test]
243 fn test_detect_normal_executable() {
244 let dir = tempfile::tempdir().unwrap();
245 let exe = dir.path().join("my-kernel");
246 std::fs::write(&exe, b"fake").unwrap();
247
248 let ctx = make_context(dir.path(), &exe);
249 assert!(!ctx.is_test);
250 }
251
252 #[test]
253 fn test_get_extra_args_test_mode() {
254 let dir = tempfile::tempdir().unwrap();
255 let exe = dir.path().join("my-test-a1b2c3d4e5f6a7b8");
256 std::fs::write(&exe, b"fake").unwrap();
257
258 let mut config = Config::default();
259 config.test.extra_args = vec!["-device".to_string(), "isa-debug-exit".to_string()];
260 config.run.extra_args = vec!["-serial".to_string(), "stdio".to_string()];
261
262 let ctx =
263 Context::new(config, dir.path().to_path_buf(), exe).unwrap();
264 assert!(ctx.is_test);
265 assert_eq!(ctx.get_extra_args(), &["-device", "isa-debug-exit"]);
266 }
267
268 #[test]
269 fn test_get_extra_args_run_mode() {
270 let dir = tempfile::tempdir().unwrap();
271 let exe = dir.path().join("my-kernel");
272 std::fs::write(&exe, b"fake").unwrap();
273
274 let mut config = Config::default();
275 config.test.extra_args = vec!["-device".to_string(), "isa-debug-exit".to_string()];
276 config.run.extra_args = vec!["-serial".to_string(), "stdio".to_string()];
277
278 let ctx =
279 Context::new(config, dir.path().to_path_buf(), exe).unwrap();
280 assert!(!ctx.is_test);
281 assert_eq!(ctx.get_extra_args(), &["-serial", "stdio"]);
282 }
283
284 #[test]
285 fn test_success_exit_code() {
286 let dir = tempfile::tempdir().unwrap();
287 let exe = dir.path().join("my-kernel");
288 std::fs::write(&exe, b"fake").unwrap();
289
290 let mut config = Config::default();
291 config.test.success_exit_code = Some(33);
292
293 let ctx =
294 Context::new(config, dir.path().to_path_buf(), exe).unwrap();
295 assert_eq!(ctx.test_success_exit_code(), Some(33));
296 }
297
298 #[test]
299 fn test_env_variables_override_config() {
300 use std::sync::Mutex;
301 static LOCK: Mutex<()> = Mutex::new(());
302 let _guard = LOCK.lock().unwrap();
303
304 let dir = tempfile::tempdir().unwrap();
305 let exe = dir.path().join("my-kernel");
306 std::fs::write(&exe, b"fake").unwrap();
307
308 let mut config = Config::default();
309 config.variables.insert("MYVAR".to_string(), "from_config".to_string());
310
311 let env_key = "CARGO_IMAGE_RUNNER_VAR_MYVAR";
313 let old = std::env::var(env_key).ok();
314 unsafe { std::env::set_var(env_key, "from_env") };
316
317 let ctx = Context::new(config, dir.path().to_path_buf(), exe).unwrap();
318 assert_eq!(ctx.template_vars.get("MYVAR").unwrap(), "from_env");
319
320 match old {
322 Some(v) => unsafe { std::env::set_var(env_key, v) },
323 None => unsafe { std::env::remove_var(env_key) },
324 }
325 }
326
327 #[test]
328 fn test_builtin_vars_override_env_vars() {
329 use std::sync::Mutex;
330 static LOCK: Mutex<()> = Mutex::new(());
331 let _guard = LOCK.lock().unwrap();
332
333 let dir = tempfile::tempdir().unwrap();
334 let exe = dir.path().join("my-kernel");
335 std::fs::write(&exe, b"fake").unwrap();
336
337 let env_key = "CARGO_IMAGE_RUNNER_VAR_EXECUTABLE_NAME";
339 let old = std::env::var(env_key).ok();
340 unsafe { std::env::set_var(env_key, "should_not_win") };
342
343 let ctx = Context::new(Config::default(), dir.path().to_path_buf(), exe).unwrap();
344 assert_eq!(ctx.template_vars.get("EXECUTABLE_NAME").unwrap(), "my-kernel");
346
347 match old {
349 Some(v) => unsafe { std::env::set_var(env_key, v) },
350 None => unsafe { std::env::remove_var(env_key) },
351 }
352 }
353}