Skip to content

Commit a4335eb

Browse files
authored
[cli_util] Add base directories (#2130)
1 parent e1b1b4c commit a4335eb

File tree

6 files changed

+349
-3
lines changed

6 files changed

+349
-3
lines changed

pkgs/cli_util/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.5.0-wip
2+
3+
- Add `BaseDirectories` class and deprecate `applicationConfigHome`.
4+
15
## 0.4.2
26

37
- Add `sdkPath` getter, deprecate `getSdkPath` function.

pkgs/cli_util/lib/cli_util.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'dart:io';
1010

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

13+
export 'src/base_directories.dart';
14+
1315
/// The path to the current Dart SDK.
1416
String get sdkPath => path.dirname(path.dirname(Platform.resolvedExecutable));
1517

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

@@ -73,8 +76,8 @@ String _requireEnv(String name) =>
7376

7477
/// Exception thrown if a required environment entry does not exist.
7578
///
76-
/// Thrown by [applicationConfigHome] if an expected and required
77-
/// platform specific environment entry is not available.
79+
/// Thrown if an expected and required platform specific environment entry is
80+
/// not available.
7881
class EnvironmentNotFoundException implements Exception {
7982
/// Name of environment entry which was needed, but not found.
8083
final String entryName;
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io' show Platform;
6+
7+
import 'package:path/path.dart' as path;
8+
9+
import '../cli_util.dart';
10+
11+
/// The standard system paths for a Dart tool.
12+
///
13+
/// These paths respects the following directory standards:
14+
///
15+
/// - On Linux, the [XDG Base Directory
16+
/// Specification](https://specifications.freedesktop.org/basedir-spec/latest/).
17+
/// - On MacOS, the
18+
/// [Library](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
19+
/// directory.
20+
/// - On Windows, `%APPDATA%` and `%LOCALAPPDATA%`.
21+
///
22+
/// Note that [cacheHome], [configHome], [dataHome], [runtimeHome], and
23+
/// [stateHome] may be overlapping or nested.
24+
///
25+
/// Note that the directories won't be created, the methods merely return the
26+
/// recommended locations.
27+
final class BaseDirectories {
28+
/// The name of the Dart tool.
29+
///
30+
/// The name is used to provide a subdirectory inside the base directories.
31+
///
32+
/// This should be a valid directory name on every operating system. The name
33+
/// is typically camel-cased. For example: `"MyApp"`.
34+
final String tool;
35+
36+
/// The environment variables to use to determine the base directories.
37+
///
38+
/// Defaults to [Platform.environment].
39+
final Map<String, String> _environment;
40+
41+
/// Constructs a [BaseDirectories] instance for the given [tool] name.
42+
///
43+
/// The [environment] map, if provided, is used to determine the base
44+
/// directories. If omitted, it defaults to using [Platform.environment].
45+
BaseDirectories(
46+
this.tool, {
47+
Map<String, String>? environment,
48+
}) : _environment = environment ?? Platform.environment;
49+
50+
/// Path of the directory where the tool will place its caches.
51+
///
52+
/// The cache may be purged by the operating system or user at any time.
53+
/// Applications should be able to reconstruct any data stored here. If [tool]
54+
/// cannot handle data being purged, use [runtimeHome] or [dataHome] instead.
55+
///
56+
/// This is a location appropriate for storing non-essential files that may be
57+
/// removed at any point. For example: intermediate compilation artifacts.
58+
///
59+
/// The directory location depends on the current [Platform.operatingSystem]:
60+
/// - on **Windows**:
61+
/// - `%LOCALAPPDATA%\<tool>`
62+
/// - on **Mac OS**:
63+
/// - `$HOME/Library/Caches/<tool>`
64+
/// - on **Linux**:
65+
/// - `$XDG_CACHE_HOME/<tool>` if `$XDG_CACHE_HOME` is defined, and
66+
/// - `$HOME/.cache/<tool>` otherwise.
67+
///
68+
/// The directory won't be created, the method merely returns the recommended
69+
/// location.
70+
///
71+
/// On some platforms, this path may overlap with [runtimeHome] and
72+
/// [stateHome].
73+
///
74+
/// Throws an [EnvironmentNotFoundException] if a necessary environment
75+
/// variable is undefined.
76+
late final String cacheHome =
77+
path.join(_baseDirectory(_XdgBaseDirectoryKind.cache)!, tool);
78+
79+
/// Path of the directory where the tool will place its configuration.
80+
///
81+
/// The configuration may be synchronized across devices by the OS and may
82+
/// survive application removal.
83+
///
84+
/// This is a location appropriate for storing application specific
85+
/// configuration for the current user.
86+
///
87+
/// The directory location depends on the current [Platform.operatingSystem]
88+
/// and what file types are stored:
89+
/// - on **Windows**:
90+
/// - `%APPDATA%\<tool>`
91+
/// - on **Mac OS**:
92+
/// - `$HOME/Library/Application Support/<tool>`
93+
/// - on **Linux**:
94+
/// - `$XDG_CONFIG_HOME/<tool>` if `$XDG_CONFIG_HOME` is defined, and
95+
/// - `$HOME/.config/<tool>` otherwise.
96+
///
97+
/// The directory won't be created, the method merely returns the recommended
98+
/// location.
99+
///
100+
/// On some platforms, this path may overlap with [dataHome].
101+
///
102+
/// Throws an [EnvironmentNotFoundException] if a necessary environment
103+
/// variable is undefined.
104+
late final String configHome =
105+
path.join(_baseDirectory(_XdgBaseDirectoryKind.config)!, tool);
106+
107+
/// Path of the directory where the tool will place its user data.
108+
///
109+
/// The data may be backed up and synchronized to other devices by the
110+
/// operating system. For large data use [stateHome] instead.
111+
///
112+
/// This is a location appropriate for storing application specific
113+
/// data for the current user. For example: documents created by the user.
114+
///
115+
/// The directory location depends on the current [Platform.operatingSystem]:
116+
/// - on **Windows**:
117+
/// - `%APPDATA%\<tool>`
118+
/// - on **Mac OS**:
119+
/// - `$HOME/Library/Application Support/<tool>`
120+
/// - on **Linux**:
121+
/// - `$XDG_DATA_HOME/<tool>` if `$XDG_DATA_HOME` is defined, and
122+
/// - `$HOME/.local/share/<tool>` otherwise.
123+
///
124+
/// The directory won't be created, the method merely returns the recommended
125+
/// location.
126+
///
127+
/// On some platforms, this path may overlap with [configHome] and
128+
/// [stateHome].
129+
///
130+
/// Throws an [EnvironmentNotFoundException] if a necessary environment
131+
/// variable is undefined.
132+
late final String dataHome =
133+
path.join(_baseDirectory(_XdgBaseDirectoryKind.data)!, tool);
134+
135+
/// Path of the directory where the tool will place its runtime data.
136+
///
137+
/// The runtime data may be deleted in between user logins by the OS. For data
138+
/// that needs to persist between sessions, use [stateHome] instead.
139+
///
140+
/// This is a location appropriate for storing runtime data for the current
141+
/// session. For example: undo history.
142+
///
143+
/// This directory might be undefined on Linux, in such case a warning should
144+
/// be printed and a suitable fallback (such as a temporary directory) should
145+
/// be used.
146+
///
147+
/// The directory location depends on the current [Platform.operatingSystem]:
148+
/// - on **Windows**:
149+
/// - `%LOCALAPPDATA%\<tool>`
150+
/// - on **Mac OS**:
151+
/// - `$HOME/Library/Caches/TemporaryItems/<tool>`
152+
/// - on **Linux**:
153+
/// - `$XDG_RUNTIME_HOME/<tool>` if `$XDG_RUNTIME_HOME` is defined, and
154+
/// - `null` otherwise.
155+
///
156+
/// The directory won't be created, the method merely returns the recommended
157+
/// location.
158+
///
159+
/// On some platforms, this path may overlap [cacheHome] and [stateHome] or be
160+
/// nested in [cacheHome].
161+
///
162+
/// Throws an [EnvironmentNotFoundException] if a necessary environment
163+
/// variable is undefined.
164+
late final String? runtimeHome =
165+
_join(_baseDirectory(_XdgBaseDirectoryKind.runtime), tool);
166+
167+
/// Path of the directory where the tool will place its state.
168+
///
169+
/// The state directory is likely not backed up or synchronized accross
170+
/// devices by the OS. For data that may be backed up and synchronized, use
171+
/// [dataHome] instead.
172+
///
173+
/// This is a location appropriate for storing data which is either not
174+
/// important enougn, not small enough, or not portable enough to store in
175+
/// [dataHome]. For example: logs and indices.
176+
///
177+
/// The directory location depends on the current [Platform.operatingSystem]:
178+
/// - on **Windows**:
179+
/// - `%LOCALAPPDATA%\<tool>`
180+
/// - on **Mac OS**:
181+
/// - `$HOME/Library/Application Support/<tool>`
182+
/// - on **Linux**:
183+
/// - `$XDG_STATE_HOME/<tool>` if `$XDG_STATE_HOME` is defined, and
184+
/// - `$HOME/.local/state/<tool>` otherwise.
185+
///
186+
/// The directory won't be created, the method merely returns the recommended
187+
/// location.
188+
///
189+
/// On some platforms, this path may overlap with [cacheHome], and
190+
/// [runtimeHome].
191+
///
192+
/// Throws an [EnvironmentNotFoundException] if a necessary environment
193+
/// variable is undefined.
194+
late final String stateHome =
195+
path.join(_baseDirectory(_XdgBaseDirectoryKind.state)!, tool);
196+
197+
String? _baseDirectory(_XdgBaseDirectoryKind directoryKind) {
198+
if (Platform.isWindows) {
199+
return _baseDirectoryWindows(directoryKind);
200+
}
201+
if (Platform.isMacOS) {
202+
return _baseDirectoryMacOs(directoryKind);
203+
}
204+
if (Platform.isLinux) {
205+
return _baseDirectoryLinux(directoryKind);
206+
}
207+
throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
208+
}
209+
210+
String _baseDirectoryWindows(_XdgBaseDirectoryKind dir) => switch (dir) {
211+
_XdgBaseDirectoryKind.config ||
212+
_XdgBaseDirectoryKind.data =>
213+
_requireEnv('APPDATA'),
214+
_XdgBaseDirectoryKind.cache ||
215+
_XdgBaseDirectoryKind.runtime ||
216+
_XdgBaseDirectoryKind.state =>
217+
_requireEnv('LOCALAPPDATA'),
218+
};
219+
220+
String _baseDirectoryMacOs(_XdgBaseDirectoryKind dir) => switch (dir) {
221+
_XdgBaseDirectoryKind.config ||
222+
// `$HOME/Library/Preferences/` may only contain `.plist` files, so use
223+
// `Application Support` instead.
224+
_XdgBaseDirectoryKind.data ||
225+
_XdgBaseDirectoryKind.state =>
226+
path.join(_home, 'Library', 'Application Support'),
227+
_XdgBaseDirectoryKind.cache => path.join(_home, 'Library', 'Caches'),
228+
_XdgBaseDirectoryKind.runtime =>
229+
// https://stackoverflow.com/a/76799489
230+
path.join(_home, 'Library', 'Caches', 'TemporaryItems'),
231+
};
232+
233+
String? _baseDirectoryLinux(_XdgBaseDirectoryKind dir) {
234+
if (Platform.isLinux) {
235+
final xdgEnv = switch (dir) {
236+
_XdgBaseDirectoryKind.config => 'XDG_CONFIG_HOME',
237+
_XdgBaseDirectoryKind.data => 'XDG_DATA_HOME',
238+
_XdgBaseDirectoryKind.state => 'XDG_STATE_HOME',
239+
_XdgBaseDirectoryKind.cache => 'XDG_CACHE_HOME',
240+
_XdgBaseDirectoryKind.runtime => 'XDG_RUNTIME_DIR',
241+
};
242+
final envVar = _environment[xdgEnv];
243+
if (envVar != null) {
244+
return envVar;
245+
}
246+
}
247+
248+
switch (dir) {
249+
case _XdgBaseDirectoryKind.runtime:
250+
// Applications should chose a different directory and print a warning.
251+
return null;
252+
case _XdgBaseDirectoryKind.cache:
253+
return path.join(_home, '.cache');
254+
case _XdgBaseDirectoryKind.config:
255+
return path.join(_home, '.config');
256+
case _XdgBaseDirectoryKind.data:
257+
return path.join(_home, '.local', 'share');
258+
case _XdgBaseDirectoryKind.state:
259+
return path.join(_home, '.local', 'state');
260+
}
261+
}
262+
263+
String get _home => _requireEnv('HOME');
264+
265+
String _requireEnv(String name) =>
266+
_environment[name] ?? (throw EnvironmentNotFoundException(name));
267+
}
268+
269+
/// A kind from the XDG base directory specification for Linux.
270+
///
271+
/// MacOS and Windows have less kinds.
272+
enum _XdgBaseDirectoryKind {
273+
cache,
274+
config,
275+
data,
276+
// Executables are also mentioned in the XDG spec, but these do not have as
277+
// well defined of locations on Windows and MacOS.
278+
runtime,
279+
state,
280+
}
281+
282+
String? _join(String? part1, String? part2) {
283+
if (part1 == null || part2 == null) {
284+
return null;
285+
}
286+
return path.join(part1, part2);
287+
}

pkgs/cli_util/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: cli_util
2-
version: 0.4.2
2+
version: 0.5.0-wip
33
description: A library to help in building Dart command-line apps.
44
repository: https://github.com/dart-lang/tools/tree/main/pkgs/cli_util
55
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acli_util
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:cli_util/cli_util.dart';
8+
import 'package:path/path.dart' as p;
9+
import 'package:test/test.dart';
10+
11+
void main() {
12+
final baseDirectories = BaseDirectories('my_app');
13+
14+
test('returns a non-empty string', () {
15+
expect(baseDirectories.cacheHome, isNotEmpty);
16+
expect(baseDirectories.configHome, isNotEmpty);
17+
expect(baseDirectories.dataHome, isNotEmpty);
18+
expect(baseDirectories.runtimeHome, isNotEmpty);
19+
expect(baseDirectories.stateHome, isNotEmpty);
20+
});
21+
22+
test('has an ancestor folder that exists', () {
23+
void expectAncestorExists(String? path) {
24+
if (path == null) {
25+
// runtimeHome may be undefined on Linux.
26+
return;
27+
}
28+
// We expect that first two segments of the path exist. This is really
29+
// just a dummy check that some part of the path exists.
30+
final ancestorPath = p.joinAll(p.split(path).take(2));
31+
expect(
32+
Directory(ancestorPath).existsSync(),
33+
isTrue,
34+
);
35+
}
36+
37+
expectAncestorExists(baseDirectories.cacheHome);
38+
expectAncestorExists(baseDirectories.configHome);
39+
expectAncestorExists(baseDirectories.dataHome);
40+
expectAncestorExists(baseDirectories.runtimeHome);
41+
expectAncestorExists(baseDirectories.stateHome);
42+
});
43+
44+
test('empty environment throws exception', () async {
45+
expect(
46+
() => BaseDirectories('Dart', environment: <String, String>{}).configHome,
47+
throwsA(isA<EnvironmentNotFoundException>()),
48+
);
49+
});
50+
}

pkgs/cli_util/test/cli_util_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
// ignore_for_file: deprecated_member_use_from_same_package
6+
57
import 'dart:async';
68
import 'dart:io';
79

0 commit comments

Comments
 (0)