diff options
Diffstat (limited to 'crates/mozart-core/src/composer.rs')
| -rw-r--r-- | crates/mozart-core/src/composer.rs | 578 |
1 files changed, 223 insertions, 355 deletions
diff --git a/crates/mozart-core/src/composer.rs b/crates/mozart-core/src/composer.rs index 64c3c97..287652a 100644 --- a/crates/mozart-core/src/composer.rs +++ b/crates/mozart-core/src/composer.rs @@ -11,116 +11,6 @@ //! `Composer\Command\BaseCommand::tryComposer()` for the upstream contract //! that [`Composer::require`] and [`Composer::try_load`] are modelled on. -use std::path::{Path, PathBuf}; - -use regex::Regex; - -use crate::config::Config; -use crate::factory::create_composer; -use crate::package::RawPackageData; - -/// Return the Composer home directory, respecting `COMPOSER_HOME` and falling -/// back to the platform default using Composer-compatible logic. -/// -/// On Unix: -/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists), -/// prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`). -/// - Always include `$HOME/.composer` as a fallback candidate. -/// - Return the first candidate directory that exists on disk; -/// if none exist, return the first candidate. -pub fn composer_home() -> PathBuf { - if let Ok(val) = std::env::var("COMPOSER_HOME") - && !val.is_empty() - { - return PathBuf::from(val); - } - - #[cfg(target_os = "windows")] - { - if let Ok(appdata) = std::env::var("APPDATA") - && !appdata.is_empty() - { - return PathBuf::from(appdata).join("Composer"); - } - return PathBuf::from("C:/ProgramData/ComposerSetup/bin"); - } - - #[cfg(not(target_os = "windows"))] - { - let home_dir = std::env::var("HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/tmp")); - - let mut candidates: Vec<PathBuf> = Vec::new(); - - if use_xdg() { - let xdg_config = std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home_dir.join(".config")); - candidates.push(xdg_config.join("composer")); - } - - candidates.push(home_dir.join(".composer")); - - // Return first candidate that exists; otherwise return the first - candidates - .iter() - .find(|p| p.is_dir()) - .cloned() - .unwrap_or_else(|| candidates.into_iter().next().unwrap()) - } -} - -#[cfg(not(target_os = "windows"))] -fn use_xdg() -> bool { - std::env::vars().any(|(k, _)| k.starts_with("XDG_")) - || std::path::Path::new("/etc/xdg").is_dir() -} - -/// Project-level Composer state. Mirrors `Composer\PartialComposer` / -/// `Composer\Composer` in PHP, exposing the subset of getters command -/// handlers need today: config, root package, repository manager, -/// installation manager, autoload generator, and locker. More -/// managers (download, …) can be layered on as commands need them. -pub struct Composer { - project_dir: PathBuf, - config: Config, - package: RawPackageData, - repository_manager: RepositoryManager, - installation_manager: InstallationManager, - autoload_generator: AutoloadGenerator, - locker: Locker, -} - -/// Which source the package was installed from. Mirrors -/// `PackageInterface::getInstallationSource` ("source" | "dist"). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InstallationSource { - Source, - Dist, -} - -impl InstallationSource { - /// Parse the `installation-source` field from `installed.json`. - pub fn parse(s: &str) -> Option<Self> { - match s { - "source" => Some(InstallationSource::Source), - "dist" => Some(InstallationSource::Dist), - _ => None, - } - } -} - -/// Source/dist descriptor — mirrors the nested `source`/`dist` objects in -/// `installed.json`. -#[derive(Debug, Clone)] -pub struct PackageReference { - pub kind: String, - pub url: String, - pub reference: Option<String>, - pub shasum: Option<String>, -} - /// Subset of `Composer\Package\PackageInterface` carried through Mozart's /// `LocalRepository`. Holds the fields needed by both the installation /// manager (`prettyName`, `targetDir`) and the status command @@ -214,6 +104,177 @@ impl LocalPackage { } } +/// Which source the package was installed from. Mirrors +/// `PackageInterface::getInstallationSource` ("source" | "dist"). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstallationSource { + Source, + Dist, +} + +impl InstallationSource { + /// Parse the `installation-source` field from `installed.json`. + pub fn parse(s: &str) -> Option<Self> { + match s { + "source" => Some(InstallationSource::Source), + "dist" => Some(InstallationSource::Dist), + _ => None, + } + } +} + +/// Source/dist descriptor — mirrors the nested `source`/`dist` objects in +/// `installed.json`. +#[derive(Debug, Clone)] +pub struct PackageReference { + pub kind: String, + pub url: String, + pub reference: Option<String>, + pub shasum: Option<String>, +} + +/// Mirror of `Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface` +/// and its three concrete implementations. +/// +/// The autoload generator and resolver consult this when deciding +/// whether to emit / enforce a `php`, `ext-*`, `lib-*`, or +/// `composer-*` requirement. For non-platform packages every variant +/// returns `false` — matching PHP's `IgnoreListPlatformRequirementFilter` +/// short-circuiting via `PlatformRepository::isPlatformPackage`. +pub enum PlatformRequirementFilter { + /// `IgnoreNothingPlatformRequirementFilter`. Default. + IgnoreNothing, + /// `IgnoreAllPlatformRequirementFilter` — every platform package is + /// ignored. + IgnoreAll, + /// `IgnoreListPlatformRequirementFilter` — match against an explicit + /// list of names (with `*` glob support). Names suffixed with `+` + /// only suppress the upper bound, mirroring the PHP constructor. + /// `None` for either regex means "no entries" (the corresponding + /// list was empty), short-circuiting to no match. + IgnoreList { + ignore_regex: Option<regex::Regex>, + ignore_upper_bound_regex: Option<regex::Regex>, + }, +} + +impl PlatformRequirementFilter { + /// Mirror of `PlatformRequirementFilterFactory::ignoreNothing`. + pub fn ignore_nothing() -> Self { + PlatformRequirementFilter::IgnoreNothing + } + + /// Mirror of `PlatformRequirementFilterFactory::ignoreAll`. + pub fn ignore_all() -> Self { + PlatformRequirementFilter::IgnoreAll + } + + /// Mirror of `PlatformRequirementFilterFactory::fromBoolOrList` for + /// the list branch. `reqs` accepts entries suffixed with `+` to + /// only ignore the upper bound (`IgnoreListPlatformRequirementFilter`'s + /// constructor splits on the same suffix). + pub fn from_list(reqs: &[String]) -> anyhow::Result<Self> { + let mut ignore_all: Vec<String> = Vec::new(); + let mut ignore_upper_bound: Vec<String> = Vec::new(); + for req in reqs { + if let Some(stripped) = req.strip_suffix('+') { + ignore_upper_bound.push(stripped.to_string()); + } else { + ignore_all.push(req.clone()); + } + } + Ok(PlatformRequirementFilter::IgnoreList { + ignore_regex: package_names_to_regexp(&ignore_all)?, + ignore_upper_bound_regex: package_names_to_regexp(&ignore_upper_bound)?, + }) + } + + /// Mirror of `PlatformRequirementFilterFactory::fromBoolOrList`. + pub fn from_bool_or_list(value: BoolOrList) -> anyhow::Result<Self> { + match value { + BoolOrList::Bool(true) => Ok(Self::ignore_all()), + BoolOrList::Bool(false) => Ok(Self::ignore_nothing()), + BoolOrList::List(list) => Self::from_list(&list), + } + } + + /// Mirror of `PlatformRequirementFilterInterface::isIgnored`. + pub fn is_ignored(&self, req: &str) -> bool { + match self { + PlatformRequirementFilter::IgnoreNothing => false, + PlatformRequirementFilter::IgnoreAll => is_platform_package(req), + PlatformRequirementFilter::IgnoreList { ignore_regex, .. } => { + is_platform_package(req) && ignore_regex.as_ref().is_some_and(|re| re.is_match(req)) + } + } + } + + /// Mirror of `PlatformRequirementFilterInterface::isUpperBoundIgnored`. + pub fn is_upper_bound_ignored(&self, req: &str) -> bool { + match self { + PlatformRequirementFilter::IgnoreNothing => false, + PlatformRequirementFilter::IgnoreAll => is_platform_package(req), + PlatformRequirementFilter::IgnoreList { + ignore_regex, + ignore_upper_bound_regex, + } => { + if !is_platform_package(req) { + return false; + } + ignore_regex.as_ref().is_some_and(|re| re.is_match(req)) + || ignore_upper_bound_regex + .as_ref() + .is_some_and(|re| re.is_match(req)) + } + } + } +} + +/// Helper accepted by [`PlatformRequirementFilter::from_bool_or_list`] +/// — mirrors PHP's `bool|string[]` union by replacing it with a tagged +/// enum at the boundary. Commands typically have an +/// `--ignore-platform-reqs` flag (the `Bool` arm) plus an optional +/// `--ignore-platform-req <name>` list (the `List` arm), and convert at +/// the call site. +pub enum BoolOrList { + Bool(bool), + List(Vec<String>), +} + +/// Compile a list of package names (with `*` glob support) into a +/// case-insensitive regex matching any of them. Mirrors +/// `BasePackage::packageNamesToRegexp` and its `packageNameToRegexp` +/// helper: each name is `preg_quote`'d, then `\*` becomes `.*`. +/// +/// Returns `None` when `names` is empty — Rust's `regex` crate refuses +/// regexes that never match, so we model "match nothing" as the +/// absence of a compiled regex and short-circuit at the call site. +fn package_names_to_regexp(names: &[String]) -> anyhow::Result<Option<regex::Regex>> { + if names.is_empty() { + return Ok(None); + } + let parts: Vec<String> = names + .iter() + .map(|n| regex::escape(n).replace("\\*", ".*")) + .collect(); + let pattern = format!("(?i)^(?:{})$", parts.join("|")); + Ok(Some(regex::Regex::new(&pattern)?)) +} + +/// Mirror of `Composer\Repository\PlatformRepository::isPlatformPackage` +/// using the same canonical regex (`PLATFORM_PACKAGE_REGEX`). +fn is_platform_package(name: &str) -> bool { + use std::sync::OnceLock; + static RE: OnceLock<regex::Regex> = OnceLock::new(); + let re = RE.get_or_init(|| { + regex::Regex::new( + r"(?i)^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer(?:-(?:plugin|runtime)-api)?)$", + ) + .expect("PLATFORM_PACKAGE_REGEX compiles") + }); + re.is_match(name) +} + /// In-memory mirror of `Composer\Repository\InstalledFilesystemRepository` /// (`vendor/composer/installed.json`). Carries enough information for /// commands that walk the local install (currently: `dump-autoload`). @@ -281,18 +342,18 @@ impl RepositoryManager { /// installer plugin chain Mozart only supports the `LibraryInstaller` /// behaviour (`vendor-dir/<pretty-name>(/<target-dir>)`). pub struct InstallationManager { - vendor_dir: PathBuf, + vendor_dir: std::path::PathBuf, } impl InstallationManager { - pub fn new(vendor_dir: PathBuf) -> Self { + pub fn new(vendor_dir: std::path::PathBuf) -> Self { Self { vendor_dir } } /// Resolved absolute path of the vendor directory. Not on PHP's /// `InstallationManager`, but the autoload generator needs it /// without the round-trip through `Config::get('vendor-dir')`. - pub fn vendor_dir(&self) -> &Path { + pub fn vendor_dir(&self) -> &std::path::Path { &self.vendor_dir } @@ -300,7 +361,7 @@ impl InstallationManager { /// path on disk where a package's code is expected to live. Returns /// `None` when the package has nothing on disk (metapackages); for /// regular library packages this matches `LibraryInstaller::getInstallPath`. - pub fn get_install_path(&self, package: &LocalPackage) -> Option<PathBuf> { + pub fn get_install_path(&self, package: &LocalPackage) -> Option<std::path::PathBuf> { let mut path = self.vendor_dir.join(package.pretty_name()); if let Some(td) = package.target_dir() { path = path.join(td); @@ -391,148 +452,6 @@ impl Default for AutoloadDumpOptions { } } -/// Mirror of `Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface` -/// and its three concrete implementations. -/// -/// The autoload generator and resolver consult this when deciding -/// whether to emit / enforce a `php`, `ext-*`, `lib-*`, or -/// `composer-*` requirement. For non-platform packages every variant -/// returns `false` — matching PHP's `IgnoreListPlatformRequirementFilter` -/// short-circuiting via `PlatformRepository::isPlatformPackage`. -pub enum PlatformRequirementFilter { - /// `IgnoreNothingPlatformRequirementFilter`. Default. - IgnoreNothing, - /// `IgnoreAllPlatformRequirementFilter` — every platform package is - /// ignored. - IgnoreAll, - /// `IgnoreListPlatformRequirementFilter` — match against an explicit - /// list of names (with `*` glob support). Names suffixed with `+` - /// only suppress the upper bound, mirroring the PHP constructor. - /// `None` for either regex means "no entries" (the corresponding - /// list was empty), short-circuiting to no match. - IgnoreList { - ignore_regex: Option<Regex>, - ignore_upper_bound_regex: Option<Regex>, - }, -} - -impl PlatformRequirementFilter { - /// Mirror of `PlatformRequirementFilterFactory::ignoreNothing`. - pub fn ignore_nothing() -> Self { - PlatformRequirementFilter::IgnoreNothing - } - - /// Mirror of `PlatformRequirementFilterFactory::ignoreAll`. - pub fn ignore_all() -> Self { - PlatformRequirementFilter::IgnoreAll - } - - /// Mirror of `PlatformRequirementFilterFactory::fromBoolOrList` for - /// the list branch. `reqs` accepts entries suffixed with `+` to - /// only ignore the upper bound (`IgnoreListPlatformRequirementFilter`'s - /// constructor splits on the same suffix). - pub fn from_list(reqs: &[String]) -> anyhow::Result<Self> { - let mut ignore_all: Vec<String> = Vec::new(); - let mut ignore_upper_bound: Vec<String> = Vec::new(); - for req in reqs { - if let Some(stripped) = req.strip_suffix('+') { - ignore_upper_bound.push(stripped.to_string()); - } else { - ignore_all.push(req.clone()); - } - } - Ok(PlatformRequirementFilter::IgnoreList { - ignore_regex: package_names_to_regexp(&ignore_all)?, - ignore_upper_bound_regex: package_names_to_regexp(&ignore_upper_bound)?, - }) - } - - /// Mirror of `PlatformRequirementFilterFactory::fromBoolOrList`. - pub fn from_bool_or_list(value: BoolOrList) -> anyhow::Result<Self> { - match value { - BoolOrList::Bool(true) => Ok(Self::ignore_all()), - BoolOrList::Bool(false) => Ok(Self::ignore_nothing()), - BoolOrList::List(list) => Self::from_list(&list), - } - } - - /// Mirror of `PlatformRequirementFilterInterface::isIgnored`. - pub fn is_ignored(&self, req: &str) -> bool { - match self { - PlatformRequirementFilter::IgnoreNothing => false, - PlatformRequirementFilter::IgnoreAll => is_platform_package(req), - PlatformRequirementFilter::IgnoreList { ignore_regex, .. } => { - is_platform_package(req) && ignore_regex.as_ref().is_some_and(|re| re.is_match(req)) - } - } - } - - /// Mirror of `PlatformRequirementFilterInterface::isUpperBoundIgnored`. - pub fn is_upper_bound_ignored(&self, req: &str) -> bool { - match self { - PlatformRequirementFilter::IgnoreNothing => false, - PlatformRequirementFilter::IgnoreAll => is_platform_package(req), - PlatformRequirementFilter::IgnoreList { - ignore_regex, - ignore_upper_bound_regex, - } => { - if !is_platform_package(req) { - return false; - } - ignore_regex.as_ref().is_some_and(|re| re.is_match(req)) - || ignore_upper_bound_regex - .as_ref() - .is_some_and(|re| re.is_match(req)) - } - } - } -} - -/// Helper accepted by [`PlatformRequirementFilter::from_bool_or_list`] -/// — mirrors PHP's `bool|string[]` union by replacing it with a tagged -/// enum at the boundary. Commands typically have an -/// `--ignore-platform-reqs` flag (the `Bool` arm) plus an optional -/// `--ignore-platform-req <name>` list (the `List` arm), and convert at -/// the call site. -pub enum BoolOrList { - Bool(bool), - List(Vec<String>), -} - -/// Compile a list of package names (with `*` glob support) into a -/// case-insensitive regex matching any of them. Mirrors -/// `BasePackage::packageNamesToRegexp` and its `packageNameToRegexp` -/// helper: each name is `preg_quote`'d, then `\*` becomes `.*`. -/// -/// Returns `None` when `names` is empty — Rust's `regex` crate refuses -/// regexes that never match, so we model "match nothing" as the -/// absence of a compiled regex and short-circuit at the call site. -fn package_names_to_regexp(names: &[String]) -> anyhow::Result<Option<Regex>> { - if names.is_empty() { - return Ok(None); - } - let parts: Vec<String> = names - .iter() - .map(|n| regex::escape(n).replace("\\*", ".*")) - .collect(); - let pattern = format!("(?i)^(?:{})$", parts.join("|")); - Ok(Some(Regex::new(&pattern)?)) -} - -/// Mirror of `Composer\Repository\PlatformRepository::isPlatformPackage` -/// using the same canonical regex (`PLATFORM_PACKAGE_REGEX`). -fn is_platform_package(name: &str) -> bool { - use std::sync::OnceLock; - static RE: OnceLock<Regex> = OnceLock::new(); - let re = RE.get_or_init(|| { - Regex::new( - r"(?i)^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer(?:-(?:plugin|runtime)-api)?)$", - ) - .expect("PLATFORM_PACKAGE_REGEX compiles") - }); - re.is_match(name) -} - /// Mirror of `Composer\Package\Locker`. The full PHP class is a thick /// wrapper around `composer.lock` (lock-data dump/load, freshness /// check, dev-package tracking, …) — Mozart's port currently just @@ -540,17 +459,17 @@ fn is_platform_package(name: &str) -> bool { /// generator needs (`isLocked()` / `getLockData()['content-hash']`). /// The richer accessors will land as more commands are ported. pub struct Locker { - lock_file_path: PathBuf, + lock_file_path: std::path::PathBuf, } impl Locker { - pub fn new(lock_file_path: PathBuf) -> Self { + pub fn new(lock_file_path: std::path::PathBuf) -> Self { Self { lock_file_path } } /// Path to the underlying `composer.lock`. Mirrors /// `Locker::getJsonFile()->getPath()`. - pub fn lock_file_path(&self) -> &Path { + pub fn lock_file_path(&self) -> &std::path::Path { &self.lock_file_path } @@ -591,111 +510,60 @@ pub struct LockData { pub content_hash: String, } -impl Composer { - /// All-args constructor used by [`crate::factory::create_composer`]. - /// Mirrors the PHP pattern of `new Composer()` followed by - /// `setConfig` / `setPackage` / `setRepositoryManager` / - /// `setInstallationManager` / `setAutoloadGenerator` / `setLocker`, - /// collapsed into a single immutable build. - pub fn new( - project_dir: PathBuf, - config: Config, - package: RawPackageData, - repository_manager: RepositoryManager, - installation_manager: InstallationManager, - autoload_generator: AutoloadGenerator, - locker: Locker, - ) -> Self { - Self { - project_dir, - config, - package, - repository_manager, - installation_manager, - autoload_generator, - locker, - } - } - - /// Load Composer state for `project_dir`, requiring a composer.json. - /// Mirrors `BaseCommand::requireComposer()`, which delegates to - /// `Factory::createComposer` after asserting the file exists. - pub fn require(project_dir: impl Into<PathBuf>) -> anyhow::Result<Self> { - let project_dir = project_dir.into(); - let composer_json = project_dir.join("composer.json"); - if !composer_json.exists() { - anyhow::bail!( - "Composer could not find a composer.json file in {}", - project_dir.display() - ); - } - create_composer(project_dir, &composer_json) +/// Return the Composer home directory, respecting `COMPOSER_HOME` and falling +/// back to the platform default using Composer-compatible logic. +/// +/// On Unix: +/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists), +/// prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`). +/// - Always include `$HOME/.composer` as a fallback candidate. +/// - Return the first candidate directory that exists on disk; +/// if none exist, return the first candidate. +pub fn composer_home() -> std::path::PathBuf { + if let Ok(val) = std::env::var("COMPOSER_HOME") + && !val.is_empty() + { + return std::path::PathBuf::from(val); } - /// Load Composer state for `project_dir`, returning `None` if no - /// composer.json exists. Other I/O or parse errors still propagate. - /// Mirrors `BaseCommand::tryComposer()`. - pub fn try_load(project_dir: impl Into<PathBuf>) -> anyhow::Result<Option<Self>> { - let project_dir = project_dir.into(); - let composer_json = project_dir.join("composer.json"); - if !composer_json.exists() { - return Ok(None); + #[cfg(target_os = "windows")] + { + if let Ok(appdata) = std::env::var("APPDATA") + && !appdata.is_empty() + { + return std::path::PathBuf::from(appdata).join("Composer"); } - create_composer(project_dir, &composer_json).map(Some) - } - - /// Load Composer state keyed on a specific `composer.json` file, deriving - /// the project directory from `file.parent()`. Mirrors - /// `ValidateCommand::createComposerInstance($file)` — Composer keys - /// instances on a file rather than a directory for non-default paths. - pub fn try_load_from_file(file: &Path) -> anyhow::Result<Option<Self>> { - let project_dir = file - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| PathBuf::from(".")); - Self::try_load(project_dir) + return std::path::PathBuf::from("C:/ProgramData/ComposerSetup/bin"); } - pub fn project_dir(&self) -> &Path { - &self.project_dir - } - - pub fn config(&self) -> &Config { - &self.config - } + #[cfg(not(target_os = "windows"))] + { + let home_dir = std::env::var("HOME") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); - /// Root package loaded from the project's `composer.json`. Mirrors - /// `Composer::getPackage()`; ideally this would return a fully - /// resolved `RootPackageInterface` equivalent, but Mozart does not - /// yet have a `RootPackageLoader` port — for now callers see the - /// raw, pre-normalised JSON shape. - pub fn package(&self) -> &RawPackageData { - &self.package - } + let mut candidates = Vec::new(); - /// Mirror of `Composer::getRepositoryManager()`. - pub fn repository_manager(&self) -> &RepositoryManager { - &self.repository_manager - } + if use_xdg() { + let xdg_config = std::env::var("XDG_CONFIG_HOME") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| home_dir.join(".config")); + candidates.push(xdg_config.join("composer")); + } - /// Mirror of `Composer::getInstallationManager()`. - pub fn installation_manager(&self) -> &InstallationManager { - &self.installation_manager - } + candidates.push(home_dir.join(".composer")); - /// Mirror of `Composer::getAutoloadGenerator()`. - /// - /// Returned by shared reference because Mozart's - /// [`AutoloadGenerator`] is stateless — per-call toggles live on - /// [`AutoloadDumpOptions`] passed into `dump()`, not on the - /// generator itself. Diverges from PHP's - /// `$composer->getAutoloadGenerator()->setDryRun(...)` chain. - pub fn autoload_generator(&self) -> &AutoloadGenerator { - &self.autoload_generator + // Return first candidate that exists; otherwise return the first + candidates + .iter() + .find(|p| p.is_dir()) + .cloned() + .unwrap_or_else(|| candidates.into_iter().next().unwrap()) } +} - /// Mirror of `Composer::getLocker()`. - pub fn locker(&self) -> &Locker { - &self.locker - } +#[cfg(not(target_os = "windows"))] +fn use_xdg() -> bool { + std::env::vars().any(|(k, _)| k.starts_with("XDG_")) + || std::path::Path::new("/etc/xdg").is_dir() } |
