intel_mkl_tool/
entry.rs

1use crate::{Config, DataModel, LinkType, Threading};
2use anyhow::{bail, Context, Result};
3use std::{
4    fs,
5    io::{self, BufRead},
6    path::{Path, PathBuf},
7    process::Command,
8};
9
10/// MKL Libraries to be linked explicitly,
11/// not include OpenMP runtime (iomp5)
12pub fn mkl_libs(cfg: Config) -> Vec<String> {
13    let mut libs = Vec::new();
14    match cfg.index_size {
15        DataModel::LP64 => {
16            libs.push("mkl_intel_lp64".into());
17        }
18        DataModel::ILP64 => {
19            libs.push("mkl_intel_ilp64".into());
20        }
21    };
22    match cfg.parallel {
23        Threading::OpenMP => {
24            libs.push("mkl_intel_thread".into());
25        }
26        Threading::Sequential => {
27            libs.push("mkl_sequential".into());
28        }
29    };
30    libs.push("mkl_core".into());
31
32    if cfg!(target_os = "windows") && cfg.link == LinkType::Dynamic {
33        libs.into_iter().map(|lib| format!("{}_dll", lib)).collect()
34    } else {
35        libs
36    }
37}
38
39/// MKL Libraries to be loaded dynamically
40pub fn mkl_dyn_libs(cfg: Config) -> Vec<String> {
41    match cfg.link {
42        LinkType::Static => Vec::new(),
43        LinkType::Dynamic => {
44            let mut libs = Vec::new();
45            for prefix in &["mkl", "mkl_vml"] {
46                for suffix in &["def", "avx", "avx2", "avx512", "avx512_mic", "mc", "mc3"] {
47                    libs.push(format!("{}_{}", prefix, suffix));
48                }
49            }
50            libs.push("mkl_rt".into());
51            libs.push("mkl_vml_mc2".into());
52            libs.push("mkl_vml_cmpt".into());
53
54            if cfg!(target_os = "windows") {
55                libs.into_iter().map(|lib| format!("{}_dll", lib)).collect()
56            } else {
57                libs
58            }
59        }
60    }
61}
62
63/// Filename convention for MKL libraries.
64pub fn mkl_file_name(link: LinkType, name: &str) -> String {
65    if cfg!(target_os = "windows") {
66        // On windows
67        //
68        // - Static:  mkl_core.lib
69        // - Dynamic: mkl_core_dll.lib
70        //
71        // and `_dll` suffix is added in [mkl_libs] and [mkl_dyn_libs]
72        format!("{}.lib", name)
73    } else {
74        match link {
75            LinkType::Static => {
76                format!("lib{}.a", name)
77            }
78            LinkType::Dynamic => {
79                format!("lib{}.{}", name, std::env::consts::DLL_EXTENSION)
80            }
81        }
82    }
83}
84
85pub const OPENMP_RUNTIME_LIB: &str = if cfg!(target_os = "windows") {
86    "libiomp5md"
87} else {
88    "iomp5"
89};
90
91/// Filename convention for OpenMP runtime.
92pub fn openmp_runtime_file_name(link: LinkType) -> String {
93    let name = OPENMP_RUNTIME_LIB;
94    if cfg!(target_os = "windows") {
95        match link {
96            LinkType::Static => {
97                format!("{}.lib", name)
98            }
99            LinkType::Dynamic => {
100                format!("{}.dll", name)
101            }
102        }
103    } else {
104        match link {
105            LinkType::Static => {
106                format!("lib{}.a", name)
107            }
108            LinkType::Dynamic => {
109                format!("lib{}.{}", name, std::env::consts::DLL_EXTENSION)
110            }
111        }
112    }
113}
114
115/// Lacked definition of [std::env::consts]
116pub const STATIC_EXTENSION: &str = if cfg!(any(target_os = "linux", target_os = "macos")) {
117    "a"
118} else {
119    "lib"
120};
121
122/// Found MKL library
123///
124/// ```no_run
125/// use std::str::FromStr;
126/// use intel_mkl_tool::{Config, Library};
127///
128/// let cfg = Config::from_str("mkl-static-lp64-iomp").unwrap();
129/// if let Ok(lib) = Library::new(cfg) {
130///     lib.print_cargo_metadata().unwrap();
131/// }
132/// ```
133#[derive(Debug, Clone)]
134pub struct Library {
135    pub config: Config,
136    /// Directory where `mkl.h` and `mkl_version.h` exists
137    pub include_dir: PathBuf,
138    /// Directory where `libmkl_core.a` or `libmkl_core.so` exists
139    pub library_dir: PathBuf,
140
141    /// Directory where `libiomp5.a` or corresponding file exists
142    ///
143    /// - They are not required for `mkl-*-*-seq` and `mkl-dynamic-*-iomp` cases, and then this is `None`.
144    /// - Both static and dynamic dir can be `Some` when `openmp-strict-link-type` feature is OFF.
145    pub iomp5_static_dir: Option<PathBuf>,
146
147    /// Directory where `libiomp5.so` or corresponding file exists
148    ///
149    /// - They are not required for `mkl-*-*-seq` cases and `mkl-static-*-iomp`, and then this is `None`.
150    /// - Both static and dynamic dir can be `Some` when `openmp-strict-link-type` feature is OFF.
151    pub iomp5_dynamic_dir: Option<PathBuf>,
152}
153
154impl Library {
155    /// Find MKL using `pkg-config`
156    ///
157    /// This only use the installed prefix obtained by `pkg-config --variable=prefix`
158    ///
159    /// ```text
160    /// $ pkg-config --variable=prefix mkl-static-lp64-seq
161    /// /opt/intel/mkl
162    /// ```
163    ///
164    /// Then pass it to [Self::seek_directory].
165    ///
166    /// Limitation
167    /// -----------
168    /// This will not work for `mkl-*-*-iomp` configure since `libiomp5.{a,so}`
169    /// will not be found under the prefix directory of MKL.
170    /// Please use `$MKLROOT` environment variable for this case,
171    /// see [Self::new] for detail.
172    ///
173    pub fn pkg_config(config: Config) -> Result<Option<Self>> {
174        if let Ok(out) = Command::new("pkg-config")
175            .arg("--variable=prefix")
176            .arg(config.to_string())
177            .output()
178        {
179            if out.status.success() {
180                let path = String::from_utf8(out.stdout).context("Non-UTF8 MKL prefix")?;
181                let prefix = Path::new(path.trim());
182                let prefix = fs::canonicalize(prefix)?;
183                log::info!("pkg-config found {} on {}", config, prefix.display());
184                Self::seek_directory(config, prefix)
185            } else {
186                log::info!("pkg-config does not find {}", config);
187                Ok(None)
188            }
189        } else {
190            log::info!("pkg-config itself is not found");
191            Ok(None)
192        }
193    }
194
195    /// Seek MKL libraries in the given directory.
196    ///
197    /// - This will seek the directory recursively until finding MKL libraries,
198    ///   but do not follow symbolic links.
199    /// - This will not seek directory named `ia32*`
200    /// - Retuns `Ok(None)` if `libiomp5.{a,so}` is not found with `mkl-*-*-iomp` configure
201    ///   even if MKL binaries are found.
202    ///
203    pub fn seek_directory(config: Config, root_dir: impl AsRef<Path>) -> Result<Option<Self>> {
204        let root_dir = root_dir.as_ref();
205        if !root_dir.is_dir() {
206            return Ok(None);
207        }
208        let mut library_dir = None;
209        let mut include_dir = None;
210        let mut iomp5_static_dir = None;
211        let mut iomp5_dynamic_dir = None;
212        for (dir, file_name) in walkdir::WalkDir::new(root_dir)
213            .into_iter()
214            .flatten() // skip unreadable directories
215            .flat_map(|entry| {
216                let path = entry.into_path();
217                // Skip directory
218                if path.is_dir() {
219                    return None;
220                }
221                // Skip files for 32bit system under `ia32*/` and `win-x86`
222                if path.components().any(|c| {
223                    if let std::path::Component::Normal(c) = c {
224                        if let Some(c) = c.to_str() {
225                            if c.starts_with("ia32") || c == "win-x86" {
226                                return true;
227                            }
228                        }
229                    }
230                    false
231                }) {
232                    return None;
233                }
234
235                let dir = path
236                    .parent()
237                    .expect("parent must exist here since this is under `root_dir`")
238                    .to_owned();
239
240                if let Some(Some(file_name)) = path.file_name().map(|f| f.to_str()) {
241                    Some((dir, file_name.to_string()))
242                } else {
243                    None
244                }
245            })
246        {
247            if include_dir.is_none() && file_name == "mkl.h" {
248                log::info!("Found mkl.h at {}", dir.display());
249                include_dir = Some(dir);
250                continue;
251            }
252
253            if library_dir.is_none() {
254                for name in mkl_libs(config) {
255                    if file_name == mkl_file_name(config.link, &name) {
256                        log::info!("Found {} at {}", file_name, dir.display());
257                        library_dir = Some(dir.clone());
258                        continue;
259                    }
260                }
261            }
262
263            // Do not seek OpenMP runtime if `Threading::Sequential`
264            if config.parallel == Threading::OpenMP {
265                // Allow both dynamic/static library by default
266                //
267                // This is due to some distribution does not provide libiomp5.a
268                let possible_link_types = if cfg!(feature = "openmp-strict-link-type") {
269                    vec![config.link]
270                } else {
271                    vec![config.link, config.link.otherwise()]
272                };
273                for link in possible_link_types {
274                    if file_name == openmp_runtime_file_name(link) {
275                        match link {
276                            LinkType::Static => {
277                                log::info!(
278                                    "Found static OpenMP runtime ({}): {}",
279                                    file_name,
280                                    dir.display()
281                                );
282                                iomp5_static_dir = Some(dir.clone())
283                            }
284                            LinkType::Dynamic => {
285                                log::info!(
286                                    "Found dynamic OpenMP runtime ({}): {}",
287                                    file_name,
288                                    dir.display()
289                                );
290                                iomp5_dynamic_dir = Some(dir.clone())
291                            }
292                        }
293                    }
294                }
295            }
296        }
297        if config.parallel == Threading::OpenMP
298            && iomp5_dynamic_dir.is_none()
299            && iomp5_static_dir.is_none()
300        {
301            if let Some(ref lib) = library_dir {
302                log::warn!(
303                    "OpenMP runtime not found while MKL found at {}",
304                    lib.display()
305                );
306            }
307            return Ok(None);
308        }
309        Ok(match (library_dir, include_dir) {
310            (Some(library_dir), Some(include_dir)) => Some(Library {
311                config,
312                include_dir,
313                library_dir,
314                iomp5_static_dir,
315                iomp5_dynamic_dir,
316            }),
317            _ => None,
318        })
319    }
320
321    /// Seek MKL in system
322    ///
323    /// This try to find installed MKL in following order:
324    ///
325    /// - Ask to `pkg-config`
326    /// - Seek the directory specified by `$MKLROOT` environment variable
327    /// - Seek well-known directory
328    ///   - `/opt/intel` for Linux
329    ///   - `C:/Program Files (x86)/IntelSWTools/` and `C:/Program Files (x86)/Intel/oneAPI/` for Windows
330    ///
331    pub fn new(config: Config) -> Result<Self> {
332        if let Some(lib) = Self::pkg_config(config)? {
333            return Ok(lib);
334        }
335        if let Ok(mklroot) = std::env::var("MKLROOT") {
336            log::info!("MKLROOT environment variable is detected: {}", mklroot);
337            if let Some(lib) = Self::seek_directory(config, mklroot)? {
338                return Ok(lib);
339            }
340        }
341        for path in [
342            "/opt/intel",
343            "C:/Program Files (x86)/IntelSWTools/",
344            "C:/Program Files (x86)/Intel/oneAPI/",
345        ] {
346            let path = Path::new(path);
347            if let Some(lib) = Self::seek_directory(config, path)? {
348                return Ok(lib);
349            }
350        }
351        bail!("Intel MKL not found in system");
352    }
353
354    pub fn available() -> Vec<Self> {
355        Config::possibles()
356            .into_iter()
357            .flat_map(|cfg| Self::new(cfg).ok())
358            .collect()
359    }
360
361    /// Found MKL version parsed from `mkl_version.h`
362    ///
363    /// `mkl_version.h` will define
364    ///
365    /// ```c
366    /// #define __INTEL_MKL__ 2020
367    /// #define __INTEL_MKL_MINOR__ 0
368    /// #define __INTEL_MKL_UPDATE__ 1
369    /// ```
370    ///
371    /// and this corresponds to `(2020, 0, 1)`
372    ///
373    pub fn version(&self) -> Result<(u32, u32, u32)> {
374        let version_h = self.include_dir.join("mkl_version.h");
375
376        let f = fs::File::open(version_h).context("Failed to open mkl_version.h")?;
377        let f = io::BufReader::new(f);
378        let mut year = None;
379        let mut minor = None;
380        let mut update = None;
381        for line in f.lines().flatten() {
382            if !line.starts_with("#define") {
383                continue;
384            }
385            let ss: Vec<&str> = line.split_whitespace().collect();
386            match ss[1] {
387                "__INTEL_MKL__" => year = Some(ss[2].parse()?),
388                "__INTEL_MKL_MINOR__" => minor = Some(ss[2].parse()?),
389                "__INTEL_MKL_UPDATE__" => update = Some(ss[2].parse()?),
390                _ => continue,
391            }
392        }
393        match (year, minor, update) {
394            (Some(year), Some(minor), Some(update)) => Ok((year, minor, update)),
395            _ => bail!("Invalid mkl_version.h"),
396        }
397    }
398
399    /// Print `cargo:rustc-link-*` metadata to stdout
400    pub fn print_cargo_metadata(&self) -> Result<()> {
401        println!("cargo:rerun-if-env-changed=MKLROOT");
402        println!("cargo:rustc-link-search={}", self.library_dir.display());
403        for lib in mkl_libs(self.config) {
404            match self.config.link {
405                LinkType::Static => {
406                    println!("cargo:rustc-link-lib=static={}", lib);
407                }
408                LinkType::Dynamic => {
409                    println!("cargo:rustc-link-lib=dylib={}", lib);
410                }
411            }
412        }
413
414        if self.config.parallel == Threading::OpenMP {
415            if let Some(ref dir) = self.iomp5_static_dir {
416                println!("cargo:rustc-link-search={}", dir.display());
417            }
418            if let Some(ref dir) = self.iomp5_dynamic_dir {
419                println!("cargo:rustc-link-search={}", dir.display());
420            }
421            println!("cargo:rustc-link-lib={}", OPENMP_RUNTIME_LIB);
422        }
423        Ok(())
424    }
425}