hardware acceleration and support for NVIDIA cards on nonfree images (#3089)

* add nvidia packages

* add nvidia deps to nonfree

* gpu_acceleration flag & nvidia hacking

* fix gpu_config & /tmp/lxc.log

* implement hardware acceleration more dynamically

* refactor OpenUI

* use mknod

* registry updates for multi-hardware-requirements

* pluralize

* handle new registry types

* remove log

* migrations and driver fixes

* wip

* misc patches

* handle nvidia-container differently

* chore: comments (#3093)

* chore: comments

* revert some sizing

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* Revert "handle nvidia-container differently"

This reverts commit d708ae53df.

* fix debian containers

* cleanup

* feat: add empty array placeholder in forms (#3095)

* fixes from testing, client side device filtering for better fingerprinting resistance

* fix mac builds

---------

Co-authored-by: Sam Sartor <me@samsartor.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
Aiden McClelland
2026-01-15 11:42:17 -08:00
committed by GitHub
parent e8ef39adad
commit 99871805bd
95 changed files with 2758 additions and 1092 deletions

View File

@@ -7,7 +7,7 @@ use ts_rs::TS;
use crate::prelude::*;
use crate::util::Invoke;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)]
#[ts(type = "string")]
pub struct GitHash(String);

View File

@@ -242,18 +242,23 @@ impl TryFrom<ManifestV1> for Manifest {
.device
.into_iter()
.map(|(class, product)| DeviceFilter {
pattern_description: format!(
description: format!(
"a {class} device matching the expression {}",
product.as_ref()
),
class,
pattern: product,
product: Some(product),
..Default::default()
})
.collect(),
},
git_hash: value.git_hash,
os_version: value.eos_version,
sdk_version: None,
hardware_acceleration: match value.main {
PackageProcedure::Docker(d) => d.gpu_acceleration,
PackageProcedure::Script(_) => false,
},
})
}
}

View File

@@ -15,6 +15,7 @@ use crate::s9pk::git_hash::GitHash;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex;
use crate::util::{VersionString, mime};
use crate::version::{Current, VersionT};
@@ -62,6 +63,8 @@ pub struct Manifest {
pub dependencies: Dependencies,
#[serde(default)]
pub hardware_requirements: HardwareRequirements,
#[serde(default)]
pub hardware_acceleration: bool,
pub git_hash: Option<GitHash>,
#[serde(default = "current_version")]
#[ts(type = "string")]
@@ -165,7 +168,7 @@ impl Manifest {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct HardwareRequirements {
@@ -176,19 +179,122 @@ pub struct HardwareRequirements {
#[ts(type = "string[] | null")]
pub arch: Option<BTreeSet<InternedString>>,
}
impl HardwareRequirements {
/// returns a value that can be used as a sort key to get most specific requirements first
pub fn specificity_desc(&self) -> (u32, u32, u64) {
(
u32::MAX - self.device.len() as u32, // more device requirements = more specific
self.arch.as_ref().map_or(u32::MAX, |a| a.len() as u32), // more arches = less specific
self.ram.map_or(0, |r| r), // more ram = more specific
)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DeviceFilter {
pub description: String,
#[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString,
#[ts(type = "string")]
pub pattern: Regex,
pub pattern_description: String,
#[ts(type = "string | null")]
pub product: Option<Regex>,
#[ts(type = "string | null")]
pub vendor: Option<Regex>,
#[ts(optional)]
pub capabilities: Option<BTreeSet<InternedString>>,
#[ts(optional)]
pub driver: Option<InternedString>,
}
// Omit description
impl PartialEq for DeviceFilter {
fn eq(&self, other: &Self) -> bool {
self.class == other.class
&& self.product == other.product
&& self.vendor == other.vendor
&& self.capabilities == other.capabilities
&& self.driver == other.driver
}
}
impl DeviceFilter {
pub fn matches(&self, device: &LshwDevice) -> bool {
if &*self.class != device.class() {
return false;
}
match device {
LshwDevice::Processor(LshwProcessor {
product,
vendor,
capabilities,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
true
}
LshwDevice::Display(LshwDisplay {
product,
vendor,
capabilities,
driver,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
if !self
.driver
.as_ref()
.map_or(true, |d| Some(d) == driver.as_ref())
{
return false;
}
true
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
#[ts(export)]
pub struct Description {
pub short: String,
@@ -212,7 +318,7 @@ impl Description {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Alerts {

View File

@@ -265,7 +265,7 @@ impl PackParams {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ImageConfig {
@@ -274,15 +274,8 @@ pub struct ImageConfig {
pub arch: BTreeSet<InternedString>,
#[ts(type = "string | null")]
pub emulate_missing_as: Option<InternedString>,
}
impl Default for ImageConfig {
fn default() -> Self {
Self {
source: ImageSource::Packed,
arch: BTreeSet::new(),
emulate_missing_as: None,
}
}
#[serde(default)]
pub nvidia_container: bool,
}
#[derive(Parser)]
@@ -299,6 +292,8 @@ struct CliImageConfig {
arch: Vec<InternedString>,
#[arg(long)]
emulate_missing_as: Option<InternedString>,
#[arg(long)]
nvidia_container: bool,
}
impl TryFrom<CliImageConfig> for ImageConfig {
type Error = clap::Error;
@@ -317,6 +312,7 @@ impl TryFrom<CliImageConfig> for ImageConfig {
},
arch: value.arch.into_iter().collect(),
emulate_missing_as: value.emulate_missing_as,
nvidia_container: value.nvidia_container,
};
res.emulate_missing_as
.as_ref()
@@ -379,20 +375,21 @@ pub enum ImageSource {
DockerTag(String),
// Recipe(DirRecipe),
}
impl Default for ImageSource {
fn default() -> Self {
ImageSource::Packed
}
}
impl ImageSource {
pub fn ingredients(&self) -> Vec<PathBuf> {
match self {
Self::Packed => Vec::new(),
Self::DockerBuild {
dockerfile,
workdir,
..
} => {
Self::DockerBuild { dockerfile, .. } => {
vec![
workdir
dockerfile
.as_deref()
.unwrap_or(Path::new("."))
.join(dockerfile.as_deref().unwrap_or(Path::new("Dockerfile"))),
.unwrap_or(Path::new("Dockerfile"))
.to_owned(),
]
}
Self::DockerTag(_) => Vec::new(),