//! Where the kernel tree and the patches live, and the file that can say so. //! //! Everything else asks this module for paths, so the config file and //! `spectral init` touch nothing else. The precedence is the one the roadmap //! promised: the environment wins over the file, and the file wins over the //! built-in default. use std::env; use std::path::{Path, PathBuf}; use std::process::Command; use serde::{Deserialize, Serialize}; use crate::cli::InitArgs; use crate::error::{Error, Result, exit_code}; /// Environment variable that points at the kernel checkout. pub const ENV_KERNEL_TREE: &str = "SPECTRAL_KERNEL"; /// Environment variable that points at the patch directory. pub const ENV_PATCH_DIR: &str = "SPECTRAL_PATCH_DIR"; /// Tree used when nothing else says otherwise, relative to `$HOME`. const DEFAULT_KERNEL_TREE: &str = ".spectral/linux"; /// Where written patches land by default, relative to `$HOME`. const DEFAULT_PATCH_DIR: &str = ".spectral/patches"; /// The file, relative to the config directory. const CONFIG_FILE: &str = "spectral/config.toml"; /// What the config file holds. /// /// Unknown keys are refused rather than ignored: a mistyped key that silently /// does nothing is worse than one that stops the run and says so. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ConfigFile { /// Where the kernel tree is. #[serde(default, skip_serializing_if = "Option::is_none")] pub kernel_tree: Option, /// Where written patches go. #[serde(default, skip_serializing_if = "Option::is_none")] pub patch_dir: Option, /// Addresses every patch is copied to, on top of what get_maintainer says. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub always_cc: Vec, /// Who a patch is sent as. #[serde(default, skip_serializing_if = "Option::is_none")] pub send_email: Option, } /// The `[send_email]` table. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SendEmail { /// `git send-email --from`, when it should differ from git's own identity. #[serde(default, skip_serializing_if = "Option::is_none")] pub from: Option, } /// Where a resolved path came from. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Origin { /// One of the two environment variables. Environment(&'static str), /// The config file. File, /// The built-in default, relative to `$HOME`. Default, } impl Origin { /// How the report names it. #[must_use] pub fn describe(self) -> String { match self { Self::Environment(variable) => format!("from ${variable}"), Self::File => "from the config file".to_owned(), Self::Default => "the built-in default".to_owned(), } } } #[derive(Debug, Clone)] pub struct Config { kernel_tree: PathBuf, patch_dir: PathBuf, tree_origin: Origin, patch_origin: Origin, file: ConfigFile, file_path: PathBuf, } impl Config { /// Resolve both paths: environment first, then the config file, then the /// default under `$HOME`. /// /// The tree is not checked for existence here. Commands that need it call /// [`Config::require_kernel_tree`], which leaves `kernel quest` working /// before anything is cloned. pub fn load() -> Result { let home = home_dir(); let file_path = config_path(&home); let file = read_file(&file_path)?; let (kernel_tree, tree_origin) = resolve( ENV_KERNEL_TREE, file.kernel_tree.clone(), home.join(DEFAULT_KERNEL_TREE), )?; let (patch_dir, patch_origin) = resolve( ENV_PATCH_DIR, file.patch_dir.clone(), home.join(DEFAULT_PATCH_DIR), )?; Ok(Self { kernel_tree, patch_dir, tree_origin, patch_origin, file, file_path, }) } #[must_use] pub fn kernel_tree(&self) -> &Path { &self.kernel_tree } #[must_use] pub fn patch_dir(&self) -> &Path { &self.patch_dir } /// Where the tree path came from, for `doctor` to print. /// /// A path that was resolved for the user and silently turns out to be the /// wrong one is a worse report than one that says which knob set it. #[must_use] pub fn kernel_tree_origin(&self) -> String { self.tree_origin.describe() } /// Where the patch directory came from, for `doctor` to print. #[must_use] pub fn patch_dir_origin(&self) -> String { self.patch_origin.describe() } /// The identity a patch is sent as, when the config file names one. #[must_use] pub fn send_email_from(&self) -> Option { self.file .send_email .as_ref() .and_then(|send_email| send_email.from.clone()) } /// Addresses every patch is copied to. #[must_use] pub fn always_cc(&self) -> &[String] { &self.file.always_cc } /// The config file this run read, whether or not it was there. #[must_use] pub fn file_path(&self) -> &Path { &self.file_path } /// Resolve the tree and check that it really is a kernel source tree. pub fn require_kernel_tree(&self) -> Result<&Path> { if self.kernel_tree.join("scripts/checkpatch.pl").is_file() { Ok(&self.kernel_tree) } else if self.kernel_tree.exists() { Err(Error::NotAKernelTree { path: self.kernel_tree.clone(), }) } else { Err(Error::KernelTreeMissing { path: self.kernel_tree.clone(), env: ENV_KERNEL_TREE, }) } } } /// What `spectral init` did. #[derive(Debug, Clone)] pub struct Init { /// The file that was written, or that already said this. pub config_file: PathBuf, /// The tree the config now names. pub kernel_tree: PathBuf, /// Whether anything was written. pub wrote: bool, } /// `spectral init`: point the config at a tree, verifying or cloning one. /// /// Idempotent on purpose. A second run that would change nothing says so and /// writes nothing; a run that would change an existing answer is refused until /// `--force` says it meant it. pub fn init(args: &InitArgs) -> Result { let home = home_dir(); let config_file = config_path(&home); let current = read_file(&config_file)?; let tree = match (&args.tree, &args.clone) { // The destination is named first: the refusal below has to happen // before a clone runs, not after one has already landed. (tree, Some(_)) => tree .clone() .unwrap_or_else(|| home.join(DEFAULT_KERNEL_TREE)), (Some(tree), None) => tree.clone(), (None, None) => Config::load()?.kernel_tree().to_path_buf(), }; let mut next = current.clone(); next.kernel_tree = Some(tree.clone()); let changed = next != current; if changed && config_file.is_file() && !args.force { return Err(Error::ConfigSaysSomethingElse { path: config_file }); } if let Some(url) = &args.clone { clone_tree(url, &tree, args.depth)?; } verify_tree(&tree)?; if changed { write_file(&config_file, &next)?; } Ok(Init { config_file, kernel_tree: tree, wrote: changed, }) } /// Clone `url` into `into`. /// /// A tree already sitting there is left alone: `init` run twice should not /// re-clone anything. fn clone_tree(url: &str, into: &Path, depth: Option) -> Result<()> { if into.join("scripts/checkpatch.pl").is_file() { return Ok(()); } if let Some(parent) = into.parent() { std::fs::create_dir_all(parent)?; } let mut args = vec!["clone".to_owned()]; if let Some(depth) = depth { args.push("--depth".to_owned()); args.push(depth.to_string()); } args.push(url.to_owned()); args.push(into.display().to_string()); let output = Command::new("git") .args(&args) .output() .map_err(|source| Error::CommandSpawn { program: "git clone".to_owned(), source, })?; if !output.status.success() { return Err(Error::ExternalCommand { program: "git".to_owned(), args: args.join(" "), code: exit_code(&output.status), stderr: String::from_utf8_lossy(&output.stderr) .trim_end() .to_owned(), }); } Ok(()) } /// Check that a path really is a kernel source tree. fn verify_tree(tree: &Path) -> Result<()> { if tree.join("scripts/checkpatch.pl").is_file() { Ok(()) } else if tree.exists() { Err(Error::NotAKernelTree { path: tree.to_path_buf(), }) } else { Err(Error::KernelTreeMissing { path: tree.to_path_buf(), env: ENV_KERNEL_TREE, }) } } /// Read the config file, or hand back an empty one when it is not there. fn read_file(path: &Path) -> Result { let text = match std::fs::read_to_string(path) { Ok(text) => text, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { return Ok(ConfigFile::default()); } Err(error) => return Err(Error::Io(error)), }; toml::from_str(&text).map_err(|source| Error::ConfigUnreadable { path: path.to_path_buf(), source, }) } /// Write the config file, making its directory first. fn write_file(path: &Path, file: &ConfigFile) -> Result<()> { let text = toml::to_string(file).map_err(|source| Error::ConfigUnwritable { source })?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, text)?; Ok(()) } /// Where the config file lives: `$XDG_CONFIG_HOME/spectral/config.toml`, or /// `~/.config/spectral/config.toml` when that variable is unset. #[must_use] pub fn config_path(home: &Path) -> PathBuf { env::var_os("XDG_CONFIG_HOME") .map_or_else(|| home.join(".config"), PathBuf::from) .join(CONFIG_FILE) } /// Pick one path out of the environment, the file, and the default. /// /// A relative path in the file is refused rather than resolved against /// whichever directory the command happened to run in. fn resolve( variable: &'static str, from_file: Option, fallback: PathBuf, ) -> Result<(PathBuf, Origin)> { if let Some(value) = env::var_os(variable) { return Ok((PathBuf::from(value), Origin::Environment(variable))); } if let Some(path) = from_file { if !path.is_absolute() { return Err(Error::ConfigPathRelative { variable, path }); } return Ok((path, Origin::File)); } Ok((fallback, Origin::Default)) } fn home_dir() -> PathBuf { env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from) }