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
33impl Context {
34    /// Create a new context from configuration and executable path.
35    pub fn new(config: Config, workspace_root: PathBuf, executable: PathBuf) -> Result<Self> {
36        let target_dir = workspace_root.join("target").join("image-runner");
37        let cache_dir = target_dir.join("cache");
38        let output_dir = target_dir.join("output");
39
40        // Ensure directories exist
41        std::fs::create_dir_all(&cache_dir)?;
42        std::fs::create_dir_all(&output_dir)?;
43
44        let mut ctx = Self {
45            config,
46            workspace_root: workspace_root.clone(),
47            target_dir,
48            executable: executable.clone(),
49            is_test: false,
50            cache_dir,
51            output_dir,
52            template_vars: HashMap::new(),
53        };
54
55        // Detect if this is a test run
56        ctx.detect_test();
57
58        // Initialize template variables
59        ctx.init_template_vars();
60
61        Ok(ctx)
62    }
63
64    /// Detect if the executable is a test binary.
65    ///
66    /// Tests are detected by checking if the executable name ends with a hash
67    /// (Cargo appends a hash suffix to test binaries).
68    /// Uses `file_stem()` to strip extensions like `.efi` before checking.
69    pub fn detect_test(&mut self) {
70        if let Some(stem) = self.executable.file_stem().and_then(|n| n.to_str()) {
71            // Test binaries typically have a hash suffix like "mytest-a1b2c3d4"
72            // We look for a pattern: contains '-' and ends with hex-like characters
73            if stem.contains('-') {
74                if let Some(suffix) = stem.rsplit('-').next() {
75                    // Check if suffix looks like a hex hash (8+ hex characters)
76                    if suffix.len() >= 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
77                        self.is_test = true;
78                    }
79                }
80            }
81        }
82    }
83
84    /// Initialize template variables.
85    fn init_template_vars(&mut self) {
86        // Start with user-defined variables from config
87        self.template_vars = self.config.variables.clone();
88
89        // Add built-in variables
90        self.template_vars.insert(
91            "EXECUTABLE".to_string(),
92            self.executable.display().to_string(),
93        );
94
95        if let Some(exe_name) = self.executable.file_name().and_then(|n| n.to_str()) {
96            self.template_vars
97                .insert("EXECUTABLE_NAME".to_string(), exe_name.to_string());
98        }
99
100        self.template_vars.insert(
101            "WORKSPACE_ROOT".to_string(),
102            self.workspace_root.display().to_string(),
103        );
104
105        self.template_vars.insert(
106            "OUTPUT_DIR".to_string(),
107            self.output_dir.display().to_string(),
108        );
109
110        self.template_vars.insert(
111            "IS_TEST".to_string(),
112            if self.is_test { "1" } else { "0" }.to_string(),
113        );
114    }
115
116    /// Get the appropriate extra arguments based on whether this is a test run.
117    pub fn get_extra_args(&self) -> &[String] {
118        if self.is_test {
119            &self.config.test.extra_args
120        } else {
121            &self.config.run.extra_args
122        }
123    }
124
125    /// Get the success exit code for tests, if configured.
126    pub fn test_success_exit_code(&self) -> Option<i32> {
127        self.config.test.success_exit_code
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::config::Config;
135
136    fn make_context(workspace: &std::path::Path, exe: &std::path::Path) -> Context {
137        Context::new(Config::default(), workspace.to_path_buf(), exe.to_path_buf()).unwrap()
138    }
139
140    #[test]
141    fn test_context_paths() {
142        let dir = tempfile::tempdir().unwrap();
143        let exe = dir.path().join("my-kernel");
144        std::fs::write(&exe, b"fake").unwrap();
145
146        let ctx = make_context(dir.path(), &exe);
147        assert_eq!(ctx.target_dir, dir.path().join("target/image-runner"));
148        assert_eq!(ctx.cache_dir, dir.path().join("target/image-runner/cache"));
149        assert_eq!(
150            ctx.output_dir,
151            dir.path().join("target/image-runner/output")
152        );
153    }
154
155    #[test]
156    fn test_builtin_template_vars() {
157        let dir = tempfile::tempdir().unwrap();
158        let exe = dir.path().join("my-kernel");
159        std::fs::write(&exe, b"fake").unwrap();
160
161        let ctx = make_context(dir.path(), &exe);
162        assert_eq!(
163            ctx.template_vars.get("EXECUTABLE").unwrap(),
164            &exe.display().to_string()
165        );
166        assert_eq!(
167            ctx.template_vars.get("EXECUTABLE_NAME").unwrap(),
168            "my-kernel"
169        );
170        assert_eq!(
171            ctx.template_vars.get("WORKSPACE_ROOT").unwrap(),
172            &dir.path().display().to_string()
173        );
174        assert_eq!(
175            ctx.template_vars.get("OUTPUT_DIR").unwrap(),
176            &ctx.output_dir.display().to_string()
177        );
178        assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "0");
179    }
180
181    #[test]
182    fn test_user_variables_included() {
183        let dir = tempfile::tempdir().unwrap();
184        let exe = dir.path().join("my-kernel");
185        std::fs::write(&exe, b"fake").unwrap();
186
187        let mut config = Config::default();
188        config
189            .variables
190            .insert("MY_VAR".to_string(), "hello".to_string());
191
192        let ctx =
193            Context::new(config, dir.path().to_path_buf(), exe).unwrap();
194        assert_eq!(ctx.template_vars.get("MY_VAR").unwrap(), "hello");
195    }
196
197    #[test]
198    fn test_detect_test_with_hash_suffix() {
199        let dir = tempfile::tempdir().unwrap();
200        let exe = dir.path().join("my-test-a1b2c3d4e5f6a7b8");
201        std::fs::write(&exe, b"fake").unwrap();
202
203        let ctx = make_context(dir.path(), &exe);
204        assert!(ctx.is_test);
205        assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "1");
206    }
207
208    #[test]
209    fn test_detect_test_with_efi_extension() {
210        let dir = tempfile::tempdir().unwrap();
211        let exe = dir.path().join("basic_boot-edba05eea98a559f.efi");
212        std::fs::write(&exe, b"fake").unwrap();
213
214        let ctx = make_context(dir.path(), &exe);
215        assert!(ctx.is_test);
216        assert_eq!(ctx.template_vars.get("IS_TEST").unwrap(), "1");
217    }
218
219    #[test]
220    fn test_detect_normal_executable() {
221        let dir = tempfile::tempdir().unwrap();
222        let exe = dir.path().join("my-kernel");
223        std::fs::write(&exe, b"fake").unwrap();
224
225        let ctx = make_context(dir.path(), &exe);
226        assert!(!ctx.is_test);
227    }
228
229    #[test]
230    fn test_get_extra_args_test_mode() {
231        let dir = tempfile::tempdir().unwrap();
232        let exe = dir.path().join("my-test-a1b2c3d4e5f6a7b8");
233        std::fs::write(&exe, b"fake").unwrap();
234
235        let mut config = Config::default();
236        config.test.extra_args = vec!["-device".to_string(), "isa-debug-exit".to_string()];
237        config.run.extra_args = vec!["-serial".to_string(), "stdio".to_string()];
238
239        let ctx =
240            Context::new(config, dir.path().to_path_buf(), exe).unwrap();
241        assert!(ctx.is_test);
242        assert_eq!(ctx.get_extra_args(), &["-device", "isa-debug-exit"]);
243    }
244
245    #[test]
246    fn test_get_extra_args_run_mode() {
247        let dir = tempfile::tempdir().unwrap();
248        let exe = dir.path().join("my-kernel");
249        std::fs::write(&exe, b"fake").unwrap();
250
251        let mut config = Config::default();
252        config.test.extra_args = vec!["-device".to_string(), "isa-debug-exit".to_string()];
253        config.run.extra_args = vec!["-serial".to_string(), "stdio".to_string()];
254
255        let ctx =
256            Context::new(config, dir.path().to_path_buf(), exe).unwrap();
257        assert!(!ctx.is_test);
258        assert_eq!(ctx.get_extra_args(), &["-serial", "stdio"]);
259    }
260
261    #[test]
262    fn test_success_exit_code() {
263        let dir = tempfile::tempdir().unwrap();
264        let exe = dir.path().join("my-kernel");
265        std::fs::write(&exe, b"fake").unwrap();
266
267        let mut config = Config::default();
268        config.test.success_exit_code = Some(33);
269
270        let ctx =
271            Context::new(config, dir.path().to_path_buf(), exe).unwrap();
272        assert_eq!(ctx.test_success_exit_code(), Some(33));
273    }
274}