///|
/// A system or global user source and the candidates that it checks. A
/// skipped source has no candidates.
priv struct MachineSource {
source : @config.ConfigSource
candidates : Array[@config.FileTreePath]
}
///|
/// The system and global user sources, in merge order. In isolated mode, or
/// with no platform, each source that the convention set uses reports
/// `Skipped`; with no platform, discovery also adds a note.
fn Conventions::machine_sources(
self : Conventions,
options : DiscoveryOptions,
context : @config.LoadContext,
extensions : Array[String],
notes : Array[@config.ConfigWarning],
) -> Array[MachineSource] raise @config.ConfigError {
let roles : Array[@config.FileRole] = []
if self.has(SystemDir) {
roles.push(System)
}
if self.has(UserConfigDir) || self.has(AppHome) {
roles.push(Global)
}
guard !roles.is_empty() else { return [] }
let platform = if options.isolated { None } else { context.platform }
guard platform is Some(platform) else {
if !options.isolated {
notes.push(
note(
self,
"discovery has no platform, so it skips the system and global user directories",
),
)
}
return roles.map(role => {
{
source: @config.ConfigSource::skipped(File(role), name=self.display),
candidates: [],
}
})
}
let vars : DirVars = {
conventions: self,
platform,
env_vars: context.env_vars,
working_dir: context.working_dir,
notes,
}
let sources = []
for role in roles {
let paths = match role {
System => self.files_in(self.system_path(vars), extensions)
_ => self.global_candidates(vars, extensions)
}
sources.push({
source: @config.ConfigSource::candidates(
texts(paths),
display=self.display,
role~,
name=self.display,
),
candidates: paths,
})
}
sources
}
///|
/// The inputs that give the system and global directories: the convention
/// set, the platform, the variable set of the run, the working directory for a
/// relative value, and the warnings of the run.
priv struct DirVars {
conventions : Conventions
platform : @config.Platform
env_vars : Map[String, String]
working_dir : @config.FileTreePath?
notes : Array[@config.ConfigWarning]
}
///|
/// The system directory of the application.
fn Conventions::system_path(
self : Conventions,
vars : DirVars,
) -> @config.FileTreePath raise @config.ConfigError {
let root = match vars.platform {
Windows =>
match vars.dir("PROGRAMDATA") {
Some(dir) => dir
None => @config.FileTreePath::parse("C:/ProgramData")
}
Linux | MacOs => @config.FileTreePath::parse("/etc")
}
root.child(self.name)
}
///|
/// The global candidates: `/.` in the user configuration
/// directory, then `.` in the application home. They are
/// alternatives at one priority.
fn Conventions::global_candidates(
self : Conventions,
vars : DirVars,
extensions : Array[String],
) -> Array[@config.FileTreePath] raise @config.ConfigError {
let paths = []
if self.has(UserConfigDir) && user_config_root(vars) is Some(root) {
paths.append(self.files_in(root.child(self.name), extensions))
}
if self.has(AppHome) && self.home_path(vars) is Some(home) {
paths.append(self.files_in(home, extensions))
}
paths
}
///|
/// The global user configuration directory, or `None` when the variables
/// that give it are not set.
fn user_config_root(
vars : DirVars,
) -> @config.FileTreePath? raise @config.ConfigError {
match vars.platform {
Windows => vars.dir("APPDATA")
Linux | MacOs => {
// A relative XDG_CONFIG_HOME is invalid, so it falls back to the default.
if vars.path("XDG_CONFIG_HOME") is Some(xdg) && xdg.is_absolute() {
return Some(normalize(xdg))
}
match (vars.dir("HOME"), vars.platform) {
(Some(home), MacOs) => Some(home.join("Library/Application Support"))
(Some(home), _) => Some(home.child(".config"))
(None, _) => None
}
}
}
}
///|
/// The application home: `$_HOME`, else `.` in the user home.
fn Conventions::home_path(
self : Conventions,
vars : DirVars,
) -> @config.FileTreePath? raise @config.ConfigError {
if vars.dir("\{self.prefix}_HOME") is Some(home) {
return Some(home)
}
let user_home = match vars.platform {
Windows => vars.dir("USERPROFILE")
Linux | MacOs => vars.dir("HOME")
}
match user_home {
Some(home) => Some(home.child(".\{self.name}"))
None => None
}
}
///|
/// `.` in `dir` for each searched extension.
fn Conventions::files_in(
self : Conventions,
dir : @config.FileTreePath,
extensions : Array[String],
) -> Array[@config.FileTreePath] raise @config.ConfigError {
extensions.map(extension => dir.child("\{self.name}.\{extension}"))
}
///|
/// The parsed path in the variable `name`, in the native form of the
/// platform, or `None` when it is not set or empty. A value that is not a
/// valid path, such as the drive-relative `C:x` on Windows, counts as not set
/// and gives a `Note` warning.
fn DirVars::path(self : DirVars, name : String) -> @config.FileTreePath? {
guard self.env_vars.get(name) is Some(value) && !value.is_empty() else {
return None
}
Some(@config.FileTreePath::from_native(value, self.platform)) catch {
InvalidFilePath(rule~, ..) => {
let warning = note(
self.conventions,
"discovery ignores the variable \{name}, because \{rule.describe()}",
)
if !self.notes.contains(warning) {
self.notes.push(warning)
}
None
}
_ => None
}
}
///|
/// The directory that the variable `name` gives. An absolute value is
/// normalized. A relative value joins the working directory, when the working
/// directory is absolute.
fn DirVars::dir(
self : DirVars,
name : String,
) -> @config.FileTreePath? raise @config.ConfigError {
guard self.path(name) is Some(value) else { return None }
if value.is_absolute() {
return Some(normalize(value))
}
match self.working_dir {
Some(dir) if dir.is_absolute() => Some(normalize(dir.resolve(value)))
_ => Some(value)
}
}