1use crate::{environment::PackageSource, PythonEnvironment, PythonVersion};
5use anyhow::{anyhow, Result};
6use ispm_wrapper::{
7 data::InstalledPackage,
8 ispm::{self, GlobalOptions},
9};
10use std::{
11 env,
12 fs::read_dir,
13 path::{Path, PathBuf},
14};
15use versions::Versioning;
16
17#[cfg(not(windows))]
18pub const HOST_DIRNAME: &str = "linux64";
20
21#[cfg(windows)]
22pub const HOST_DIRNAME: &str = "win64";
24
25pub const PYTHON3_INCLUDE_ENV: &str = "PYTHON3_INCLUDE";
27pub const PYTHON3_LDFLAGS_ENV: &str = "PYTHON3_LDFLAGS";
29
30#[derive(Debug, Clone)]
36struct PlatformConfig {
37 lib_subdir: &'static [&'static str],
40 lib_prefix: &'static str,
42 lib_extension: &'static str,
44 generic_lib_name: &'static str,
46}
47
48#[cfg(unix)]
49const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
50 lib_subdir: &["sys", "lib"],
51 lib_prefix: "libpython",
52 lib_extension: ".so",
53 generic_lib_name: "libpython3.so",
54};
55
56#[cfg(windows)]
57const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
58 lib_subdir: &["bin"],
59 lib_prefix: "python3",
60 lib_extension: ".dll",
61 generic_lib_name: "python3.dll",
62};
63
64fn is_python_library_name(name: &str, config: &PlatformConfig) -> bool {
70 name.starts_with(config.lib_prefix) && name.contains(config.lib_extension)
71}
72
73fn filter_python_libraries_with_config(files: &mut Vec<PathBuf>, config: &PlatformConfig) {
75 if files.len() > 1 {
77 files.retain(|path| {
78 path.file_name()
79 .and_then(|name| name.to_str())
80 .map(|name| name != config.generic_lib_name)
81 .unwrap_or(false)
82 });
83 }
84
85 if config.lib_extension == ".so" && files.len() > 1 {
87 files.retain(|path| {
88 path.file_name()
89 .and_then(|name| name.to_str())
90 .map(|name| !name.ends_with(".so"))
91 .unwrap_or(false)
92 });
93 }
94}
95
96fn get_lib_dir(base_path: &Path, config: &PlatformConfig) -> PathBuf {
98 config
99 .lib_subdir
100 .iter()
101 .fold(base_path.to_path_buf(), |p, component| p.join(component))
102}
103
104fn find_libpython_in_dir_with_config(
106 lib_dir: &Path,
107 config: &PlatformConfig,
108) -> Result<(PathBuf, PathBuf)> {
109 if !lib_dir.exists() {
110 return Err(anyhow!(
111 "Library directory does not exist: {}",
112 lib_dir.display()
113 ));
114 }
115
116 let entries = read_dir(lib_dir).map_err(|e| {
117 anyhow!(
118 "Failed to read library directory {}: {}",
119 lib_dir.display(),
120 e
121 )
122 })?;
123
124 let mut libpython_files: Vec<PathBuf> = entries
125 .filter_map(|entry| entry.ok())
126 .map(|entry| entry.path())
127 .filter(|path| path.is_file())
128 .filter(|path| {
129 path.file_name()
130 .and_then(|name| name.to_str())
131 .map(|name| is_python_library_name(name, config))
132 .unwrap_or(false)
133 })
134 .collect();
135
136 filter_python_libraries_with_config(&mut libpython_files, config);
137
138 match libpython_files.len() {
139 0 => Err(anyhow!(
140 "No Python library file found in {}",
141 lib_dir.display()
142 )),
143 1 => {
144 let lib_path = libpython_files
145 .into_iter()
146 .next()
147 .expect("exactly one element guaranteed by match arm");
148 Ok((lib_dir.to_path_buf(), lib_path))
149 }
150 _ => Err(anyhow!(
151 "Multiple Python library files found in {}, expected exactly one",
152 lib_dir.display()
153 )),
154 }
155}
156
157fn find_latest_python_package(simics_major_version: u32) -> Result<InstalledPackage> {
163 let packages = ispm::packages::list(&GlobalOptions::default())
164 .map_err(|e| anyhow!("Failed to query ISPM for installed packages: {}", e))?;
165
166 let installed = packages
167 .installed_packages
168 .as_ref()
169 .ok_or_else(|| anyhow!("No installed packages found"))?;
170
171 let python_packages: Vec<_> = installed
173 .iter()
174 .filter(|pkg| pkg.package_number == 1033)
175 .filter(|pkg| pkg.name == "Python")
176 .filter(|pkg| {
177 pkg.version
179 .starts_with(&format!("{}.", simics_major_version))
180 })
181 .collect();
182
183 if python_packages.is_empty() {
184 return Err(anyhow!(
185 "No Python packages found for Simics major version {}",
186 simics_major_version
187 ));
188 }
189
190 let latest_package = python_packages
192 .iter()
193 .max_by(|a, b| {
194 let version_a = Versioning::new(&a.version).unwrap_or_default();
195 let version_b = Versioning::new(&b.version).unwrap_or_default();
196 version_a.cmp(&version_b)
197 })
198 .ok_or_else(|| anyhow!("Failed to find latest Python package"))?;
199
200 Ok((*latest_package).clone())
201}
202
203fn detect_simics_major_version_from_base(simics_base: &Path) -> Result<u32> {
205 if let Some(dir_name) = simics_base.file_name().and_then(|n| n.to_str()) {
206 if let Some(version_part) = dir_name.strip_prefix("simics-") {
208 if let Some(major_str) = version_part.split('.').next() {
209 if let Ok(major) = major_str.parse::<u32>() {
210 return Ok(major);
211 }
212 }
213 }
214 }
215
216 Err(anyhow!(
217 "Unable to determine Simics major version from SIMICS base path {}; expected directory name like simics-x.x.x",
218 simics_base.display()
219 ))
220}
221
222pub fn discover_python_environment() -> Result<PythonEnvironment> {
228 let simics_base =
229 env::var("SIMICS_BASE").map_err(|_| anyhow!("SIMICS_BASE environment variable not set"))?;
230
231 discover_python_environment_from_base(simics_base)
232}
233
234pub fn discover_python_environment_from_base<P: AsRef<Path>>(
236 simics_base: P,
237) -> Result<PythonEnvironment> {
238 let simics_base = simics_base.as_ref();
239
240 let traditional_err = match try_traditional_paths(simics_base) {
242 Ok(env) => return Ok(env.with_source(PackageSource::Traditional)),
243 Err(err) => err.context("Traditional discovery failed"),
244 };
245
246 let dynamic_err = match try_dynamic_python_package_discovery(simics_base) {
248 Ok(env) => return Ok(env.with_source(PackageSource::SeparatePackage)),
249 Err(err) => err.context("Dynamic discovery failed"),
250 };
251
252 Err(anyhow!(
253 "Python environment not found in traditional location ({}) or through dynamic package discovery\nTraditional error: {:#}\nDynamic error: {:#}",
254 simics_base.join(HOST_DIRNAME).display(),
255 traditional_err,
256 dynamic_err
257 ))
258}
259
260fn try_traditional_paths(simics_base: &Path) -> Result<PythonEnvironment> {
262 let base_path = simics_base.join(HOST_DIRNAME);
263 discover_from_base_path(base_path)
264}
265
266fn try_dynamic_python_package_discovery(simics_base: &Path) -> Result<PythonEnvironment> {
268 let major_version = detect_simics_major_version_from_base(simics_base)?;
270
271 let python_package = find_latest_python_package(major_version)?;
273
274 println!(
275 "cargo:warning=Using dynamically discovered Python package: {} (version {})",
276 python_package.name, python_package.version
277 );
278
279 let package_path = python_package.paths.first().ok_or_else(|| {
281 anyhow!(
282 "No installation paths found for Python package {}",
283 python_package.name
284 )
285 })?;
286
287 let python_package_path = package_path.join(HOST_DIRNAME);
289
290 discover_from_base_path(python_package_path)
291}
292
293fn discover_from_base_path(base_path: PathBuf) -> Result<PythonEnvironment> {
295 let mini_python = find_mini_python(&base_path)?;
297 let include_dir = find_python_include(&base_path)?;
299 let (lib_dir, lib_path) = find_python_library(&base_path)?;
302 let version = PythonVersion::parse_from_include_dir(&include_dir)?;
303
304 let env = PythonEnvironment::new(
305 mini_python,
306 include_dir,
307 lib_dir,
308 lib_path,
309 version,
310 PackageSource::Traditional, );
312
313 env.validate()?;
315
316 Ok(env)
317}
318
319fn find_mini_python(base_path: &Path) -> Result<PathBuf> {
321 #[cfg(unix)]
322 let executable_name = "mini-python";
323
324 #[cfg(windows)]
325 let executable_name = "mini-python.exe";
326
327 let mini_python_path = base_path.join("bin").join(executable_name);
328
329 if mini_python_path.exists() {
330 Ok(mini_python_path)
331 } else {
332 Err(anyhow!(
333 "Mini-python executable not found at {}",
334 mini_python_path.display()
335 ))
336 }
337}
338
339fn find_python_include(base_path: &Path) -> Result<PathBuf> {
341 if let Ok(include_env) = env::var(PYTHON3_INCLUDE_ENV) {
343 let include_path = if let Some(path) = include_env.strip_prefix("-I") {
345 PathBuf::from(path)
346 } else {
347 PathBuf::from(include_env)
348 };
349
350 if include_path.exists() {
351 return Ok(include_path);
352 }
353 }
354
355 let include_base = base_path.join("include");
356 find_python_subdir(&include_base)
357}
358
359fn find_python_subdir(include_dir: &Path) -> Result<PathBuf> {
361 if !include_dir.exists() {
362 return Err(anyhow!(
363 "Include directory does not exist: {}",
364 include_dir.display()
365 ));
366 }
367
368 let entries = read_dir(include_dir).map_err(|e| {
369 anyhow!(
370 "Failed to read include directory {}: {}",
371 include_dir.display(),
372 e
373 )
374 })?;
375
376 let python_dirs: Vec<PathBuf> = entries
377 .filter_map(|entry| entry.ok())
378 .map(|entry| entry.path())
379 .filter(|path| path.is_dir())
380 .filter(|path| {
381 path.file_name()
382 .and_then(|name| name.to_str())
383 .map(|name| name.starts_with("python"))
384 .unwrap_or(false)
385 })
386 .collect();
387
388 match python_dirs.len() {
389 0 => Err(anyhow!(
390 "No python* subdirectory found in {}",
391 include_dir.display()
392 )),
393 1 => Ok(python_dirs
394 .into_iter()
395 .next()
396 .expect("exactly one element guaranteed by match arm")),
397 _ => Err(anyhow!(
398 "Multiple python* subdirectories found in {}, expected exactly one",
399 include_dir.display()
400 )),
401 }
402}
403
404fn find_python_library(base_path: &Path) -> Result<(PathBuf, PathBuf)> {
406 if let Ok(lib_env) = env::var(PYTHON3_LDFLAGS_ENV) {
408 let lib_path = PathBuf::from(lib_env);
409 if lib_path.exists() {
410 let lib_dir = lib_path
411 .parent()
412 .ok_or_else(|| anyhow!("Library path has no parent directory"))?
413 .to_path_buf();
414 return Ok((lib_dir, lib_path));
415 }
416 }
417
418 let lib_dir = get_lib_dir(base_path, &PLATFORM_CONFIG);
419 find_libpython_in_dir_with_config(&lib_dir, &PLATFORM_CONFIG)
420}
421
422#[cfg(test)]
427mod tests {
428 use super::*;
429 use std::fs;
430 use tempfile::TempDir;
431
432 const UNIX_CONFIG: PlatformConfig = PlatformConfig {
434 lib_subdir: &["sys", "lib"],
435 lib_prefix: "libpython",
436 lib_extension: ".so",
437 generic_lib_name: "libpython3.so",
438 };
439
440 const WINDOWS_CONFIG: PlatformConfig = PlatformConfig {
441 lib_subdir: &["bin"],
442 lib_prefix: "python3",
443 lib_extension: ".dll",
444 generic_lib_name: "python3.dll",
445 };
446
447 #[test]
452 fn test_is_python_library_name_unix() {
453 assert!(is_python_library_name("libpython3.9.so.1.0", &UNIX_CONFIG));
454 assert!(is_python_library_name("libpython3.so", &UNIX_CONFIG));
455 assert!(is_python_library_name("libpython3.9.so", &UNIX_CONFIG));
456 assert!(!is_python_library_name("python3.9.dll", &UNIX_CONFIG));
457 assert!(!is_python_library_name("libfoo.so", &UNIX_CONFIG));
458 }
459
460 #[test]
461 fn test_is_python_library_name_windows() {
462 assert!(is_python_library_name("python3.10.dll", &WINDOWS_CONFIG));
463 assert!(is_python_library_name("python3.dll", &WINDOWS_CONFIG));
464 assert!(!is_python_library_name("libpython3.9.so", &WINDOWS_CONFIG));
465 assert!(!is_python_library_name("python3.10.lib", &WINDOWS_CONFIG));
466 assert!(!is_python_library_name("python2.7.dll", &WINDOWS_CONFIG));
467 }
468
469 #[test]
470 fn test_get_lib_dir_unix() {
471 let base = PathBuf::from("/opt/simics/linux64");
472 let lib_dir = get_lib_dir(&base, &UNIX_CONFIG);
473 assert_eq!(lib_dir, PathBuf::from("/opt/simics/linux64/sys/lib"));
474 }
475
476 #[test]
477 fn test_get_lib_dir_windows() {
478 let base = PathBuf::from("/opt/simics/win64");
479 let lib_dir = get_lib_dir(&base, &WINDOWS_CONFIG);
480 assert_eq!(lib_dir, PathBuf::from("/opt/simics/win64/bin"));
481 }
482
483 #[test]
484 fn test_filter_removes_generic_unix() {
485 let mut files = vec![
486 PathBuf::from("/lib/libpython3.so"),
487 PathBuf::from("/lib/libpython3.9.so.1.0"),
488 ];
489 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
490 assert_eq!(files.len(), 1);
491 assert!(files[0].to_string_lossy().contains("libpython3.9.so.1.0"));
492 }
493
494 #[test]
495 fn test_filter_removes_generic_windows() {
496 let mut files = vec![
497 PathBuf::from("/bin/python3.dll"),
498 PathBuf::from("/bin/python3.10.dll"),
499 ];
500 filter_python_libraries_with_config(&mut files, &WINDOWS_CONFIG);
501 assert_eq!(files.len(), 1);
502 assert!(files[0].to_string_lossy().contains("python3.10.dll"));
503 }
504
505 #[test]
506 fn test_filter_prefers_versioned_so_unix() {
507 let mut files = vec![
508 PathBuf::from("/lib/libpython3.9.so"),
509 PathBuf::from("/lib/libpython3.9.so.1.0"),
510 ];
511 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
512 assert_eq!(files.len(), 1);
513 assert!(files[0].to_string_lossy().ends_with(".so.1.0"));
514 }
515
516 #[test]
517 fn test_filter_keeps_single_file() {
518 let mut files = vec![PathBuf::from("/lib/libpython3.9.so.1.0")];
519 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
520 assert_eq!(files.len(), 1);
521 }
522
523 #[test]
528 fn test_find_libpython_unix_structure() -> Result<()> {
529 let temp_dir = TempDir::new()?;
530 let lib_dir = temp_dir.path().join("sys").join("lib");
531 fs::create_dir_all(&lib_dir)?;
532 fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
533
534 let base_path = temp_dir.path();
535 let actual_lib_dir = get_lib_dir(base_path, &UNIX_CONFIG);
536 let (found_dir, found_path) =
537 find_libpython_in_dir_with_config(&actual_lib_dir, &UNIX_CONFIG)?;
538
539 assert_eq!(found_dir, lib_dir);
540 assert!(found_path.to_string_lossy().contains("libpython3.9.so.1.0"));
541 Ok(())
542 }
543
544 #[test]
545 fn test_find_libpython_windows_structure() -> Result<()> {
546 let temp_dir = TempDir::new()?;
547 let bin_dir = temp_dir.path().join("bin");
548 fs::create_dir_all(&bin_dir)?;
549 fs::write(bin_dir.join("python3.10.dll"), "")?;
550
551 let base_path = temp_dir.path();
552 let actual_lib_dir = get_lib_dir(base_path, &WINDOWS_CONFIG);
553 let (found_dir, found_path) =
554 find_libpython_in_dir_with_config(&actual_lib_dir, &WINDOWS_CONFIG)?;
555
556 assert_eq!(found_dir, bin_dir);
557 assert!(found_path.to_string_lossy().contains("python3.10.dll"));
558 Ok(())
559 }
560
561 #[test]
562 fn test_multiple_libraries_error_unix() -> Result<()> {
563 let temp_dir = TempDir::new()?;
564 let lib_dir = temp_dir.path();
565 fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
566 fs::write(lib_dir.join("libpython3.10.so.1.0"), "")?;
567
568 let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
569 assert!(err.to_string().contains("Multiple Python library files"));
570 Ok(())
571 }
572
573 #[test]
574 fn test_multiple_libraries_error_windows() -> Result<()> {
575 let temp_dir = TempDir::new()?;
576 let lib_dir = temp_dir.path();
577 fs::write(lib_dir.join("python3.9.dll"), "")?;
578 fs::write(lib_dir.join("python3.10.dll"), "")?;
579
580 let err = find_libpython_in_dir_with_config(lib_dir, &WINDOWS_CONFIG).unwrap_err();
581 assert!(err.to_string().contains("Multiple Python library files"));
582 Ok(())
583 }
584
585 #[test]
586 fn test_no_library_error() -> Result<()> {
587 let temp_dir = TempDir::new()?;
588 let lib_dir = temp_dir.path();
589 let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
592 assert!(err.to_string().contains("No Python library file found"));
593 Ok(())
594 }
595
596 #[test]
597 fn test_nonexistent_directory_error() {
598 let lib_dir = PathBuf::from("/nonexistent/path");
599 let err = find_libpython_in_dir_with_config(&lib_dir, &UNIX_CONFIG).unwrap_err();
600 assert!(err.to_string().contains("does not exist"));
601 }
602
603 fn create_mock_simics_structure(base_dir: &Path, python_version: &str) -> Result<()> {
608 let host_dir = base_dir.join(HOST_DIRNAME);
609 fs::create_dir_all(host_dir.join("bin"))?;
610 fs::create_dir_all(
611 host_dir
612 .join("include")
613 .join(format!("python{}", python_version)),
614 )?;
615
616 let mini_python = if cfg!(windows) {
618 "mini-python.exe"
619 } else {
620 "mini-python"
621 };
622 fs::write(host_dir.join("bin").join(mini_python), "")?;
623
624 #[cfg(unix)]
626 {
627 fs::create_dir_all(host_dir.join("sys").join("lib"))?;
628 fs::write(
629 host_dir
630 .join("sys")
631 .join("lib")
632 .join(format!("libpython{}.so", python_version)),
633 "",
634 )?;
635 }
636
637 #[cfg(windows)]
638 {
639 fs::write(
640 host_dir
641 .join("bin")
642 .join(format!("python{}.dll", python_version)),
643 "",
644 )?;
645 }
646
647 Ok(())
648 }
649
650 #[test]
651 fn test_discover_traditional_structure() -> Result<()> {
652 let temp_dir = TempDir::new()?;
653 let base_path = temp_dir.path();
654
655 create_mock_simics_structure(base_path, "3.9")?;
656
657 let env = discover_python_environment_from_base(base_path)?;
658
659 assert_eq!(env.package_source, PackageSource::Traditional);
660 assert_eq!(env.version.major, 3);
661 assert_eq!(env.version.minor, 9);
662 assert!(env.mini_python.exists());
663 assert!(env.include_dir.exists());
664 assert!(env.lib_path.exists());
665
666 Ok(())
667 }
668
669 #[test]
670 fn test_version_parsing() -> Result<()> {
671 let temp_dir = TempDir::new()?;
672 let include_dir = temp_dir.path().join("python3.9.10");
673 fs::create_dir_all(&include_dir)?;
674
675 let version = PythonVersion::parse_from_include_dir(&include_dir)?;
676 assert_eq!(version.major, 3);
677 assert_eq!(version.minor, 9);
678 assert_eq!(version.patch, 10);
679
680 Ok(())
681 }
682
683 #[test]
684 fn test_multiple_python_include_dirs_error() -> Result<()> {
685 let temp_dir = TempDir::new()?;
686 let include_dir = temp_dir.path().join("include");
687 fs::create_dir_all(include_dir.join("python3.9"))?;
688 fs::create_dir_all(include_dir.join("python3.10"))?;
689
690 let err = find_python_subdir(&include_dir).unwrap_err();
691 assert!(err.to_string().contains("Multiple python* subdirectories"));
692
693 Ok(())
694 }
695
696 #[test]
697 fn test_detect_simics_major_version_from_base() -> Result<()> {
698 let base = PathBuf::from("/opt/simics/simics-7.38.0");
699 let major = detect_simics_major_version_from_base(&base)?;
700 assert_eq!(major, 7);
701
702 Ok(())
703 }
704
705 #[test]
706 fn test_detect_simics_major_version_from_base_v6() -> Result<()> {
707 let base = PathBuf::from("/opt/simics/simics-6.0.191");
708 let major = detect_simics_major_version_from_base(&base)?;
709 assert_eq!(major, 6);
710
711 Ok(())
712 }
713
714 #[test]
715 fn test_detect_simics_major_version_from_base_invalid() {
716 let base = PathBuf::from("/opt/simics/current");
717 let err = detect_simics_major_version_from_base(&base).unwrap_err();
718 assert!(err
719 .to_string()
720 .contains("expected directory name like simics-x.x.x"));
721 }
722}