Line | Count | Source |
1 | | // SPDX-License-Identifier: Apache-2.0 |
2 | | //! Intel `oneAPI` detection. |
3 | | //! |
4 | | //! The Intel counterpart of [`crate::rocm_host`], and the same kind of probe: a |
5 | | //! userspace install, not a driver. Intel exposes no single release marker the |
6 | | //! way `ROCm` writes `.info/version`, so the release is taken from the |
7 | | //! component layout the toolkit installs: |
8 | | //! |
9 | | //! ```text |
10 | | //! /opt/intel/oneapi/compiler/latest -> 2024.2 |
11 | | //! ``` |
12 | | //! |
13 | | //! `ONEAPI_ROOT` — which `setvars.sh` exports — is honoured first, so a |
14 | | //! non-default or side-by-side install is picked over `/opt/intel/oneapi`. |
15 | | //! |
16 | | //! What this does **not** cover: the Level Zero loader and Intel's |
17 | | //! compute-runtime, which distros package into `/usr/lib` with no version file. |
18 | | //! Reading those means `dlopen` plus `zeInit`/`zeDriverGetProperties`, which is |
19 | | //! a linked runtime call rather than a file read, so it stays out of scope |
20 | | //! here. A host running GPU compute through a distro-packaged Level Zero with |
21 | | //! no toolkit installed therefore reports `None`. |
22 | | //! |
23 | | //! **Unverified against a real install.** Written without Intel hardware or a |
24 | | //! `oneAPI` install to test against; the parsing is unit-tested and the layout |
25 | | //! above is the part to confirm first. |
26 | | |
27 | | use crate::{OneApiHost, OneApiVersion}; |
28 | | use std::path::{Path, PathBuf}; |
29 | | |
30 | | /// Conventional install prefix, used when `ONEAPI_ROOT` is unset. |
31 | | const DEFAULT_ROOT: &str = "/opt/intel/oneapi"; |
32 | | |
33 | | /// Component directories whose release the toolkit version is read from, in |
34 | | /// preference order. `compiler` leads because it is the component a GPU build |
35 | | /// actually needs; the rest are fallbacks for partial installs. |
36 | | const COMPONENTS: &[&str] = &["compiler", "mkl", "tbb", "dpl", "ccl"]; |
37 | | |
38 | | /// Parse a version directory name (`2024.2`, `2024.2.1`). |
39 | 7 | fn parse_version(name: &str) -> Option<OneApiVersion> { |
40 | 7 | let (major3 , minor3 , patch3 ) = crate::parse_dotted_version(name)?4 ; |
41 | 3 | Some(OneApiVersion { |
42 | 3 | major, |
43 | 3 | minor, |
44 | 3 | patch, |
45 | 3 | }) |
46 | 7 | } |
47 | | |
48 | | /// Release installed for one component: the `latest` symlink's target, falling |
49 | | /// back to the newest versioned directory when no such symlink exists. |
50 | 26 | fn component_version(component: &Path) -> Option<OneApiVersion> { |
51 | 26 | let from_symlink = std::fs::read_link(component.join("latest")) |
52 | 26 | .ok() |
53 | 26 | .and_then(|target| parse_version(&target.file_name()0 ?0 .to_string_lossy0 ())); |
54 | 26 | if let Some(version0 ) = from_symlink { |
55 | 0 | return Some(version); |
56 | 26 | } |
57 | | // No `latest`: take the highest version present rather than directory |
58 | | // order, which is arbitrary. |
59 | 26 | let mut versions0 : Vec<OneApiVersion>0 = std::fs::read_dir(component) |
60 | 26 | .ok()? |
61 | 0 | .flatten() |
62 | 0 | .filter_map(|entry| parse_version(&entry.file_name().to_string_lossy())) |
63 | 0 | .collect(); |
64 | 0 | versions.sort_unstable(); |
65 | 0 | versions.pop() |
66 | 26 | } |
67 | | |
68 | | /// Read the toolkit release from one install root, if it holds one. |
69 | 5 | fn version_at(root: &Path) -> Option<OneApiVersion> { |
70 | 5 | COMPONENTS |
71 | 5 | .iter() |
72 | 25 | .find_map5 (|component| component_version(&root.join(component))) |
73 | 5 | } |
74 | | |
75 | | /// Install roots to try, most specific first. |
76 | 5 | fn roots() -> Vec<PathBuf> { |
77 | 5 | let mut roots = Vec::new(); |
78 | | // `setvars.sh` exports this; an explicit value selects a side-by-side |
79 | | // install, so preferring the default prefix would report the wrong release. |
80 | 5 | if let Some(path0 ) = std::env::var_os("ONEAPI_ROOT") { |
81 | 0 | roots.push(PathBuf::from(path)); |
82 | 5 | } |
83 | 5 | roots.push(PathBuf::from(DEFAULT_ROOT)); |
84 | 5 | roots |
85 | 5 | } |
86 | | |
87 | 4 | pub(crate) fn host() -> Option<OneApiHost> { |
88 | 4 | let version0 = roots().iter().find_map(|root| version_at(root))?; |
89 | 0 | Some(OneApiHost { version }) |
90 | 4 | } |
91 | | |
92 | | #[cfg(test)] |
93 | | mod tests { |
94 | | use super::*; |
95 | | |
96 | | #[test] |
97 | 1 | fn parses_release_directory_names() { |
98 | 1 | assert_eq!( |
99 | 1 | parse_version("2024.2"), |
100 | 1 | Some(OneApiVersion::new(2024, 2, 0)) |
101 | | ); |
102 | 1 | assert_eq!( |
103 | 1 | parse_version("2024.2.1"), |
104 | 1 | Some(OneApiVersion::new(2024, 2, 1)) |
105 | | ); |
106 | 1 | assert_eq!( |
107 | 1 | parse_version("2025.0"), |
108 | 1 | Some(OneApiVersion::new(2025, 0, 0)) |
109 | | ); |
110 | 1 | } |
111 | | |
112 | | #[test] |
113 | 1 | fn rejects_non_version_directories() { |
114 | | // `latest` itself, and the component subdirectories beside it. |
115 | 1 | assert_eq!(parse_version("latest"), None); |
116 | 1 | assert_eq!(parse_version("bin"), None); |
117 | 1 | assert_eq!(parse_version("2024"), None, "a bare year is not a version"); |
118 | 1 | assert_eq!(parse_version(""), None); |
119 | 1 | } |
120 | | |
121 | | #[test] |
122 | 1 | fn versions_order_by_release_year_first() { |
123 | 1 | assert!(OneApiVersion::new(2025, 0, 0) > OneApiVersion::new(2024, 2, 1)); |
124 | 1 | assert!(OneApiVersion::new(2024, 2, 1) > OneApiVersion::new(2024, 2, 0)); |
125 | 1 | assert!(OneApiVersion::new(2024, 10, 0) > OneApiVersion::new(2024, 9, 0)); |
126 | 1 | } |
127 | | |
128 | | #[test] |
129 | 1 | fn missing_install_root_reports_nothing() { |
130 | 1 | assert_eq!(version_at(Path::new("/nonexistent-oneapi-root")), None); |
131 | 1 | assert_eq!(component_version(Path::new("/nonexistent-component")), None); |
132 | 1 | } |
133 | | |
134 | | #[test] |
135 | 1 | fn compiler_is_the_preferred_component() { |
136 | 1 | assert_eq!( |
137 | 1 | COMPONENTS.first(), |
138 | | Some(&"compiler"), |
139 | | "a GPU build needs the compiler component, so it names the release", |
140 | | ); |
141 | 1 | } |
142 | | |
143 | | #[test] |
144 | 1 | fn oneapi_root_is_searched_before_the_default() { |
145 | 1 | let roots = roots(); |
146 | 1 | assert_eq!( |
147 | 1 | roots.last().map(PathBuf::as_path), |
148 | 1 | Some(Path::new(DEFAULT_ROOT)), |
149 | | "the conventional prefix is always the fallback", |
150 | | ); |
151 | 1 | if std::env::var_os("ONEAPI_ROOT").is_some() { |
152 | 0 | assert_eq!(roots.len(), 2, "an explicit ONEAPI_ROOT is tried first"); |
153 | 1 | } |
154 | 1 | } |
155 | | |
156 | | #[test] |
157 | 1 | fn host_lookup_never_panics() { |
158 | | // Environment-dependent: most hosts have no oneAPI at all. |
159 | 1 | if let Some(oneapi0 ) = host() { |
160 | 0 | assert!(oneapi.version.major > 0); |
161 | 1 | } |
162 | 1 | } |
163 | | } |