-
Notifications
You must be signed in to change notification settings - Fork 60
[cli_util] Add base directories #2130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab74f0a
d341626
6af75a3
6057607
8d36992
930b8d1
238bcea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
// 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, | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) : _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**: | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// - `$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. | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// | ||
/// 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. | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// | ||
/// 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only used on Linux, so => (Platform.isLinux ? _environment[dir._xdgKey] : null) ?? path.join(_home, dir._linuxDefault); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ditto, that mixes all OSes together, making the code less readable. |
||
} | ||
} | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
dcharkes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
runtime, | ||
state, | ||
} | ||
|
||
String? _join(String? part1, String? part2) { | ||
if (part1 == null || part2 == null) { | ||
return null; | ||
} | ||
return path.join(part1, part2); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// 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'; | ||
|
||
import 'package:cli_util/cli_util.dart'; | ||
import 'package:path/path.dart' as p; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
final baseDirectories = BaseDirectories('my_app'); | ||
|
||
test('returns a non-empty string', () { | ||
expect(baseDirectories.cacheHome, isNotEmpty); | ||
expect(baseDirectories.configHome, isNotEmpty); | ||
expect(baseDirectories.dataHome, isNotEmpty); | ||
expect(baseDirectories.runtimeHome, isNotEmpty); | ||
expect(baseDirectories.stateHome, isNotEmpty); | ||
}); | ||
|
||
test('has an ancestor folder that exists', () { | ||
void expectAncestorExists(String? path) { | ||
if (path == null) { | ||
// runtimeHome may be undefined on Linux. | ||
return; | ||
} | ||
// We expect that first two segments of the path exist. This is really | ||
// just a dummy check that some part of the path exists. | ||
final ancestorPath = p.joinAll(p.split(path).take(2)); | ||
expect( | ||
Directory(ancestorPath).existsSync(), | ||
isTrue, | ||
); | ||
} | ||
|
||
expectAncestorExists(baseDirectories.cacheHome); | ||
expectAncestorExists(baseDirectories.configHome); | ||
expectAncestorExists(baseDirectories.dataHome); | ||
expectAncestorExists(baseDirectories.runtimeHome); | ||
expectAncestorExists(baseDirectories.stateHome); | ||
}); | ||
|
||
test('empty environment throws exception', () async { | ||
expect( | ||
() => BaseDirectories('Dart', environment: <String, String>{}).configHome, | ||
throwsA(isA<EnvironmentNotFoundException>()), | ||
); | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.