ovfetch 0.2.2

Resolve, download, and hash-verify the OpenVINO-enabled ONNX Runtime an Intel NPU needs
Documentation
//! The repo's `data/` files. They are compiled into the binary, so a release
//! carries the exact ledger it was reviewed with; `--data-dir` reads a checkout
//! instead, which is how CI edits them.

use crate::version::Ver;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Platforms {
    #[serde(default, rename = "platform")]
    pub list: Vec<Platform>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Platform {
    /// NPU PCI device ID as sysfs prints it, e.g. `0xfd3e`.
    pub pci_id: String,
    /// The kernel's `PCI_DEVICE_ID_*` suffix.
    pub kernel_name: String,
    /// Name as Intel's NPU driver release notes print it. Empty until a human
    /// fills it in, which leaves the platform's floors unknown.
    #[serde(default)]
    pub codename: String,
    /// Derived from the first NPU driver release verified on this platform.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub min_openvino: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub min_npu_driver: Option<String>,
    /// A human-tested floor that `ci discover` must not overwrite.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pinned_note: Option<String>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct NpuDrivers {
    #[serde(default, rename = "driver")]
    pub list: Vec<NpuDriver>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NpuDriver {
    pub version: String,
    /// The OpenVINO Intel's release notes pair this driver with.
    pub openvino: String,
    pub platforms: Vec<String>,
    /// A newer OpenVINO measured compiling on this driver; `ci discover` never
    /// writes it, so it survives regeneration.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub measured_openvino: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub measured_note: Option<String>,
}

impl NpuDriver {
    /// Newest OpenVINO known to compile on this driver: Intel's pairing, or a
    /// newer one measured by hand.
    pub fn max_openvino(&self) -> Option<Ver> {
        let paired = Ver::parse(&self.openvino);
        let measured = self.measured_openvino.as_deref().and_then(Ver::parse);
        paired.max(measured)
    }
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Ledger {
    #[serde(default, rename = "artifact")]
    pub list: Vec<Entry>,
}

/// One immutable artifact. A published hash for an id must never change, and
/// `ci audit` fails the day one does.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
    /// `pypi/<project>/<filename>` or `github/<owner>/<repo>/<tag>/<asset>`.
    pub id: String,
    /// sha256 of the file.
    pub digest: String,
    pub first_seen: String,
    /// PyPI reported a PEP 740 provenance attestation for this file.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub provenance: bool,
}

impl Ledger {
    pub fn get(&self, id: &str) -> Option<&Entry> {
        self.list.iter().find(|e| e.id == id)
    }
}

pub struct Data {
    pub dir: Option<PathBuf>,
    pub platforms: Platforms,
    pub npu_drivers: NpuDrivers,
    pub ledger: Ledger,
}

fn parse<T: for<'de> Deserialize<'de>>(name: &str, text: &str) -> Result<T> {
    toml::from_str(text).with_context(|| format!("parsing data/{name}"))
}

impl Data {
    pub fn load(dir: Option<&Path>) -> Result<Data> {
        let read = |name: &str, embedded: &'static str| -> Result<String> {
            match dir {
                Some(d) => std::fs::read_to_string(d.join(name))
                    .with_context(|| format!("reading {}", d.join(name).display())),
                None => Ok(embedded.to_owned()),
            }
        };
        Ok(Data {
            dir: dir.map(Path::to_path_buf),
            platforms: parse(
                "platforms.toml",
                &read("platforms.toml", include_str!("../data/platforms.toml"))?,
            )?,
            npu_drivers: parse(
                "npu_drivers.toml",
                &read("npu_drivers.toml", include_str!("../data/npu_drivers.toml"))?,
            )?,
            ledger: parse(
                "ledger.toml",
                &read("ledger.toml", include_str!("../data/ledger.toml"))?,
            )?,
        })
    }

    pub fn save(&self) -> Result<()> {
        let dir = self.dir.as_ref().context("saving needs --data-dir")?;
        let header =
            "# Generated by `ovfetch ci discover`; human edits are reviewed in its PRs.\n\n";
        let write =
            |name: &str, body: String| std::fs::write(dir.join(name), format!("{header}{body}"));
        write("platforms.toml", toml::to_string(&self.platforms)?)?;
        write("npu_drivers.toml", toml::to_string(&self.npu_drivers)?)?;
        write("ledger.toml", toml::to_string(&self.ledger)?)?;
        Ok(())
    }
}