Skip to content

[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

Merged
merged 7 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkgs/cli_util/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.0-wip

- Add `BaseDirectories` class and deprecate `applicationConfigHome`.

## 0.4.2

- Add `sdkPath` getter, deprecate `getSdkPath` function.
Expand Down
7 changes: 5 additions & 2 deletions pkgs/cli_util/lib/cli_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import 'dart:io';

import 'package:path/path.dart' as path;

export 'src/base_directories.dart';

/// The path to the current Dart SDK.
String get sdkPath => path.dirname(path.dirname(Platform.resolvedExecutable));

Expand Down Expand Up @@ -41,6 +43,7 @@ String getSdkPath() => sdkPath;
///
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
@Deprecated('Use BaseDirectories(tool: productName).configHome() instead.')
String applicationConfigHome(String productName) =>
path.join(_configHome, productName);

Expand Down Expand Up @@ -73,8 +76,8 @@ String _requireEnv(String name) =>

/// Exception thrown if a required environment entry does not exist.
///
/// Thrown by [applicationConfigHome] if an expected and required
/// platform specific environment entry is not available.
/// Thrown if an expected and required platform specific environment entry is
/// not available.
class EnvironmentNotFoundException implements Exception {
/// Name of environment entry which was needed, but not found.
final String entryName;
Expand Down
287 changes: 287 additions & 0 deletions pkgs/cli_util/lib/src/base_directories.dart
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,
}) : _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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used on Linux, so path.join(_home, '.local/state') should be fine.
Which also means that these can be stored on the enum as a single string,
making this entire function reduce to

 => (Platform.isLinux ? _environment[dir._xdgKey] : null) ?? path.join(_home, dir._linuxDefault);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be stored on the enum

Ditto, that mixes all OSes together, making the code less readable.

}
}

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);
}
2 changes: 1 addition & 1 deletion pkgs/cli_util/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: cli_util
version: 0.4.2
version: 0.5.0-wip
description: A library to help in building Dart command-line apps.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/cli_util
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acli_util
Expand Down
50 changes: 50 additions & 0 deletions pkgs/cli_util/test/base_directories_test.dart
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>()),
);
});
}
2 changes: 2 additions & 0 deletions pkgs/cli_util/test/cli_util_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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.

// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';
import 'dart:io';

Expand Down