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_has_version_subdir: bool,
43 lib_prefix: &'static str,
45 lib_extension: &'static str,
47 generic_lib_name: &'static str,
49}
50
51#[cfg(unix)]
52const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
53 lib_subdir: &["sys", "lib"],
54 lib_has_version_subdir: false,
55 lib_prefix: "libpython",
56 lib_extension: ".so",
57 generic_lib_name: "libpython3.so",
58};
59
60#[cfg(windows)]
61const PLATFORM_CONFIG: PlatformConfig = PlatformConfig {
62 lib_subdir: &["lib"],
63 lib_has_version_subdir: true,
64 lib_prefix: "python3",
65 lib_extension: ".dll",
66 generic_lib_name: "python3.dll",
67};
68
69fn is_python_library_name(name: &str, config: &PlatformConfig) -> bool {
75 name.starts_with(config.lib_prefix) && name.contains(config.lib_extension)
76}
77
78fn filter_python_libraries_with_config(files: &mut Vec<PathBuf>, config: &PlatformConfig) {
80 if files.len() > 1 {
82 files.retain(|path| {
83 path.file_name()
84 .and_then(|name| name.to_str())
85 .map(|name| name != config.generic_lib_name)
86 .unwrap_or(false)
87 });
88 }
89
90 if config.lib_extension == ".so" && files.len() > 1 {
92 files.retain(|path| {
93 path.file_name()
94 .and_then(|name| name.to_str())
95 .map(|name| !name.ends_with(".so"))
96 .unwrap_or(false)
97 });
98 }
99}
100
101fn get_lib_dir(base_path: &Path, config: &PlatformConfig) -> Result<PathBuf> {
103 let base = config
104 .lib_subdir
105 .iter()
106 .fold(base_path.to_path_buf(), |p, component| p.join(component));
107 if config.lib_has_version_subdir {
108 find_python_subdir(&base)
109 } else {
110 Ok(base)
111 }
112}
113
114fn find_libpython_in_dir_with_config(
116 lib_dir: &Path,
117 config: &PlatformConfig,
118) -> Result<(PathBuf, PathBuf)> {
119 if !lib_dir.exists() {
120 return Err(anyhow!(
121 "Library directory does not exist: {}",
122 lib_dir.display()
123 ));
124 }
125
126 let entries = read_dir(lib_dir).map_err(|e| {
127 anyhow!(
128 "Failed to read library directory {}: {}",
129 lib_dir.display(),
130 e
131 )
132 })?;
133
134 let mut libpython_files: Vec<PathBuf> = entries
135 .filter_map(|entry| entry.ok())
136 .map(|entry| entry.path())
137 .filter(|path| path.is_file())
138 .filter(|path| {
139 path.file_name()
140 .and_then(|name| name.to_str())
141 .map(|name| is_python_library_name(name, config))
142 .unwrap_or(false)
143 })
144 .collect();
145
146 filter_python_libraries_with_config(&mut libpython_files, config);
147
148 match libpython_files.len() {
149 0 => Err(anyhow!(
150 "No Python library file found in {}",
151 lib_dir.display()
152 )),
153 1 => {
154 let lib_path = libpython_files
155 .into_iter()
156 .next()
157 .expect("exactly one element guaranteed by match arm");
158 Ok((lib_dir.to_path_buf(), lib_path))
159 }
160 _ => Err(anyhow!(
161 "Multiple Python library files found in {}, expected exactly one",
162 lib_dir.display()
163 )),
164 }
165}
166
167fn find_latest_python_package(simics_major_version: u32) -> Result<InstalledPackage> {
173 let packages = ispm::packages::list(&GlobalOptions::default())
174 .map_err(|e| anyhow!("Failed to query ISPM for installed packages: {}", e))?;
175
176 let installed = packages
177 .installed_packages
178 .as_ref()
179 .ok_or_else(|| anyhow!("No installed packages found"))?;
180
181 let python_packages: Vec<_> = installed
183 .iter()
184 .filter(|pkg| pkg.package_number == 1033)
185 .filter(|pkg| pkg.name == "Python")
186 .filter(|pkg| {
187 pkg.version
189 .starts_with(&format!("{}.", simics_major_version))
190 })
191 .collect();
192
193 if python_packages.is_empty() {
194 return Err(anyhow!(
195 "No Python packages found for Simics major version {}",
196 simics_major_version
197 ));
198 }
199
200 let latest_package = python_packages
202 .iter()
203 .max_by(|a, b| {
204 let version_a = Versioning::new(&a.version).unwrap_or_default();
205 let version_b = Versioning::new(&b.version).unwrap_or_default();
206 version_a.cmp(&version_b)
207 })
208 .ok_or_else(|| anyhow!("Failed to find latest Python package"))?;
209
210 Ok((*latest_package).clone())
211}
212
213fn detect_simics_major_version_from_base(simics_base: &Path) -> Result<u32> {
215 if let Some(dir_name) = simics_base.file_name().and_then(|n| n.to_str()) {
216 if let Some(version_part) = dir_name.strip_prefix("simics-") {
218 if let Some(major_str) = version_part.split('.').next() {
219 if let Ok(major) = major_str.parse::<u32>() {
220 return Ok(major);
221 }
222 }
223 }
224 }
225
226 Err(anyhow!(
227 "Unable to determine Simics major version from SIMICS base path {}; expected directory name like simics-x.x.x",
228 simics_base.display()
229 ))
230}
231
232pub fn discover_python_environment() -> Result<PythonEnvironment> {
238 let simics_base =
239 env::var("SIMICS_BASE").map_err(|_| anyhow!("SIMICS_BASE environment variable not set"))?;
240
241 discover_python_environment_from_base(simics_base)
242}
243
244pub fn discover_python_environment_from_base<P: AsRef<Path>>(
246 simics_base: P,
247) -> Result<PythonEnvironment> {
248 let simics_base = simics_base.as_ref();
249
250 let bundled_err = match try_bundled_paths(simics_base) {
252 Ok(env) => return Ok(env.with_source(PackageSource::Bundled)),
253 Err(err) => err.context("Bundled discovery failed"),
254 };
255
256 let separate_err = match try_separate_python_package_discovery(simics_base) {
258 Ok(env) => return Ok(env.with_source(PackageSource::SeparatePackage)),
259 Err(err) => err.context("Separate package discovery failed"),
260 };
261
262 Err(anyhow!(
263 "Python environment not found in bundled location ({}) or through separate package discovery\nBundled error: {:#}\nSeparate error: {:#}",
264 simics_base.join(HOST_DIRNAME).display(),
265 bundled_err,
266 separate_err
267 ))
268}
269
270fn try_bundled_paths(simics_base: &Path) -> Result<PythonEnvironment> {
272 let base_path = simics_base.join(HOST_DIRNAME);
273 discover_from_base_path(base_path)
274}
275
276fn try_separate_python_package_discovery(simics_base: &Path) -> Result<PythonEnvironment> {
278 let major_version = detect_simics_major_version_from_base(simics_base)?;
280
281 let python_package = find_latest_python_package(major_version)?;
283
284 println!(
285 "cargo:warning=Using separate Python package: {} (version {})",
286 python_package.name, python_package.version
287 );
288
289 let package_path = python_package.paths.first().ok_or_else(|| {
291 anyhow!(
292 "No installation paths found for Python package {}",
293 python_package.name
294 )
295 })?;
296
297 let python_package_path = package_path.join(HOST_DIRNAME);
299
300 discover_from_base_path(python_package_path)
301}
302
303fn discover_from_base_path(base_path: PathBuf) -> Result<PythonEnvironment> {
305 let mini_python = find_mini_python(&base_path)?;
307 let include_dir = find_python_include(&base_path)?;
309 let (lib_dir, lib_path) = find_python_library(&base_path)?;
312 let import_lib_dir = find_import_lib_dir(&base_path);
314 let version = PythonVersion::parse_from_include_dir(&include_dir)?;
315
316 let env = PythonEnvironment::new(
317 mini_python,
318 include_dir,
319 lib_dir,
320 lib_path,
321 import_lib_dir,
322 version,
323 PackageSource::Bundled, );
325
326 env.validate()?;
328
329 Ok(env)
330}
331
332fn find_mini_python(base_path: &Path) -> Result<PathBuf> {
334 #[cfg(unix)]
335 let executable_name = "mini-python";
336
337 #[cfg(windows)]
338 let executable_name = "mini-python.exe";
339
340 let mini_python_path = base_path.join("bin").join(executable_name);
341
342 if mini_python_path.exists() {
343 Ok(mini_python_path)
344 } else {
345 Err(anyhow!(
346 "Mini-python executable not found at {}",
347 mini_python_path.display()
348 ))
349 }
350}
351
352fn find_python_include(base_path: &Path) -> Result<PathBuf> {
354 if let Ok(include_env) = env::var(PYTHON3_INCLUDE_ENV) {
356 let include_path = if let Some(path) = include_env.strip_prefix("-I") {
358 PathBuf::from(path)
359 } else {
360 PathBuf::from(include_env)
361 };
362
363 if include_path.exists() {
364 return Ok(include_path);
365 }
366 }
367
368 let include_base = base_path.join("include");
369 find_python_subdir(&include_base)
370}
371
372fn find_python_subdir(include_dir: &Path) -> Result<PathBuf> {
374 if !include_dir.exists() {
375 return Err(anyhow!(
376 "Include directory does not exist: {}",
377 include_dir.display()
378 ));
379 }
380
381 let entries = read_dir(include_dir).map_err(|e| {
382 anyhow!(
383 "Failed to read include directory {}: {}",
384 include_dir.display(),
385 e
386 )
387 })?;
388
389 let python_dirs: Vec<PathBuf> = entries
390 .filter_map(|entry| entry.ok())
391 .map(|entry| entry.path())
392 .filter(|path| path.is_dir())
393 .filter(|path| {
394 path.file_name()
395 .and_then(|name| name.to_str())
396 .map(|name| name.starts_with("python3."))
397 .unwrap_or(false)
398 })
399 .collect();
400
401 match python_dirs.len() {
402 0 => Err(anyhow!(
403 "No python3.* subdirectory found in {}",
404 include_dir.display()
405 )),
406 1 => Ok(python_dirs
407 .into_iter()
408 .next()
409 .expect("exactly one element guaranteed by match arm")),
410 _ => Err(anyhow!(
411 "Multiple python3.* subdirectories found in {}, expected exactly one",
412 include_dir.display()
413 )),
414 }
415}
416
417fn find_import_lib_dir(base_path: &Path) -> PathBuf {
422 let py3_dir = base_path.join("bin").join("py3");
423 if py3_dir.join("python3.lib").exists() {
424 return py3_dir;
425 }
426 base_path.join("bin")
428}
429
430fn find_python_library(base_path: &Path) -> Result<(PathBuf, PathBuf)> {
432 if let Ok(lib_env) = env::var(PYTHON3_LDFLAGS_ENV) {
434 let lib_path = PathBuf::from(lib_env);
435 if lib_path.exists() {
436 let lib_dir = lib_path
437 .parent()
438 .ok_or_else(|| anyhow!("Library path has no parent directory"))?
439 .to_path_buf();
440 return Ok((lib_dir, lib_path));
441 }
442 }
443
444 let lib_dir = get_lib_dir(base_path, &PLATFORM_CONFIG)?;
445 find_libpython_in_dir_with_config(&lib_dir, &PLATFORM_CONFIG)
446}
447
448#[cfg(test)]
453mod tests {
454 use super::*;
455 use std::fs;
456 use tempfile::TempDir;
457
458 const UNIX_CONFIG: PlatformConfig = PlatformConfig {
460 lib_subdir: &["sys", "lib"],
461 lib_has_version_subdir: false,
462 lib_prefix: "libpython",
463 lib_extension: ".so",
464 generic_lib_name: "libpython3.so",
465 };
466
467 const WINDOWS_CONFIG: PlatformConfig = PlatformConfig {
468 lib_subdir: &["lib"],
469 lib_has_version_subdir: true,
470 lib_prefix: "python3",
471 lib_extension: ".dll",
472 generic_lib_name: "python3.dll",
473 };
474
475 #[test]
480 fn test_is_python_library_name_unix() {
481 assert!(is_python_library_name("libpython3.9.so.1.0", &UNIX_CONFIG));
482 assert!(is_python_library_name("libpython3.so", &UNIX_CONFIG));
483 assert!(is_python_library_name("libpython3.9.so", &UNIX_CONFIG));
484 assert!(!is_python_library_name("python3.9.dll", &UNIX_CONFIG));
485 assert!(!is_python_library_name("libfoo.so", &UNIX_CONFIG));
486 }
487
488 #[test]
489 fn test_is_python_library_name_windows() {
490 assert!(is_python_library_name("python3.10.dll", &WINDOWS_CONFIG));
491 assert!(is_python_library_name("python3.dll", &WINDOWS_CONFIG));
492 assert!(!is_python_library_name("libpython3.9.so", &WINDOWS_CONFIG));
493 assert!(!is_python_library_name("python3.10.lib", &WINDOWS_CONFIG));
494 assert!(!is_python_library_name("python2.7.dll", &WINDOWS_CONFIG));
495 }
496
497 #[test]
498 fn test_get_lib_dir_unix() -> Result<()> {
499 let base = PathBuf::from("/opt/simics/linux64");
500 let lib_dir = get_lib_dir(&base, &UNIX_CONFIG)?;
501 assert_eq!(lib_dir, PathBuf::from("/opt/simics/linux64/sys/lib"));
502 Ok(())
503 }
504
505 #[test]
506 fn test_get_lib_dir_windows() -> Result<()> {
507 let temp_dir = TempDir::new()?;
508 let lib_dir = temp_dir.path().join("lib").join("python3.10");
509 fs::create_dir_all(&lib_dir)?;
510
511 let result = get_lib_dir(temp_dir.path(), &WINDOWS_CONFIG)?;
512 assert_eq!(result, lib_dir);
513 Ok(())
514 }
515
516 #[test]
517 fn test_filter_removes_generic_unix() {
518 let mut files = vec![
519 PathBuf::from("/lib/libpython3.so"),
520 PathBuf::from("/lib/libpython3.9.so.1.0"),
521 ];
522 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
523 assert_eq!(files.len(), 1);
524 assert!(files[0].to_string_lossy().contains("libpython3.9.so.1.0"));
525 }
526
527 #[test]
528 fn test_filter_removes_generic_windows() {
529 let mut files = vec![
530 PathBuf::from("/bin/python3.dll"),
531 PathBuf::from("/bin/python3.10.dll"),
532 ];
533 filter_python_libraries_with_config(&mut files, &WINDOWS_CONFIG);
534 assert_eq!(files.len(), 1);
535 assert!(files[0].to_string_lossy().contains("python3.10.dll"));
536 }
537
538 #[test]
539 fn test_filter_prefers_versioned_so_unix() {
540 let mut files = vec![
541 PathBuf::from("/lib/libpython3.9.so"),
542 PathBuf::from("/lib/libpython3.9.so.1.0"),
543 ];
544 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
545 assert_eq!(files.len(), 1);
546 assert!(files[0].to_string_lossy().ends_with(".so.1.0"));
547 }
548
549 #[test]
550 fn test_filter_keeps_single_file() {
551 let mut files = vec![PathBuf::from("/lib/libpython3.9.so.1.0")];
552 filter_python_libraries_with_config(&mut files, &UNIX_CONFIG);
553 assert_eq!(files.len(), 1);
554 }
555
556 #[test]
561 fn test_find_libpython_unix_structure() -> Result<()> {
562 let temp_dir = TempDir::new()?;
563 let lib_dir = temp_dir.path().join("sys").join("lib");
564 fs::create_dir_all(&lib_dir)?;
565 fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
566
567 let base_path = temp_dir.path();
568 let actual_lib_dir = get_lib_dir(base_path, &UNIX_CONFIG)?;
569 let (found_dir, found_path) =
570 find_libpython_in_dir_with_config(&actual_lib_dir, &UNIX_CONFIG)?;
571
572 assert_eq!(found_dir, lib_dir);
573 assert!(found_path.to_string_lossy().contains("libpython3.9.so.1.0"));
574 Ok(())
575 }
576
577 #[test]
578 fn test_find_libpython_windows_structure() -> Result<()> {
579 let temp_dir = TempDir::new()?;
580 let bin_dir = temp_dir.path().join("lib").join("python3.10");
581 fs::create_dir_all(&bin_dir)?;
582 fs::write(bin_dir.join("python3.dll"), "")?;
583
584 let base_path = temp_dir.path();
585 let actual_lib_dir = get_lib_dir(base_path, &WINDOWS_CONFIG)?;
586 let (found_dir, found_path) =
587 find_libpython_in_dir_with_config(&actual_lib_dir, &WINDOWS_CONFIG)?;
588
589 assert_eq!(found_dir, bin_dir);
590 assert!(found_path.to_string_lossy().contains("python3.dll"));
591 Ok(())
592 }
593
594 #[test]
595 fn test_multiple_libraries_error_unix() -> Result<()> {
596 let temp_dir = TempDir::new()?;
597 let lib_dir = temp_dir.path();
598 fs::write(lib_dir.join("libpython3.9.so.1.0"), "")?;
599 fs::write(lib_dir.join("libpython3.10.so.1.0"), "")?;
600
601 let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
602 assert!(err.to_string().contains("Multiple Python library files"));
603 Ok(())
604 }
605
606 #[test]
607 fn test_multiple_libraries_error_windows() -> Result<()> {
608 let temp_dir = TempDir::new()?;
609 let lib_dir = temp_dir.path();
610 fs::write(lib_dir.join("python3.9.dll"), "")?;
611 fs::write(lib_dir.join("python3.10.dll"), "")?;
612
613 let err = find_libpython_in_dir_with_config(lib_dir, &WINDOWS_CONFIG).unwrap_err();
614 assert!(err.to_string().contains("Multiple Python library files"));
615 Ok(())
616 }
617
618 #[test]
619 fn test_no_library_error() -> Result<()> {
620 let temp_dir = TempDir::new()?;
621 let lib_dir = temp_dir.path();
622 let err = find_libpython_in_dir_with_config(lib_dir, &UNIX_CONFIG).unwrap_err();
625 assert!(err.to_string().contains("No Python library file found"));
626 Ok(())
627 }
628
629 #[test]
630 fn test_nonexistent_directory_error() {
631 let lib_dir = PathBuf::from("/nonexistent/path");
632 let err = find_libpython_in_dir_with_config(&lib_dir, &UNIX_CONFIG).unwrap_err();
633 assert!(err.to_string().contains("does not exist"));
634 }
635
636 fn create_mock_simics_structure(base_dir: &Path, python_version: &str) -> Result<()> {
641 let host_dir = base_dir.join(HOST_DIRNAME);
642 fs::create_dir_all(host_dir.join("bin"))?;
643 fs::create_dir_all(
644 host_dir
645 .join("include")
646 .join(format!("python{}", python_version)),
647 )?;
648
649 let mini_python = if cfg!(windows) {
651 "mini-python.exe"
652 } else {
653 "mini-python"
654 };
655 fs::write(host_dir.join("bin").join(mini_python), "")?;
656
657 #[cfg(unix)]
659 {
660 fs::create_dir_all(host_dir.join("sys").join("lib"))?;
661 fs::write(
662 host_dir
663 .join("sys")
664 .join("lib")
665 .join(format!("libpython{}.so", python_version)),
666 "",
667 )?;
668 }
669
670 #[cfg(windows)]
671 {
672 fs::write(
673 host_dir
674 .join("bin")
675 .join(format!("python{}.dll", python_version)),
676 "",
677 )?;
678 }
679
680 Ok(())
681 }
682
683 #[test]
684 fn test_discover_traditional_structure() -> Result<()> {
685 let temp_dir = TempDir::new()?;
686 let base_path = temp_dir.path();
687
688 create_mock_simics_structure(base_path, "3.9")?;
689
690 let env = discover_python_environment_from_base(base_path)?;
691
692 assert_eq!(env.package_source, PackageSource::Bundled);
693 assert_eq!(env.version.major, 3);
694 assert_eq!(env.version.minor, 9);
695 assert!(env.mini_python.exists());
696 assert!(env.include_dir.exists());
697 assert!(env.lib_path.exists());
698
699 Ok(())
700 }
701
702 #[test]
703 fn test_version_parsing() -> Result<()> {
704 let temp_dir = TempDir::new()?;
705 let include_dir = temp_dir.path().join("python3.9.10");
706 fs::create_dir_all(&include_dir)?;
707
708 let version = PythonVersion::parse_from_include_dir(&include_dir)?;
709 assert_eq!(version.major, 3);
710 assert_eq!(version.minor, 9);
711 assert_eq!(version.patch, 10);
712
713 Ok(())
714 }
715
716 #[test]
717 fn test_multiple_python_include_dirs_error() -> Result<()> {
718 let temp_dir = TempDir::new()?;
719 let include_dir = temp_dir.path().join("include");
720 fs::create_dir_all(include_dir.join("python3.9"))?;
721 fs::create_dir_all(include_dir.join("python3.10"))?;
722
723 let err = find_python_subdir(&include_dir).unwrap_err();
724 assert!(err
725 .to_string()
726 .contains("Multiple python3.* subdirectories"));
727
728 Ok(())
729 }
730
731 #[test]
732 fn test_detect_simics_major_version_from_base() -> Result<()> {
733 let base = PathBuf::from("/opt/simics/simics-7.38.0");
734 let major = detect_simics_major_version_from_base(&base)?;
735 assert_eq!(major, 7);
736
737 Ok(())
738 }
739
740 #[test]
741 fn test_detect_simics_major_version_from_base_v6() -> Result<()> {
742 let base = PathBuf::from("/opt/simics/simics-6.0.191");
743 let major = detect_simics_major_version_from_base(&base)?;
744 assert_eq!(major, 6);
745
746 Ok(())
747 }
748
749 #[test]
750 fn test_detect_simics_major_version_from_base_invalid() {
751 let base = PathBuf::from("/opt/simics/current");
752 let err = detect_simics_major_version_from_base(&base).unwrap_err();
753 assert!(err
754 .to_string()
755 .contains("expected directory name like simics-x.x.x"));
756 }
757}