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
+2 -2
View File
@@ -54,7 +54,7 @@ pub(crate) fn build_tool_package_option(build_type: BuildType) -> Option<&'stati
BuildType::Python => Some("DEPOT_PYTHON_PACKAGE"), BuildType::Python => Some("DEPOT_PYTHON_PACKAGE"),
BuildType::Rust => Some("DEPOT_RUST_PACKAGE"), BuildType::Rust => Some("DEPOT_RUST_PACKAGE"),
BuildType::Makefile => Some("DEPOT_MAKEFILE_PACKAGE"), BuildType::Makefile => Some("DEPOT_MAKEFILE_PACKAGE"),
BuildType::Bin | BuildType::Meta => None, BuildType::Dkms | BuildType::Bin | BuildType::Meta => None,
} }
} }
@@ -68,7 +68,7 @@ pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option<Stri
BuildType::Python => normalize_string_option(option_env!("DEPOT_PYTHON_PACKAGE")), BuildType::Python => normalize_string_option(option_env!("DEPOT_PYTHON_PACKAGE")),
BuildType::Rust => normalize_string_option(option_env!("DEPOT_RUST_PACKAGE")), BuildType::Rust => normalize_string_option(option_env!("DEPOT_RUST_PACKAGE")),
BuildType::Makefile => normalize_string_option(option_env!("DEPOT_MAKEFILE_PACKAGE")), BuildType::Makefile => normalize_string_option(option_env!("DEPOT_MAKEFILE_PACKAGE")),
BuildType::Bin | BuildType::Meta => None, BuildType::Dkms | BuildType::Bin | BuildType::Meta => None,
} }
} }
+378
View File
@@ -0,0 +1,378 @@
//! Depot-managed kernel module source packaging.
use crate::cross::CrossConfig;
use crate::install::scripts;
use crate::package::{DkmsModule, PackageSpec};
use anyhow::{Context, Result};
use std::collections::BTreeSet;
use std::fs;
use std::path::{Component, Path, PathBuf};
const DEPOT_KMOD_MANIFEST: &str = ".depot-kmod.toml";
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct DepotKmodManifest {
pub name: String,
pub version: String,
pub install_dir: String,
pub make_args: Vec<String>,
pub pre_build: Vec<String>,
pub modules: Vec<DepotKmodModule>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct DepotKmodModule {
pub name: String,
pub dest_name: String,
pub build_dir: String,
pub built_location: String,
pub install_dir: String,
}
pub fn build(
spec: &PackageSpec,
src_dir: &Path,
destdir: &Path,
_cross: Option<&CrossConfig>,
_export_compiler_flags: bool,
_host_build_dir: Option<&Path>,
) -> Result<()> {
if spec.build.flags.lib32_variant {
anyhow::bail!("build.type = \"dkms\" does not support lib32 variants");
}
fs::create_dir_all(destdir)
.with_context(|| format!("Failed to create DESTDIR: {}", destdir.display()))?;
let source_root = resolve_dkms_source_root(spec, src_dir)?;
let source_dest = staged_source_dir(spec, destdir);
if source_dest.exists() {
fs::remove_dir_all(&source_dest).with_context(|| {
format!(
"Failed to remove existing DKMS source staging dir: {}",
source_dest.display()
)
})?;
}
crate::fs_copy::copy_tree_preserving_links(&source_root, &source_dest).with_context(|| {
format!(
"Failed to stage DKMS source tree from {} to {}",
source_root.display(),
source_dest.display()
)
})?;
let manifest = manifest_from_spec(spec)?;
let manifest_toml =
toml::to_string_pretty(&manifest).context("Failed to serialize DKMS metadata")?;
fs::write(source_dest.join(DEPOT_KMOD_MANIFEST), manifest_toml).with_context(|| {
format!(
"Failed to write DKMS metadata: {}",
source_dest.join(DEPOT_KMOD_MANIFEST).display()
)
})?;
Ok(())
}
pub(crate) fn stage_lifecycle_scripts(spec: &PackageSpec, destdir: &Path) -> Result<()> {
if !spec.build.flags.dkms_autoinstall {
return Ok(());
}
let scripts_dir = scripts::staged_scripts_dir(destdir);
fs::create_dir_all(&scripts_dir)
.with_context(|| format!("Failed to create scripts dir: {}", scripts_dir.display()))?;
let source_rel = format!("/usr/src/{}", source_dir_name(spec));
let package_name = spec.effective_dkms_name();
stage_script_command(
&scripts_dir.join("post_install"),
&autoinstall_command(&source_rel),
)?;
stage_script_command(
&scripts_dir.join("post_update"),
&autoinstall_command(&source_rel),
)?;
stage_script_command(
&scripts_dir.join("pre_remove"),
&remove_command(&package_name),
)?;
stage_script_command(
&scripts_dir.join("pre_update"),
&remove_command(&package_name),
)?;
Ok(())
}
pub(crate) fn manifest_from_spec(spec: &PackageSpec) -> Result<DepotKmodManifest> {
let default_install_dir = spec.effective_dkms_install_dir();
let modules = spec
.build
.flags
.dkms_modules
.iter()
.map(|module| manifest_module_from_spec(spec, module, &default_install_dir))
.collect::<Result<Vec<_>>>()?;
let mut build_dirs = BTreeSet::new();
for module in &modules {
build_dirs.insert(module.build_dir.clone());
}
if build_dirs.is_empty() {
anyhow::bail!("DKMS manifest must contain at least one module");
}
Ok(DepotKmodManifest {
name: spec.effective_dkms_name(),
version: spec.effective_dkms_version(),
install_dir: default_install_dir,
make_args: spec
.build
.flags
.dkms_make_args
.iter()
.map(|arg| spec.expand_vars(arg))
.collect(),
pre_build: spec
.build
.flags
.dkms_pre_build
.iter()
.map(|arg| spec.expand_vars(arg))
.collect(),
modules,
})
}
fn manifest_module_from_spec(
spec: &PackageSpec,
module: &DkmsModule,
default_install_dir: &str,
) -> Result<DepotKmodModule> {
let name = spec.expand_vars(module.name.trim());
let dest_name = spec.effective_dkms_module_dest_name(module);
let build_dir = normalized_rel_string(&spec.expand_vars(module.build_dir.trim()), true)?;
let built_location = if module.built_location.trim().is_empty() {
build_dir.clone()
} else {
normalized_rel_string(&spec.expand_vars(module.built_location.trim()), true)?
};
let install_dir = if module.install_dir.trim().is_empty() {
default_install_dir.to_string()
} else {
spec.effective_dkms_module_install_dir(module)
};
Ok(DepotKmodModule {
name,
dest_name,
build_dir,
built_location,
install_dir,
})
}
fn resolve_dkms_source_root(spec: &PackageSpec, src_dir: &Path) -> Result<PathBuf> {
let base = if spec.build.flags.source_subdir.trim().is_empty() {
src_dir.to_path_buf()
} else {
let subdir =
normalized_rel_string(&spec.expand_vars(&spec.build.flags.source_subdir), false)?;
src_dir.join(subdir)
};
let source_dir = spec.expand_vars(spec.build.flags.dkms_source_dir.trim());
let source_root = if source_dir.trim().is_empty() {
base
} else {
base.join(normalized_rel_string(&source_dir, false)?)
};
if !source_root.is_dir() {
anyhow::bail!("DKMS source directory not found: {}", source_root.display());
}
Ok(source_root)
}
fn staged_source_dir(spec: &PackageSpec, destdir: &Path) -> PathBuf {
destdir.join("usr/src").join(source_dir_name(spec))
}
fn source_dir_name(spec: &PackageSpec) -> String {
format!(
"{}-{}",
spec.effective_dkms_name(),
spec.effective_dkms_version()
)
}
fn autoinstall_command(source_rel: &str) -> String {
format!(
"depot internal dkms-autoinstall --rootfs \"$DEPOT_ROOTFS\" --source {}",
sh_quote(source_rel)
)
}
fn remove_command(name: &str) -> String {
format!(
"depot internal dkms-remove --rootfs \"$DEPOT_ROOTFS\" --name {}",
sh_quote(name)
)
}
fn stage_script_command(path: &Path, command: &str) -> Result<()> {
if path.exists() {
use std::io::Write;
let mut file = fs::OpenOptions::new()
.append(true)
.open(path)
.with_context(|| format!("Failed to open lifecycle script: {}", path.display()))?;
writeln!(file).with_context(|| format!("Failed to update {}", path.display()))?;
writeln!(file, "# Depot DKMS autoinstall").with_context(|| {
format!(
"Failed to append lifecycle script marker: {}",
path.display()
)
})?;
writeln!(file, "{command}")
.with_context(|| format!("Failed to append lifecycle script: {}", path.display()))?;
} else {
fs::write(path, format!("#!/bin/sh\nset -eu\n{command}\n"))
.with_context(|| format!("Failed to write lifecycle script: {}", path.display()))?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms)
.with_context(|| format!("Failed to make script executable: {}", path.display()))?;
}
Ok(())
}
fn normalized_rel_string(raw: &str, allow_empty: bool) -> Result<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
if allow_empty {
return Ok(".".to_string());
}
anyhow::bail!("DKMS path cannot be empty");
}
let path = Path::new(trimmed);
if path.is_absolute() {
anyhow::bail!("DKMS path must be relative: {}", trimmed);
}
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
_ => anyhow::bail!("DKMS path contains unsafe component: {}", trimmed),
}
}
if out.as_os_str().is_empty() {
Ok(".".to_string())
} else {
Ok(out.to_string_lossy().into_owned())
}
}
fn sh_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source,
};
use tempfile::tempdir;
fn mk_spec() -> PackageSpec {
let flags = BuildFlags {
dkms_modules: vec![
DkmsModule {
name: "zfs".into(),
build_dir: "module".into(),
built_location: "module/zfs".into(),
..DkmsModule::default()
},
DkmsModule {
name: "spl".into(),
dest_name: "spl_compat".into(),
build_dir: "module".into(),
built_location: "module/spl".into(),
install_dir: "updates/storage".into(),
},
],
dkms_make_args: vec!["V=1".into()],
dkms_pre_build: vec!["./configure --with-linux=$kernel_build_dir".into()],
..BuildFlags::default()
};
PackageSpec {
package: PackageInfo {
name: "zfs-dkms".into(),
real_name: None,
version: "2.4.3".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: Vec::new(),
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/zfs.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "zfs".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Dkms,
flags,
},
dependencies: Dependencies::default(),
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
#[test]
fn dkms_build_stages_source_metadata_and_scripts() -> Result<()> {
let tmp = tempdir()?;
let src = tmp.path().join("src");
let dest = tmp.path().join("dest");
fs::create_dir_all(src.join("module/zfs"))?;
fs::write(src.join("module/zfs/zfs.c"), "source")?;
let spec = mk_spec();
build(&spec, &src, &dest, None, true, None)?;
fs::create_dir_all(dest.join("scripts"))?;
fs::write(
dest.join("scripts/post_install"),
"#!/bin/sh\necho package\n",
)?;
stage_lifecycle_scripts(&spec, &dest)?;
let staged = dest.join("usr/src/zfs-dkms-2.4.3");
assert!(staged.join("module/zfs/zfs.c").exists());
let manifest: DepotKmodManifest =
toml::from_str(&fs::read_to_string(staged.join(DEPOT_KMOD_MANIFEST))?)?;
assert_eq!(manifest.name, "zfs-dkms");
assert_eq!(manifest.pre_build.len(), 1);
assert_eq!(manifest.modules.len(), 2);
assert_eq!(manifest.modules[1].dest_name, "spl_compat");
let post_install = fs::read_to_string(dest.join("scripts/post_install"))?;
assert!(post_install.contains("echo package"));
assert!(post_install.contains("dkms-autoinstall"));
assert!(dest.join("scripts/pre_update").exists());
Ok(())
}
}
+16
View File
@@ -4,6 +4,7 @@ mod autotools;
mod bin; mod bin;
mod cmake; mod cmake;
mod custom; mod custom;
mod dkms;
mod makefile; mod makefile;
mod meson; mod meson;
mod perl; mod perl;
@@ -489,6 +490,13 @@ pub(crate) fn requested_development_package() -> Option<String> {
crate::build_options::requested_development_package() crate::build_options::requested_development_package()
} }
pub(crate) fn stage_generated_lifecycle_scripts(spec: &PackageSpec, destdir: &Path) -> Result<()> {
match spec.build.build_type {
BuildType::Dkms => dkms::stage_lifecycle_scripts(spec, destdir),
_ => Ok(()),
}
}
fn configured_install_dir(value: &str, default: &str) -> String { fn configured_install_dir(value: &str, default: &str) -> String {
let trimmed = value.trim(); let trimmed = value.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@@ -1100,6 +1108,14 @@ pub fn build(
export_compiler_flags, export_compiler_flags,
host_build_dir, host_build_dir,
), ),
BuildType::Dkms => dkms::build(
spec,
src_dir,
destdir,
cross,
export_compiler_flags,
host_build_dir,
),
BuildType::Bin => bin::build( BuildType::Bin => bin::build(
spec, spec,
src_dir, src_dir,
+16
View File
@@ -384,6 +384,22 @@ pub enum InternalCommands {
#[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)] #[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)]
args: Vec<String>, args: Vec<String>,
}, },
#[command(hide = true)]
DkmsAutoinstall {
#[arg(long, default_value = "/")]
rootfs: PathBuf,
#[arg(long)]
source: PathBuf,
},
#[command(hide = true)]
DkmsRemove {
#[arg(long, default_value = "/")]
rootfs: PathBuf,
#[arg(long)]
source: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
},
} }
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
+1
View File
@@ -2501,6 +2501,7 @@ fn run_direct_install_request(
// 3.1 Copy license files into staged tree // 3.1 Copy license files into staged tree
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?; staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?; install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?;
builder::stage_generated_lifecycle_scripts(&pkg_spec, &destdir)?;
} }
destdir destdir
+1
View File
@@ -365,6 +365,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
if !lib32_only { if !lib32_only {
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?; staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?; install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?;
builder::stage_generated_lifecycle_scripts(&pkg_spec, &destdir)?;
staging::process(&destdir, &pkg_spec)?; staging::process(&destdir, &pkg_spec)?;
staging::stage_split_package_licenses(&src_dir, &destdir, &pkg_spec)?; staging::stage_split_package_licenses(&src_dir, &destdir, &pkg_spec)?;
if let Some(watcher) = interrupt_watcher.as_ref() { if let Some(watcher) = interrupt_watcher.as_ref() {
+1
View File
@@ -354,6 +354,7 @@ pub(crate) fn build_lib32_companion_package(
); );
} }
install::scripts::stage_scripts_from_spec_dir(&lib32_pkg_spec, &lib32_destdir)?; install::scripts::stage_scripts_from_spec_dir(&lib32_pkg_spec, &lib32_destdir)?;
builder::stage_generated_lifecycle_scripts(&lib32_pkg_spec, &lib32_destdir)?;
staging::process(&lib32_destdir, &lib32_pkg_spec)?; staging::process(&lib32_destdir, &lib32_pkg_spec)?;
staging::symlink_package_license( staging::symlink_package_license(
&lib32_destdir, &lib32_destdir,
+13
View File
@@ -233,6 +233,19 @@ pub(crate) fn run_internal_command(command: InternalCommands) -> Result<()> {
&args, &args,
) )
} }
InternalCommands::DkmsAutoinstall { rootfs, source } => {
crate::install::dkms::autoinstall(&rootfs, &source)
}
InternalCommands::DkmsRemove {
rootfs,
source,
name,
} => {
if source.is_none() && name.as_deref().is_none_or(|value| value.trim().is_empty()) {
anyhow::bail!("internal dkms-remove requires --source or --name");
}
crate::install::dkms::remove(&rootfs, source.as_deref(), name.as_deref())
}
} }
} }
+547
View File
@@ -0,0 +1,547 @@
//! Runtime support for Depot-managed kernel module source packages.
use anyhow::{Context, Result};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
const DEPOT_KMOD_MANIFEST: &str = ".depot-kmod.toml";
const TRACKING_DIR_REL: &str = "var/lib/depot/kmods";
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct DepotKmodManifest {
name: String,
version: String,
#[serde(default)]
install_dir: String,
#[serde(default)]
make_args: Vec<String>,
#[serde(default)]
pre_build: Vec<String>,
modules: Vec<DepotKmodModule>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct DepotKmodModule {
name: String,
dest_name: String,
build_dir: String,
built_location: String,
install_dir: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct InstalledKmodManifest {
name: String,
version: String,
entries: Vec<InstalledKmodEntry>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct InstalledKmodEntry {
kernel: String,
module: String,
path: String,
}
#[derive(Debug, Clone)]
struct KernelTarget {
version: String,
build_dir: PathBuf,
}
/// Build and install a Depot-managed kernel module source tree for all kernels.
pub(crate) fn autoinstall(rootfs: &Path, source: &Path) -> Result<()> {
let source_dir = resolve_rootfs_path(rootfs, source);
let manifest = read_source_manifest(&source_dir)?;
if manifest.modules.is_empty() {
anyhow::bail!(
"DKMS source manifest contains no modules: {}",
source_dir.display()
);
}
let kernels = discover_kernels(rootfs)?;
if kernels.is_empty() {
crate::log_warn!(
"Skipping Depot DKMS autoinstall for {}/{}: no kernels under {}",
manifest.name,
manifest.version,
rootfs.join("lib/modules").display()
);
return Ok(());
}
let buildable: Vec<_> = kernels
.into_iter()
.filter_map(|kernel| {
if kernel.build_dir.exists() {
Some(kernel)
} else {
crate::log_warn!(
"Skipping kernel {} for {}/{}: missing build directory {}",
kernel.version,
manifest.name,
manifest.version,
kernel.build_dir.display()
);
None
}
})
.collect();
if buildable.is_empty() {
anyhow::bail!(
"No buildable kernels found for {}/{} under {}",
manifest.name,
manifest.version,
rootfs.join("lib/modules").display()
);
}
let mut installed = Vec::new();
for kernel in &buildable {
build_for_kernel(rootfs, &source_dir, &manifest, kernel)?;
installed.extend(install_for_kernel(rootfs, &source_dir, &manifest, kernel)?);
run_depmod(rootfs, &kernel.version)?;
}
write_tracking_manifest(rootfs, &manifest, installed)?;
Ok(())
}
/// Remove Depot-installed kernel modules by source tree or package name.
pub(crate) fn remove(rootfs: &Path, source: Option<&Path>, name: Option<&str>) -> Result<()> {
let selected = selected_tracking_manifests(rootfs, source, name)?;
if selected.is_empty() {
if let Some(name) = name {
crate::log_warn!("No installed Depot DKMS modules found for {}", name);
}
return Ok(());
}
let mut depmod_kernels = BTreeSet::new();
for (manifest_path, manifest) in selected {
for entry in &manifest.entries {
let module_path = rootfs.join(&entry.path);
if module_path.exists() {
fs::remove_file(&module_path).with_context(|| {
format!(
"Failed to remove installed module {}",
module_path.display()
)
})?;
}
depmod_kernels.insert(entry.kernel.clone());
}
fs::remove_file(&manifest_path).with_context(|| {
format!(
"Failed to remove Depot DKMS tracking manifest {}",
manifest_path.display()
)
})?;
}
for kernel in depmod_kernels {
run_depmod(rootfs, &kernel)?;
}
Ok(())
}
fn read_source_manifest(source_dir: &Path) -> Result<DepotKmodManifest> {
let manifest_path = source_dir.join(DEPOT_KMOD_MANIFEST);
let raw = fs::read_to_string(&manifest_path)
.with_context(|| format!("Failed to read {}", manifest_path.display()))?;
toml::from_str(&raw).with_context(|| format!("Failed to parse {}", manifest_path.display()))
}
fn discover_kernels(rootfs: &Path) -> Result<Vec<KernelTarget>> {
let modules_root = rootfs.join("lib/modules");
if !modules_root.exists() {
return Ok(Vec::new());
}
if !modules_root.is_dir() {
anyhow::bail!(
"Kernel modules path is not a directory: {}",
modules_root.display()
);
}
let mut kernels = Vec::new();
for entry in fs::read_dir(&modules_root)
.with_context(|| format!("Failed to read {}", modules_root.display()))?
{
let entry = entry.with_context(|| format!("Failed to list {}", modules_root.display()))?;
let file_type = entry
.file_type()
.with_context(|| format!("Failed to inspect {}", entry.path().display()))?;
if !file_type.is_dir() {
continue;
}
let version = entry.file_name().to_string_lossy().into_owned();
if version.trim().is_empty() {
continue;
}
kernels.push(KernelTarget {
version,
build_dir: entry.path().join("build"),
});
}
kernels.sort_by(|a, b| a.version.cmp(&b.version));
Ok(kernels)
}
fn build_for_kernel(
rootfs: &Path,
source_dir: &Path,
manifest: &DepotKmodManifest,
kernel: &KernelTarget,
) -> Result<()> {
run_pre_build_commands(rootfs, source_dir, manifest, kernel)?;
let build_dirs = module_build_dirs(source_dir, manifest)?;
for build_dir in build_dirs {
crate::log_info!(
"Building Depot DKMS module {}/{} for kernel {}",
manifest.name,
manifest.version,
kernel.version
);
let mut cmd = Command::new("make");
cmd.arg("-C")
.arg(&kernel.build_dir)
.arg(format!("M={}", build_dir.display()))
.arg("modules")
.args(&manifest.make_args);
crate::builder::prepare_tool_command(&mut cmd, &Vec::new());
let status = crate::interrupts::command_status(&mut cmd).with_context(|| {
format!(
"Failed to run kernel module build for {} on {}",
manifest.name, kernel.version
)
})?;
if !status.success() {
anyhow::bail!(
"Kernel module build failed for {}/{} on {}",
manifest.name,
manifest.version,
kernel.version
);
}
}
Ok(())
}
fn run_pre_build_commands(
rootfs: &Path,
source_dir: &Path,
manifest: &DepotKmodManifest,
kernel: &KernelTarget,
) -> Result<()> {
for raw in &manifest.pre_build {
let command = expand_kernel_command(raw, rootfs, source_dir, kernel);
if command.trim().is_empty() {
continue;
}
crate::log_info!(
"Preparing Depot DKMS module {}/{} for kernel {}: {}",
manifest.name,
manifest.version,
kernel.version,
command
);
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(&command).current_dir(source_dir);
crate::builder::prepare_tool_command(&mut cmd, &Vec::new());
let status = crate::interrupts::command_status(&mut cmd).with_context(|| {
format!(
"Failed to run DKMS pre-build command for {} on {}",
manifest.name, kernel.version
)
})?;
if !status.success() {
anyhow::bail!(
"DKMS pre-build command failed for {}/{} on {}: {}",
manifest.name,
manifest.version,
kernel.version,
command
);
}
}
Ok(())
}
fn expand_kernel_command(
raw: &str,
rootfs: &Path,
source_dir: &Path,
kernel: &KernelTarget,
) -> String {
raw.replace("$kernel_build_dir", &shell_path(&kernel.build_dir))
.replace("${kernel_build_dir}", &shell_path(&kernel.build_dir))
.replace("$source_dir", &shell_path(source_dir))
.replace("${source_dir}", &shell_path(source_dir))
.replace("$rootfs", &shell_path(rootfs))
.replace("${rootfs}", &shell_path(rootfs))
.replace("$kernel", &kernel.version)
.replace("${kernel}", &kernel.version)
}
fn shell_path(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
fn module_build_dirs(source_dir: &Path, manifest: &DepotKmodManifest) -> Result<Vec<PathBuf>> {
let mut seen = BTreeSet::new();
let mut dirs = Vec::new();
for module in &manifest.modules {
let rel = safe_rel_path(&module.build_dir)?;
let dir = source_dir.join(rel);
if seen.insert(dir.clone()) {
dirs.push(dir);
}
}
Ok(dirs)
}
fn install_for_kernel(
rootfs: &Path,
source_dir: &Path,
manifest: &DepotKmodManifest,
kernel: &KernelTarget,
) -> Result<Vec<InstalledKmodEntry>> {
let mut entries = Vec::new();
for module in &manifest.modules {
let built_location = source_dir.join(safe_rel_path(&module.built_location)?);
let built_module = built_location.join(format!("{}.ko", module.name));
if !built_module.is_file() {
anyhow::bail!(
"Built module not found for {}/{} on {}: {}",
manifest.name,
module.name,
kernel.version,
built_module.display()
);
}
let install_dir = safe_rel_path(&module.install_dir)?;
let dest_rel = Path::new("lib/modules")
.join(&kernel.version)
.join(install_dir)
.join(format!("{}.ko", module.dest_name));
let dest = rootfs.join(&dest_rel);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
fs::copy(&built_module, &dest).with_context(|| {
format!(
"Failed to install module {} to {}",
built_module.display(),
dest.display()
)
})?;
entries.push(InstalledKmodEntry {
kernel: kernel.version.clone(),
module: module.dest_name.clone(),
path: dest_rel.to_string_lossy().into_owned(),
});
}
Ok(entries)
}
fn run_depmod(rootfs: &Path, kernel: &str) -> Result<()> {
let mut cmd = Command::new("depmod");
if rootfs == Path::new("/") {
cmd.arg("-a").arg(kernel);
} else {
cmd.arg("-b").arg(rootfs).arg(kernel);
}
let status = crate::interrupts::command_status(&mut cmd)
.with_context(|| format!("Failed to run depmod for kernel {}", kernel))?;
if !status.success() {
anyhow::bail!("depmod failed for kernel {}", kernel);
}
Ok(())
}
fn write_tracking_manifest(
rootfs: &Path,
manifest: &DepotKmodManifest,
entries: Vec<InstalledKmodEntry>,
) -> Result<()> {
let tracking_dir = rootfs.join(TRACKING_DIR_REL);
fs::create_dir_all(&tracking_dir)
.with_context(|| format!("Failed to create {}", tracking_dir.display()))?;
let installed = InstalledKmodManifest {
name: manifest.name.clone(),
version: manifest.version.clone(),
entries,
};
let path = tracking_manifest_path(rootfs, &manifest.name, &manifest.version);
let raw = toml::to_string_pretty(&installed)
.context("Failed to serialize Depot DKMS tracking manifest")?;
fs::write(&path, raw).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
fn selected_tracking_manifests(
rootfs: &Path,
source: Option<&Path>,
name: Option<&str>,
) -> Result<Vec<(PathBuf, InstalledKmodManifest)>> {
let mut selected = BTreeMap::new();
if let Some(source) = source {
let source_dir = resolve_rootfs_path(rootfs, source);
if source_dir.join(DEPOT_KMOD_MANIFEST).exists() {
let manifest = read_source_manifest(&source_dir)?;
let tracking = tracking_manifest_path(rootfs, &manifest.name, &manifest.version);
if tracking.exists() {
selected.insert(tracking.clone(), read_tracking_manifest(&tracking)?);
}
}
}
if let Some(name) = name {
for (path, manifest) in tracking_manifests_by_name(rootfs, name)? {
selected.insert(path, manifest);
}
}
Ok(selected.into_iter().collect())
}
fn tracking_manifests_by_name(
rootfs: &Path,
name: &str,
) -> Result<Vec<(PathBuf, InstalledKmodManifest)>> {
let tracking_dir = rootfs.join(TRACKING_DIR_REL);
if !tracking_dir.exists() {
return Ok(Vec::new());
}
let mut manifests = Vec::new();
for entry in fs::read_dir(&tracking_dir)
.with_context(|| format!("Failed to read {}", tracking_dir.display()))?
{
let entry = entry.with_context(|| format!("Failed to list {}", tracking_dir.display()))?;
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
continue;
}
let manifest = read_tracking_manifest(&path)?;
if manifest.name == name {
manifests.push((path, manifest));
}
}
manifests.sort_by(|left, right| left.0.cmp(&right.0));
Ok(manifests)
}
fn read_tracking_manifest(path: &Path) -> Result<InstalledKmodManifest> {
let raw =
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
toml::from_str(&raw).with_context(|| format!("Failed to parse {}", path.display()))
}
fn tracking_manifest_path(rootfs: &Path, name: &str, version: &str) -> PathBuf {
rootfs.join(TRACKING_DIR_REL).join(format!(
"{}-{}.toml",
safe_tracking_component(name),
safe_tracking_component(version)
))
}
fn safe_tracking_component(raw: &str) -> String {
raw.chars()
.map(|ch| match ch {
'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '+' | '-' => ch,
_ => '_',
})
.collect()
}
fn resolve_rootfs_path(rootfs: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
rootfs.join(path.strip_prefix(Path::new("/")).unwrap_or(path))
} else {
rootfs.join(path)
}
}
fn safe_rel_path(raw: &str) -> Result<PathBuf> {
let trimmed = raw.trim().trim_start_matches('/');
if trimmed.is_empty() || trimmed == "." {
return Ok(PathBuf::new());
}
let path = Path::new(trimmed);
if path.is_absolute() {
anyhow::bail!("Expected relative path: {}", raw);
}
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
_ => anyhow::bail!("Unsafe path component in DKMS manifest path: {}", raw),
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::TestEnv;
use tempfile::tempdir;
fn write_tracking(rootfs: &Path) -> Result<PathBuf> {
let path = tracking_manifest_path(rootfs, "zfs", "2.4.3");
fs::create_dir_all(path.parent().unwrap())?;
let manifest = InstalledKmodManifest {
name: "zfs".into(),
version: "2.4.3".into(),
entries: vec![InstalledKmodEntry {
kernel: "6.1.0".into(),
module: "zfs".into(),
path: "lib/modules/6.1.0/updates/depot/zfs.ko".into(),
}],
};
fs::write(&path, toml::to_string(&manifest)?)?;
Ok(path)
}
#[test]
fn remove_uses_tracking_manifest_and_only_removes_tracked_modules() -> Result<()> {
let tmp = tempdir()?;
let rootfs = tmp.path();
let module_path = rootfs.join("lib/modules/6.1.0/updates/depot/zfs.ko");
fs::create_dir_all(module_path.parent().unwrap())?;
fs::write(&module_path, "module")?;
let tracking = write_tracking(rootfs)?;
let depmod = rootfs.join("depmod-bin");
fs::write(&depmod, "#!/bin/sh\nexit 0\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&depmod)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&depmod, perms)?;
}
let mut env = TestEnv::new();
env.set_var("PATH", rootfs);
fs::rename(&depmod, rootfs.join("depmod"))?;
remove(rootfs, None, Some("zfs"))?;
assert!(!module_path.exists());
assert!(!tracking.exists());
Ok(())
}
#[test]
fn safe_rel_path_rejects_traversal() {
assert!(safe_rel_path("../x").is_err());
assert!(safe_rel_path("updates/depot").is_ok());
}
}
+1
View File
@@ -1,4 +1,5 @@
//! Installation helpers. //! Installation helpers.
pub(crate) mod dkms;
pub mod hooks; pub mod hooks;
pub mod scripts; pub mod scripts;
+2
View File
@@ -22,6 +22,7 @@ impl fmt::Display for BuildType {
BuildType::Python => write!(f, "Python"), BuildType::Python => write!(f, "Python"),
BuildType::Rust => write!(f, "Rust"), BuildType::Rust => write!(f, "Rust"),
BuildType::Makefile => write!(f, "Makefile"), BuildType::Makefile => write!(f, "Makefile"),
BuildType::Dkms => write!(f, "DKMS"),
BuildType::Bin => write!(f, "Binary installer"), BuildType::Bin => write!(f, "Binary installer"),
BuildType::Meta => write!(f, "Metapackage"), BuildType::Meta => write!(f, "Metapackage"),
} }
@@ -960,6 +961,7 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
BuildType::Python => "python", BuildType::Python => "python",
BuildType::Rust => "rust", BuildType::Rust => "rust",
BuildType::Makefile => "makefile", BuildType::Makefile => "makefile",
BuildType::Dkms => "dkms",
BuildType::Bin => "bin", BuildType::Bin => "bin",
BuildType::Meta => "meta", BuildType::Meta => "meta",
}; };
+140
View File
@@ -87,6 +87,7 @@ impl PackageSpec {
); );
} }
spec.validate_manual_sources()?; spec.validate_manual_sources()?;
spec.validate_dkms()?;
Ok(spec) Ok(spec)
} }
@@ -166,6 +167,52 @@ impl PackageSpec {
Ok(()) 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. /// Expand variables like `$name` and `$version` in a string.
pub fn expand_vars(&self, input: &str) -> String { pub fn expand_vars(&self, input: &str) -> String {
let specdir = self.spec_dir.to_string_lossy(); let specdir = self.spec_dir.to_string_lossy();
@@ -180,6 +227,56 @@ impl PackageSpec {
&self.source &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. /// Returns true when this spec is a metadata-only package that exists to pull dependencies.
pub fn is_metapackage(&self) -> bool { pub fn is_metapackage(&self) -> bool {
matches!(self.build.build_type, BuildType::Meta) 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 { impl fmt::Display for PackageSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!( writeln!(
+79
View File
@@ -217,10 +217,36 @@ pub enum BuildType {
Python, Python,
Rust, Rust,
Makefile, Makefile,
Dkms,
Bin, Bin,
Meta, Meta,
} }
/// One kernel module output managed by Depot's DKMS-compatible backend.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct DkmsModule {
/// Built module name without a `.ko` suffix.
pub name: String,
/// Installed module name without a `.ko` suffix. Defaults to `name`.
#[serde(default, alias = "dest-name", alias = "dest_name")]
pub dest_name: String,
/// Source subdirectory passed as `M=` to the kernel build system.
#[serde(default, alias = "path", alias = "build-dir", alias = "build_dir")]
pub build_dir: String,
/// Directory containing the resulting `.ko`. Defaults to `build_dir`.
#[serde(
default,
alias = "built-location",
alias = "built_location",
alias = "output-dir",
alias = "output_dir"
)]
pub built_location: String,
/// Install directory under `/lib/modules/<kernel>`. Defaults to `dkms_install_dir`.
#[serde(default, alias = "install-dir", alias = "install_dir")]
pub install_dir: String,
}
/// Build flags and toolchain configuration /// Build flags and toolchain configuration
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct BuildFlags { pub struct BuildFlags {
@@ -715,6 +741,47 @@ pub struct BuildFlags {
/// Binary package type when using BuildType::Bin (e.g. "deb") /// Binary package type when using BuildType::Bin (e.g. "deb")
#[serde(default)] #[serde(default)]
pub binary_type: String, pub binary_type: String,
/// DKMS package/module name for Depot-managed kernel modules.
#[serde(default, alias = "dkms-name", alias = "dkms_name")]
pub dkms_name: String,
/// DKMS package/module version. Defaults to the package version.
#[serde(default, alias = "dkms-version", alias = "dkms_version")]
pub dkms_version: String,
/// Source subdirectory to stage into `/usr/src/<name>-<version>`.
#[serde(default, alias = "dkms-source-dir", alias = "dkms_source_dir")]
pub dkms_source_dir: String,
/// Default install directory under `/lib/modules/<kernel>` for built modules.
#[serde(default, alias = "dkms-install-dir", alias = "dkms_install_dir")]
pub dkms_install_dir: String,
/// Additional arguments appended to kernel module `make` invocations.
#[serde(
default,
alias = "dkms-make-args",
alias = "dkms_make_args",
deserialize_with = "deserialize_string_or_array"
)]
pub dkms_make_args: Vec<String>,
/// Commands run from the staged source tree before each per-kernel module build.
///
/// Supports `$kernel`, `$kernel_build_dir`, `$source_dir`, and `$rootfs`.
#[serde(
default,
alias = "dkms-pre-build",
alias = "dkms_pre_build",
deserialize_with = "deserialize_string_or_array_no_split"
)]
pub dkms_pre_build: Vec<String>,
/// Run Depot's kernel-module build/install hook after package install/update.
#[serde(
default = "default_dkms_autoinstall",
alias = "dkms-autoinstall",
alias = "dkms_autoinstall",
deserialize_with = "deserialize_boolish"
)]
pub dkms_autoinstall: bool,
/// Kernel modules produced by this DKMS source package.
#[serde(default, alias = "dkms-modules", alias = "dkms_modules")]
pub dkms_modules: Vec<DkmsModule>,
/// Internal runtime marker used to adjust builder behavior for the lib32 variant. /// Internal runtime marker used to adjust builder behavior for the lib32 variant.
#[serde(skip)] #[serde(skip)]
pub lib32_variant: bool, pub lib32_variant: bool,
@@ -819,6 +886,14 @@ impl Default for BuildFlags {
source_subdir: String::new(), source_subdir: String::new(),
build_dir: None, build_dir: None,
binary_type: String::new(), binary_type: String::new(),
dkms_name: String::new(),
dkms_version: String::new(),
dkms_source_dir: String::new(),
dkms_install_dir: String::new(),
dkms_make_args: Vec::new(),
dkms_pre_build: Vec::new(),
dkms_autoinstall: default_dkms_autoinstall(),
dkms_modules: Vec::new(),
lib32_variant: false, lib32_variant: false,
host_build_dir: None, host_build_dir: None,
} }
@@ -958,6 +1033,10 @@ fn default_use_lto() -> bool {
true true
} }
fn default_dkms_autoinstall() -> bool {
true
}
fn default_ar() -> String { fn default_ar() -> String {
"ar".to_string() "ar".to_string()
} }
+96
View File
@@ -2379,6 +2379,102 @@ fn docs_package_for_output_derives_name_and_description() {
assert_eq!(docs.version, "1.0"); assert_eq!(docs.version, "1.0");
} }
#[test]
fn parse_dkms_build_type_and_modules() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("zfs.toml");
std::fs::write(
&path,
r#"
[package]
name = "zfs-dkms"
version = "2.4.3"
description = "d"
homepage = "h"
license = "CDDL"
[source]
url = "https://github.com/openzfs/zfs/releases/download/zfs-$version/zfs-$version.tar.gz"
sha256 = "skip"
extract_dir = "zfs-$version"
[build]
type = "dkms"
[build.flags]
dkms_name = "zfs"
dkms_version = "$version"
dkms_source_dir = "."
dkms_install_dir = "updates/depot"
dkms_make_args = ["V=1"]
dkms_pre_build = [
"./configure --with-config=kernel --with-linux=$kernel_build_dir --with-linux-obj=$kernel_build_dir"
]
[[build.flags.dkms_modules]]
name = "zfs"
path = "module"
built_location = "module/zfs"
[[build.flags.dkms_modules]]
name = "spl"
dest_name = "spl_compat"
build_dir = "module"
built_location = "module/spl"
install_dir = "/updates/storage"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert!(matches!(spec.build.build_type, BuildType::Dkms));
assert_eq!(spec.effective_dkms_name(), "zfs");
assert_eq!(spec.effective_dkms_version(), "2.4.3");
assert_eq!(spec.effective_dkms_install_dir(), "updates/depot");
assert_eq!(spec.build.flags.dkms_pre_build.len(), 1);
assert_eq!(spec.build.flags.dkms_modules.len(), 2);
assert_eq!(spec.build.flags.dkms_modules[0].build_dir, "module");
assert_eq!(
spec.effective_dkms_module_install_dir(&spec.build.flags.dkms_modules[1]),
"updates/storage"
);
}
#[test]
fn parse_dkms_rejects_unsafe_module_paths() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("bad.toml");
std::fs::write(
&path,
r#"
[package]
name = "bad-dkms"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
[source]
url = "https://example.com/bad.tar.gz"
sha256 = "skip"
extract_dir = "bad"
[build]
type = "dkms"
[[build.flags.dkms_modules]]
name = "bad"
path = "../outside"
"#,
)
.unwrap();
let err = PackageSpec::from_file(&path).unwrap_err().to_string();
assert!(err.contains("unsafe path component"), "{err}");
}
fn mk_spec(name: &str, version: &str) -> PackageSpec { fn mk_spec(name: &str, version: &str) -> PackageSpec {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {