Viewing: lib.rs

// SPDX-License-Identifier: MIT

// Copyright (c) 2025 DDN. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

use std::process::Command;

#[derive(Debug)]
pub struct LustreVersion {
    pub major: u32,
    pub minor: u32,
    pub patch: u32,
    pub extra: String,
}

impl LustreVersion {
    fn features(self) {
        assert_eq!(self.major, 2);
        if self.minor < 14 {
            println!(
                "cargo:error=Unsupported Lustre version: minor version must be >= 14, but found {}",
                self.minor
            );
            return;
        }
        if self.minor >= 14 {
            println!(r#"cargo:rustc-cfg=feature="LUSTRE_{}_14""#, self.major);
        }
        if self.minor >= 15 {
            println!(r#"cargo:rustc-cfg=feature="LUSTRE_{}_15""#, self.major);
        }
        if self.minor >= 16 {
            println!(r#"cargo:rustc-cfg=feature="LUSTRE_{}_16""#, self.major);
        }
        if self.minor >= 17 {
            println!(r#"cargo:rustc-cfg=feature="LUSTRE_{}_17""#, self.major);
        }
        if self.minor >= 18 {
            println!(r#"cargo:rustc-cfg=feature="LUSTRE_{}_18""#, self.major);
        }

        if self.patch >= 50 {
            println!(
                r#"cargo:rustc-cfg=feature="LUSTRE_{}_{}""#,
                self.major,
                self.minor + 1
            );
        }
    }
}

pub fn export_features() -> Result<(), String> {
    current()
        .map_err(|e| format!("failed to get Lustre version: {e}"))?
        .features();

    Ok(())
}

/// Detects the installed Lustre filesystem version.
///
/// This function executes the `lfs --version` command and parses the output
/// to extract the major, minor, and patch version numbers of the installed
/// Lustre client.
///
/// # Returns
///
/// A `Result` containing:
/// - `Ok(LustreVersion)`: A struct with major, minor, patch version numbers and extra info if successful
/// - `Err(String)`: An error message describing what went wrong
///
/// # Errors
///
/// Returns an error string in the following cases:
/// - If the Lustre client tools are not installed or the command execution fails
/// - If the version string output doesn't match the expected format
/// - If any version component cannot be parsed as a `u32` number
///
/// # Examples
///
/// ```
/// let version = lu_version::current().expect("should be able to detect Lustre version");
/// println!("Detected Lustre version {}.{}.{}", version.major, version.minor, version.patch);
/// ```
pub fn current() -> Result<LustreVersion, String> {
    let output = Command::new("lfs")
        .arg("--version")
        .output()
        .map_err(|e| format!("lustre client tools should be installed: {e}"))?;

    let s = String::from_utf8_lossy(output.stdout.as_ref());
    let s = s.trim();
    let s = if s.contains("lfs ") { &s[4..] } else { s };

    parse_version(s)
}

fn parse_version(s: &str) -> Result<LustreVersion, String> {
    let re =
        regex::Regex::new(r"^(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)_?(?<extra>.*)$")
            .expect("regex should compile");

    let Some(caps) = re.captures(s.trim()) else {
        return Err(format!("unexpected output from lfs --version: {s}"));
    };

    let major = caps
        .name("major")
        .ok_or("missing major version")?
        .as_str()
        .parse::<u32>()
        .map_err(|e| format!("error in major: {e}"))?;

    let minor = caps
        .name("minor")
        .ok_or("missing minor version")?
        .as_str()
        .parse::<u32>()
        .map_err(|e| format!("error in minor: {e}"))?;

    let patch = caps
        .name("patch")
        .ok_or("missing patch version")?
        .as_str()
        .parse::<u32>()
        .map_err(|e| format!("error in patch: {e}"))?;

    let extra = caps
        .name("extra")
        .ok_or("error parsing extra")?
        .as_str()
        .to_string();

    Ok(LustreVersion {
        major,
        minor,
        patch,
        extra,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic() {
        let version = parse_version("1.2.3");
        assert!(version.is_ok(), "Failed to get Lustre version: {version:?}");
        let version = version.unwrap();
        assert_eq!(version.major, 1);
        assert_eq!(version.minor, 2);
        assert_eq!(version.patch, 3);
        assert_eq!(version.extra, "");
    }

    #[test]
    fn test_extra() {
        let version = parse_version("2.16.55_16_g33442e0");
        assert!(version.is_ok(), "Failed to get Lustre version: {version:?}");
        let version = version.unwrap();
        assert_eq!(version.major, 2);
        assert_eq!(version.minor, 16);
        assert_eq!(version.patch, 55);
        assert_eq!(version.extra, "16_g33442e0");
    }
}