Skip to main content

cargo_image_runner/core/
context.rs

1use crate::config::Config;
2use crate::core::error::Result;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Context object that carries state through the build pipeline.
7pub struct Context {
8    /// Configuration.
9    pub config: Config,
10
11    /// Workspace root directory.
12    pub workspace_root: PathBuf,
13
14    /// Target directory for build artifacts.
15    pub target_dir: PathBuf,
16
17    /// Path to the executable being run.
18    pub executable: PathBuf,
19
20    /// Whether this is a test run.
21    pub is_test: bool,
22
23    /// Cache directory for downloaded/generated files.
24    pub cache_dir: PathBuf,
25
26    /// Output directory for the current build.
27    pub output_dir: PathBuf,
28
29    /// Template variables available for substitution.
30    pub template_vars: HashMap<String, String>,
31
32    /// Extra QEMU arguments from CLI passthrough (`-- args`).
33    pub cli_extra_args: Vec<String>,
34
35    /// Extra QEMU arguments from `CARGO_IMAGE_RUNNER_QEMU_ARGS` env var.
36    pub env_extra_args: Vec<String>,
37}
38
39impl Context {
40    /// Create a new context from configuration and executable path.
41    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        // Ensure directories exist
47        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        // Detect if this is a test run
64        ctx.detect_test();
65
66        // Initialize template variables
67        ctx.init_template_vars();
68
69        Ok(ctx)
70    }
71
72    /// Detect if the executable is a test binary.
73    ///
74    /// Tests are detected by checking if the executable name ends with a hash
75    /// (Cargo appends a hash suffix to test binaries).
76    /// Uses `file_stem()` to strip extensions like `.efi` before checking.
77    pub fn detect_test(&mut self) {
78        if let Some(stem) = self.executable.file_stem().and_then(|n| n.to_str()) {
79            // Test binaries typically have a hash suffix like "mytest-a1b2c3d4"
80            // We look for a pattern: contains '-' and ends with hex-like characters
81            if stem.contains('-') {
82                if let Some(suffix) = stem.rsplit('-').next() {
83                    // Check if suffix looks like a hex hash (8+ hex characters)
84                    if suffix.len() >= 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
85                        self.is_test = true;
86                    }
87                }
88            }
89        }
90    }
91
92    /// Initialize template variables.
93    ///
94    /// Layering order:
95    /// 1. Config variables (`[variables]`)
96    /// 2. Env var variables (`CARGO_IMAGE_RUNNER_VAR_*`)
97    /// 3. Built-in variables (always win)
98    fn init_template_vars(&mut self) {
99        // 1. Start with user-defined variables from config
100        self.template_vars = self.config.variables.clone();
101
102        // 2. Overlay env var variables (override config vars)
103        for (key, value) in crate::config::env::collect_env_variables() {
104            self.template_vars.insert(key, value);
105        }
106
107        // 3. Built-in variables (always override everything)
108        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        // ARGS: CLI extra args joined with spaces (for kernel command line).
134        // Initialized empty here; populated later when cli_extra_args are available.
135        self.template_vars
136            .insert("ARGS".to_string(), String::new());
137    }
138
139    /// Get the appropriate extra arguments based on whether this is a test run.
140    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    /// Get the success exit code for tests, if configured.
149    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        // Set env var that should override
312        let env_key = "CARGO_IMAGE_RUNNER_VAR_MYVAR";
313        let old = std::env::var(env_key).ok();
314        // SAFETY: test is serialized via LOCK
315        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        // SAFETY: test is serialized via LOCK
321        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        // Try to override a built-in var via env — it should NOT succeed
338        let env_key = "CARGO_IMAGE_RUNNER_VAR_EXECUTABLE_NAME";
339        let old = std::env::var(env_key).ok();
340        // SAFETY: test is serialized via LOCK
341        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        // Built-in EXECUTABLE_NAME should win
345        assert_eq!(ctx.template_vars.get("EXECUTABLE_NAME").unwrap(), "my-kernel");
346
347        // SAFETY: test is serialized via LOCK
348        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}