blob: a8f7304d91812a3a2e2d8a17e9d86d5ea022791e [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:path/path.dart' as path;
import '../cli_util.dart';
/// The standard system paths for a Dart tool.
///
/// These paths respects the following directory standards:
///
/// - On Linux, the [XDG Base Directory
/// Specification](https://specifications.freedesktop.org/basedir-spec/latest/).
/// - On MacOS, the
/// [Library](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
/// directory.
/// - On Windows, `%APPDATA%` and `%LOCALAPPDATA%`.
///
/// Note that [cacheHome], [configHome], [dataHome], [runtimeHome], and
/// [stateHome] may be overlapping or nested.
///
/// Note that the directories won't be created, the methods merely return the
/// recommended locations.
final class BaseDirectories {
/// The name of the Dart tool.
///
/// The name is used to provide a subdirectory inside the base directories.
///
/// This should be a valid directory name on every operating system. The name
/// is typically camel-cased. For example: `"MyApp"`.
final String tool;
/// The environment variables to use to determine the base directories.
///
/// Defaults to [Platform.environment].
final Map<String, String> _environment;
/// Constructs a [BaseDirectories] instance for the given [tool] name.
///
/// The [environment] map, if provided, is used to determine the base
/// directories. If omitted, it defaults to using [Platform.environment].
BaseDirectories(
this.tool, {
Map<String, String>? environment,
}) : _environment = environment ?? Platform.environment;
/// Path of the directory where the tool will place its caches.
///
/// The cache may be purged by the operating system or user at any time.
/// Applications should be able to reconstruct any data stored here. If [tool]
/// cannot handle data being purged, use [runtimeHome] or [dataHome] instead.
///
/// This is a location appropriate for storing non-essential files that may be
/// removed at any point. For example: intermediate compilation artifacts.
///
/// The directory location depends on the current [Platform.operatingSystem]:
/// - on **Windows**:
/// - `%LOCALAPPDATA%\<tool>`
/// - on **Mac OS**:
/// - `$HOME/Library/Caches/<tool>`
/// - on **Linux**:
/// - `$XDG_CACHE_HOME/<tool>` if `$XDG_CACHE_HOME` is defined, and
/// - `$HOME/.cache/<tool>` otherwise.
///
/// The directory won't be created, the method merely returns the recommended
/// location.
///
/// On some platforms, this path may overlap with [runtimeHome] and
/// [stateHome].
///
/// Throws an [EnvironmentNotFoundException] if a necessary environment
/// variable is undefined.
late final String cacheHome =
path.join(_baseDirectory(_XdgBaseDirectoryKind.cache)!, tool);
/// Path of the directory where the tool will place its configuration.
///
/// The configuration may be synchronized across devices by the OS and may
/// survive application removal.
///
/// This is a location appropriate for storing application specific
/// configuration for the current user.
///
/// The directory location depends on the current [Platform.operatingSystem]
/// and what file types are stored:
/// - on **Windows**:
/// - `%APPDATA%\<tool>`
/// - on **Mac OS**:
/// - `$HOME/Library/Application Support/<tool>`
/// - on **Linux**:
/// - `$XDG_CONFIG_HOME/<tool>` if `$XDG_CONFIG_HOME` is defined, and
/// - `$HOME/.config/<tool>` otherwise.
///
/// The directory won't be created, the method merely returns the recommended
/// location.
///
/// On some platforms, this path may overlap with [dataHome].
///
/// Throws an [EnvironmentNotFoundException] if a necessary environment
/// variable is undefined.
late final String configHome =
path.join(_baseDirectory(_XdgBaseDirectoryKind.config)!, tool);
/// Path of the directory where the tool will place its user data.
///
/// The data may be backed up and synchronized to other devices by the
/// operating system. For large data use [stateHome] instead.
///
/// This is a location appropriate for storing application specific
/// data for the current user. For example: documents created by the user.
///
/// The directory location depends on the current [Platform.operatingSystem]:
/// - on **Windows**:
/// - `%APPDATA%\<tool>`
/// - on **Mac OS**:
/// - `$HOME/Library/Application Support/<tool>`
/// - on **Linux**:
/// - `$XDG_DATA_HOME/<tool>` if `$XDG_DATA_HOME` is defined, and
/// - `$HOME/.local/share/<tool>` otherwise.
///
/// The directory won't be created, the method merely returns the recommended
/// location.
///
/// On some platforms, this path may overlap with [configHome] and
/// [stateHome].
///
/// Throws an [EnvironmentNotFoundException] if a necessary environment
/// variable is undefined.
late final String dataHome =
path.join(_baseDirectory(_XdgBaseDirectoryKind.data)!, tool);
/// Path of the directory where the tool will place its runtime data.
///
/// The runtime data may be deleted in between user logins by the OS. For data
/// that needs to persist between sessions, use [stateHome] instead.
///
/// This is a location appropriate for storing runtime data for the current
/// session. For example: undo history.
///
/// This directory might be undefined on Linux, in such case a warning should
/// be printed and a suitable fallback (such as a temporary directory) should
/// be used.
///
/// The directory location depends on the current [Platform.operatingSystem]:
/// - on **Windows**:
/// - `%LOCALAPPDATA%\<tool>`
/// - on **Mac OS**:
/// - `$HOME/Library/Caches/TemporaryItems/<tool>`
/// - on **Linux**:
/// - `$XDG_RUNTIME_HOME/<tool>` if `$XDG_RUNTIME_HOME` is defined, and
/// - `null` otherwise.
///
/// The directory won't be created, the method merely returns the recommended
/// location.
///
/// On some platforms, this path may overlap [cacheHome] and [stateHome] or be
/// nested in [cacheHome].
///
/// Throws an [EnvironmentNotFoundException] if a necessary environment
/// variable is undefined.
late final String? runtimeHome =
_join(_baseDirectory(_XdgBaseDirectoryKind.runtime), tool);
/// Path of the directory where the tool will place its state.
///
/// The state directory is likely not backed up or synchronized accross
/// devices by the OS. For data that may be backed up and synchronized, use
/// [dataHome] instead.
///
/// This is a location appropriate for storing data which is either not
/// important enougn, not small enough, or not portable enough to store in
/// [dataHome]. For example: logs and indices.
///
/// The directory location depends on the current [Platform.operatingSystem]:
/// - on **Windows**:
/// - `%LOCALAPPDATA%\<tool>`
/// - on **Mac OS**:
/// - `$HOME/Library/Application Support/<tool>`
/// - on **Linux**:
/// - `$XDG_STATE_HOME/<tool>` if `$XDG_STATE_HOME` is defined, and
/// - `$HOME/.local/state/<tool>` otherwise.
///
/// The directory won't be created, the method merely returns the recommended
/// location.
///
/// On some platforms, this path may overlap with [cacheHome], and
/// [runtimeHome].
///
/// Throws an [EnvironmentNotFoundException] if a necessary environment
/// variable is undefined.
late final String stateHome =
path.join(_baseDirectory(_XdgBaseDirectoryKind.state)!, tool);
String? _baseDirectory(_XdgBaseDirectoryKind directoryKind) {
if (Platform.isWindows) {
return _baseDirectoryWindows(directoryKind);
}
if (Platform.isMacOS) {
return _baseDirectoryMacOs(directoryKind);
}
if (Platform.isLinux) {
return _baseDirectoryLinux(directoryKind);
}
throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
}
String _baseDirectoryWindows(_XdgBaseDirectoryKind dir) => switch (dir) {
_XdgBaseDirectoryKind.config ||
_XdgBaseDirectoryKind.data =>
_requireEnv('APPDATA'),
_XdgBaseDirectoryKind.cache ||
_XdgBaseDirectoryKind.runtime ||
_XdgBaseDirectoryKind.state =>
_requireEnv('LOCALAPPDATA'),
};
String _baseDirectoryMacOs(_XdgBaseDirectoryKind dir) => switch (dir) {
_XdgBaseDirectoryKind.config ||
// `$HOME/Library/Preferences/` may only contain `.plist` files, so use
// `Application Support` instead.
_XdgBaseDirectoryKind.data ||
_XdgBaseDirectoryKind.state =>
path.join(_home, 'Library', 'Application Support'),
_XdgBaseDirectoryKind.cache => path.join(_home, 'Library', 'Caches'),
_XdgBaseDirectoryKind.runtime =>
// https://stackoverflow.com/a/76799489
path.join(_home, 'Library', 'Caches', 'TemporaryItems'),
};
String? _baseDirectoryLinux(_XdgBaseDirectoryKind dir) {
if (Platform.isLinux) {
final xdgEnv = switch (dir) {
_XdgBaseDirectoryKind.config => 'XDG_CONFIG_HOME',
_XdgBaseDirectoryKind.data => 'XDG_DATA_HOME',
_XdgBaseDirectoryKind.state => 'XDG_STATE_HOME',
_XdgBaseDirectoryKind.cache => 'XDG_CACHE_HOME',
_XdgBaseDirectoryKind.runtime => 'XDG_RUNTIME_DIR',
};
final envVar = _environment[xdgEnv];
if (envVar != null) {
return envVar;
}
}
switch (dir) {
case _XdgBaseDirectoryKind.runtime:
// Applications should chose a different directory and print a warning.
return null;
case _XdgBaseDirectoryKind.cache:
return path.join(_home, '.cache');
case _XdgBaseDirectoryKind.config:
return path.join(_home, '.config');
case _XdgBaseDirectoryKind.data:
return path.join(_home, '.local', 'share');
case _XdgBaseDirectoryKind.state:
return path.join(_home, '.local', 'state');
}
}
String get _home => _requireEnv('HOME');
String _requireEnv(String name) =>
_environment[name] ?? (throw EnvironmentNotFoundException(name));
}
/// A kind from the XDG base directory specification for Linux.
///
/// MacOS and Windows have less kinds.
enum _XdgBaseDirectoryKind {
cache,
config,
data,
// Executables are also mentioned in the XDG spec, but these do not have as
// well defined of locations on Windows and MacOS.
runtime,
state,
}
String? _join(String? part1, String? part2) {
if (part1 == null || part2 == null) {
return null;
}
return path.join(part1, part2);
}