Skip to main content

simics_python_utils/
version.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::{anyhow, Result};
5use std::path::Path;
6
7/// Python version information parsed from include directory
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct PythonVersion {
10    /// Major version number (e.g., 3)
11    pub major: u32,
12    /// Minor version number (e.g., 9)
13    pub minor: u32,
14    /// Patch version number (e.g., 10)
15    pub patch: u32,
16}
17
18impl PythonVersion {
19    /// Create a new Python version
20    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
21        Self {
22            major,
23            minor,
24            patch,
25        }
26    }
27
28    /// Generate Py_LIMITED_API hex value for this Python version
29    ///
30    /// Format: 0xMMmmpppp where MM=major, mm=minor, pppp=patch*1000
31    /// Example: Python 3.9.10 -> 0x03090000 (patch is truncated to 0 for API compatibility)
32    pub fn py_limited_api_hex(&self) -> String {
33        format!("0x{:02x}{:02x}0000", self.major, self.minor)
34    }
35
36    /// Parse Python version from include directory name
37    ///
38    /// Expects directory names like "python3.9" or "python3.9.10"
39    pub fn parse_from_include_dir<P: AsRef<Path>>(include_dir: P) -> Result<Self> {
40        let dir_name = include_dir
41            .as_ref()
42            .file_name()
43            .ok_or_else(|| anyhow!("Failed to get include directory filename"))?
44            .to_str()
45            .ok_or_else(|| anyhow!("Failed to convert include directory name to string"))?;
46
47        // Remove "python" prefix and parse version components
48        let version_str = dir_name.strip_prefix("python").ok_or_else(|| {
49            anyhow!(
50                "Include directory name does not start with 'python': {}",
51                dir_name
52            )
53        })?;
54
55        let components: Result<Vec<u32>> = version_str
56            .split('.')
57            .map(|s| {
58                s.parse::<u32>()
59                    .map_err(|e| anyhow!("Failed to parse version component '{}': {}", s, e))
60            })
61            .collect();
62
63        let components = components?;
64
65        match components.len() {
66            2 => Ok(Self::new(components[0], components[1], 0)),
67            3 => Ok(Self::new(components[0], components[1], components[2])),
68            _ => Err(anyhow!(
69                "Invalid Python version format '{}', expected 'X.Y' or 'X.Y.Z'",
70                version_str
71            )),
72        }
73    }
74}
75
76impl std::fmt::Display for PythonVersion {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        if self.patch == 0 {
79            write!(f, "{}.{}", self.major, self.minor)
80        } else {
81            write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::path::PathBuf;
90
91    #[test]
92    fn test_py_limited_api_hex() {
93        let version = PythonVersion::new(3, 9, 10);
94        assert_eq!(version.py_limited_api_hex(), "0x03090000");
95
96        let version = PythonVersion::new(3, 8, 0);
97        assert_eq!(version.py_limited_api_hex(), "0x03080000");
98    }
99
100    #[test]
101    fn test_parse_from_include_dir() {
102        // Test 2-component version
103        let path = PathBuf::from("/some/path/python3.9");
104        let version = PythonVersion::parse_from_include_dir(&path).unwrap();
105        assert_eq!(version, PythonVersion::new(3, 9, 0));
106
107        // Test 3-component version
108        let path = PathBuf::from("/some/path/python3.9.10");
109        let version = PythonVersion::parse_from_include_dir(&path).unwrap();
110        assert_eq!(version, PythonVersion::new(3, 9, 10));
111    }
112
113    #[test]
114    fn test_display() {
115        let version = PythonVersion::new(3, 9, 0);
116        assert_eq!(version.to_string(), "3.9");
117
118        let version = PythonVersion::new(3, 9, 10);
119        assert_eq!(version.to_string(), "3.9.10");
120    }
121}