feat: Add DKMS support for Depot-managed kernel modules

- Introduced a new `dkms` module in the installation helpers.
- Updated `BuildType` enum to include `Dkms`.
- Enhanced `PackageSpec` to validate DKMS-specific fields and added methods to retrieve effective DKMS names, versions, and install directories.
- Implemented DKMS build and installation logic in a new `dkms.rs` file, including staging source trees and lifecycle scripts.
- Added runtime support for managing installed DKMS modules, including autoinstall and removal functionalities.
- Created tests to validate DKMS parsing and module management.
This commit is contained in:
2026-07-08 02:23:19 -05:00
parent 86905c59d3
commit 98eb1fe246
14 changed files with 1293 additions and 2 deletions
+140
View File
@@ -87,6 +87,7 @@ impl PackageSpec {
);
}
spec.validate_manual_sources()?;
spec.validate_dkms()?;
Ok(spec)
}
@@ -166,6 +167,52 @@ impl PackageSpec {
Ok(())
}
fn validate_dkms(&self) -> Result<()> {
if !matches!(self.build.build_type, BuildType::Dkms) {
return Ok(());
}
if self.build.flags.dkms_modules.is_empty() {
anyhow::bail!(
"build.type = \"dkms\" requires at least one build.flags.dkms_modules entry"
);
}
validate_dkms_component("build.flags.dkms_name", &self.effective_dkms_name())?;
validate_dkms_component("build.flags.dkms_version", &self.effective_dkms_version())?;
validate_dkms_rel_path(
"build.flags.dkms_source_dir",
self.build.flags.dkms_source_dir.trim(),
true,
)?;
validate_dkms_install_dir(
"build.flags.dkms_install_dir",
self.effective_dkms_install_dir().as_str(),
)?;
let mut installed_names = HashSet::new();
for (idx, module) in self.build.flags.dkms_modules.iter().enumerate() {
let label = format!("build.flags.dkms_modules[{idx}]");
validate_dkms_component(&format!("{label}.name"), module.name.trim())?;
let dest_name = self.effective_dkms_module_dest_name(module);
validate_dkms_component(&format!("{label}.dest_name"), &dest_name)?;
validate_dkms_rel_path(&format!("{label}.build_dir"), module.build_dir.trim(), true)?;
validate_dkms_rel_path(
&format!("{label}.built_location"),
module.built_location.trim(),
true,
)?;
let install_dir = self.effective_dkms_module_install_dir(module);
validate_dkms_install_dir(&format!("{label}.install_dir"), &install_dir)?;
if !installed_names.insert(dest_name.clone()) {
anyhow::bail!("Duplicate DKMS destination module name: {}", dest_name);
}
}
Ok(())
}
/// Expand variables like `$name` and `$version` in a string.
pub fn expand_vars(&self, input: &str) -> String {
let specdir = self.spec_dir.to_string_lossy();
@@ -180,6 +227,56 @@ impl PackageSpec {
&self.source
}
/// Return the effective Depot DKMS source package name.
pub fn effective_dkms_name(&self) -> String {
let configured = self.build.flags.dkms_name.trim();
if configured.is_empty() {
self.package.name.clone()
} else {
self.expand_vars(configured)
}
}
/// Return the effective Depot DKMS source package version.
pub fn effective_dkms_version(&self) -> String {
let configured = self.build.flags.dkms_version.trim();
if configured.is_empty() {
self.package.version.clone()
} else {
self.expand_vars(configured)
}
}
/// Return the default module install directory for Depot DKMS packages.
pub fn effective_dkms_install_dir(&self) -> String {
let configured = self.build.flags.dkms_install_dir.trim();
if configured.is_empty() {
"updates/depot".to_string()
} else {
normalize_dkms_install_dir(&self.expand_vars(configured))
}
}
/// Return the effective installed module name for a DKMS module entry.
pub fn effective_dkms_module_dest_name(&self, module: &DkmsModule) -> String {
let configured = module.dest_name.trim();
if configured.is_empty() {
module.name.clone()
} else {
self.expand_vars(configured)
}
}
/// Return the effective install directory for a DKMS module entry.
pub fn effective_dkms_module_install_dir(&self, module: &DkmsModule) -> String {
let configured = module.install_dir.trim();
if configured.is_empty() {
self.effective_dkms_install_dir()
} else {
normalize_dkms_install_dir(&self.expand_vars(configured))
}
}
/// Returns true when this spec is a metadata-only package that exists to pull dependencies.
pub fn is_metapackage(&self) -> bool {
matches!(self.build.build_type, BuildType::Meta)
@@ -339,6 +436,49 @@ impl PackageSpec {
}
}
fn normalize_dkms_install_dir(raw: &str) -> String {
raw.trim().trim_start_matches('/').to_string()
}
fn validate_dkms_component(label: &str, value: &str) -> Result<()> {
let value = value.trim();
if value.is_empty() {
anyhow::bail!("{label} cannot be empty");
}
if value.contains('/') || value.contains('\\') {
anyhow::bail!("{label} cannot contain path separators: {value}");
}
if value == "." || value == ".." {
anyhow::bail!("{label} cannot be '.' or '..'");
}
Ok(())
}
fn validate_dkms_install_dir(label: &str, value: &str) -> Result<()> {
validate_dkms_rel_path(label, value.trim_start_matches('/'), false)
}
fn validate_dkms_rel_path(label: &str, value: &str, allow_empty: bool) -> Result<()> {
let value = value.trim();
if value.is_empty() {
if allow_empty {
return Ok(());
}
anyhow::bail!("{label} cannot be empty");
}
let path = Path::new(value);
if path.is_absolute() {
anyhow::bail!("{label} must be relative: {value}");
}
for component in path.components() {
match component {
std::path::Component::Normal(_) | std::path::Component::CurDir => {}
_ => anyhow::bail!("{label} contains an unsafe path component: {value}"),
}
}
Ok(())
}
impl fmt::Display for PackageSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(