Skip to content
This repository was archived by the owner on Nov 1, 2024. It is now read-only.

Commit a625dad

Browse files
committed
Add utilities for different base directories
This should be used to eventually fix dart-lang/sdk#41560. See dart-lang/sdk#49166 (comment) Test plan: ``` $ dart test 00:01 +16: All tests passed! ``` run `dart doc` and inspect docs for correctness.
1 parent c37d5e1 commit a625dad

File tree

4 files changed

+187
-44
lines changed

4 files changed

+187
-44
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
# Name/Organization <email address>
55

66
Google Inc.
7+
Calvin Lee <[email protected]>

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## 0.4.2-wip
22

33
- Add `sdkPath` getter, deprecate `getSdkPath` function.
4-
4+
- Introduce `applicationCacheHome`, `applicationDataHome`,
5+
`applicationRuntimeDir` and `applicationStateHome`.
6+
57
## 0.4.1
68

79
- Fix a broken link in the readme.

lib/cli_util.dart

Lines changed: 158 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,24 @@
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-
/// Utilities to locate the Dart SDK.
5+
/// Utilities for CLI programs written in dart.
6+
///
7+
/// This library contains information for returning the location of the dart
8+
/// SDK, and other directories that command-line applications may need to
9+
/// access. This library aims follows best practices for each platform, honoring
10+
/// the [XDG Base Directory Specification][1] on Linux and
11+
/// [File System Basics][2] on Mac OS.
12+
///
13+
/// Many functions require a `productName`, as data should be stored in a
14+
/// directory unique to your application, as to not avoid clashes with other
15+
/// programs on the same machine. For example, if you are writing a command-line
16+
/// application named 'zinger' then `productName` on Linux could be `zinger`. On
17+
/// MacOS, this should be your bundle identifier (for example,
18+
/// `com.example.Zinger`).
19+
///
20+
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
21+
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
22+
623
library cli_util;
724

825
import 'dart:async';
@@ -17,13 +34,34 @@ String get sdkPath => path.dirname(path.dirname(Platform.resolvedExecutable));
1734
@Deprecated("Use 'sdkPath' instead")
1835
String getSdkPath() => sdkPath;
1936

20-
/// The user-specific application configuration folder for the current platform.
37+
// Executables are also mentioned in the XDG spec, but these do not have as well
38+
// defined of locations on Windows and MacOS.
39+
enum _BaseDirectory { cache, config, data, runtime, state }
40+
41+
/// Get the user-specific application cache folder for the current platform.
42+
///
43+
/// This is a location appropriate for storing non-essential files that may be
44+
/// removed at any point. This method won't create the directory; It will merely
45+
/// return the recommended location.
46+
///
47+
/// The folder location depends on the platform:
48+
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
49+
/// * `$HOME/Library/Caches/<productName>` on **Mac OS**,
50+
/// * `$XDG_CACHE_HOME/<productName>` on **Linux**
51+
/// (if `$XDG_CACHE_HOME` is defined), and,
52+
/// * `$HOME/.cache/` otherwise.
53+
///
54+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
55+
/// are undefined.
56+
String applicationCacheHome(String productName) =>
57+
path.join(_baseDirectory(_BaseDirectory.cache), productName);
58+
59+
/// Get the user-specific application configuration folder for the current
60+
/// platform.
2161
///
2262
/// This is a location appropriate for storing application specific
23-
/// configuration for the current user. The [productName] should be unique to
24-
/// avoid clashes with other applications on the same machine. This method won't
25-
/// actually create the folder, merely return the recommended location for
26-
/// storing user-specific application configuration.
63+
/// configuration for the current user. This method won't create the directory;
64+
/// It will merely return the recommended location.
2765
///
2866
/// The folder location depends on the platform:
2967
/// * `%APPDATA%\<productName>` on **Windows**,
@@ -32,42 +70,135 @@ String getSdkPath() => sdkPath;
3270
/// (if `$XDG_CONFIG_HOME` is defined), and,
3371
/// * `$HOME/.config/<productName>` otherwise.
3472
///
35-
/// The chosen location aims to follow best practices for each platform,
36-
/// honoring the [XDG Base Directory Specification][1] on Linux and
37-
/// [File System Basics][2] on Mac OS.
73+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
74+
/// are undefined.
75+
String applicationConfigHome(String productName) =>
76+
path.join(_baseDirectory(_BaseDirectory.config), productName);
77+
78+
/// Get the user-specific application data folder for the current platform.
3879
///
39-
/// Throws an [EnvironmentNotFoundException] if an environment entry,
40-
/// `%APPDATA%` or `$HOME`, is needed and not available.
80+
/// This is a location appropriate for storing application specific
81+
/// semi-permanent data for the current user. This method won't create the
82+
/// directory; It will merely return the recommended location.
4183
///
42-
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
43-
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
44-
String applicationConfigHome(String productName) =>
45-
path.join(_configHome, productName);
84+
/// The folder location depends on the platform:
85+
/// * `%APPDATA%\<productName>` on **Windows**,
86+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
87+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
88+
/// (if `$XDG_DATA_HOME` is defined), and,
89+
/// * `$HOME/.local/share/<productName>` otherwise.
90+
///
91+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
92+
/// are undefined.
93+
String applicationDataHome(String productName) =>
94+
path.join(_baseDirectory(_BaseDirectory.data), productName);
4695

47-
String get _configHome {
96+
/// Get the runtime data folder for the current platform.
97+
///
98+
/// This is a location appropriate for storing runtime data for the current
99+
/// session. This method won't create the directory; It will merely return the
100+
/// recommended location.
101+
///
102+
/// The folder location depends on the platform:
103+
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
104+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
105+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
106+
/// (if `$XDG_DATA_HOME` is defined), and,
107+
/// * `$HOME/.local/share/<productName>` otherwise.
108+
///
109+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
110+
/// are undefined.
111+
String applicationRuntimeDir(String productName) =>
112+
path.join(_baseDirectory(_BaseDirectory.runtime), productName);
113+
114+
/// Get the user-specific application state folder for the current platform.
115+
///
116+
/// This is a location appropriate for storing application specific state
117+
/// for the current user. This differs from [applicationDataHome] insomuch as it
118+
/// should contain data which should persist restarts, but is not important
119+
/// enough to be backed up. This method won't create the directory;
120+
// It will merely return the recommended location.
121+
///
122+
/// The folder location depends on the platform:
123+
/// * `%APPDATA%\<productName>` on **Windows**,
124+
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
125+
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
126+
/// (if `$XDG_DATA_HOME` is defined), and,
127+
/// * `$HOME/.local/share/<productName>` otherwise.
128+
///
129+
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
130+
/// are undefined.
131+
String applicationStateHome(String productName) =>
132+
path.join(_baseDirectory(_BaseDirectory.state), productName);
133+
134+
String _baseDirectory(_BaseDirectory dir) {
48135
if (Platform.isWindows) {
49-
return _requireEnv('APPDATA');
136+
switch (dir) {
137+
case _BaseDirectory.config:
138+
case _BaseDirectory.data:
139+
return _requireEnv('APPDATA');
140+
case _BaseDirectory.cache:
141+
case _BaseDirectory.runtime:
142+
case _BaseDirectory.state:
143+
return _requireEnv('LOCALAPPDATA');
144+
}
50145
}
51146

52147
if (Platform.isMacOS) {
53-
return path.join(_requireEnv('HOME'), 'Library', 'Application Support');
148+
switch (dir) {
149+
case _BaseDirectory.config:
150+
case _BaseDirectory.data:
151+
case _BaseDirectory.state:
152+
return path.join(_home, 'Library', 'Application Support');
153+
case _BaseDirectory.cache:
154+
return path.join(_home, 'Library', 'Caches');
155+
case _BaseDirectory.runtime:
156+
// https://stackoverflow.com/a/76799489
157+
return path.join(_home, 'Library', 'Caches', 'TemporaryItems');
158+
}
54159
}
55160

56161
if (Platform.isLinux) {
57-
final xdgConfigHome = _env['XDG_CONFIG_HOME'];
58-
if (xdgConfigHome != null) {
59-
return xdgConfigHome;
162+
String xdgEnv;
163+
switch (dir) {
164+
case _BaseDirectory.config:
165+
xdgEnv = 'XDG_CONFIG_HOME';
166+
break;
167+
case _BaseDirectory.data:
168+
xdgEnv = 'XDG_DATA_HOME';
169+
break;
170+
case _BaseDirectory.state:
171+
xdgEnv = 'XDG_STATE_HOME';
172+
break;
173+
case _BaseDirectory.cache:
174+
xdgEnv = 'XDG_CACHE_HOME';
175+
break;
176+
case _BaseDirectory.runtime:
177+
xdgEnv = 'XDG_RUNTIME_HOME';
178+
break;
179+
}
180+
final val = _env[xdgEnv];
181+
if (val != null) {
182+
return val;
60183
}
61-
// XDG Base Directory Specification says to use $HOME/.config/ when
62-
// $XDG_CONFIG_HOME isn't defined.
63-
return path.join(_requireEnv('HOME'), '.config');
64184
}
65185

66-
// We have no guidelines, perhaps we should just do: $HOME/.config/
67-
// same as XDG specification would specify as fallback.
68-
return path.join(_requireEnv('HOME'), '.config');
186+
switch (dir) {
187+
case _BaseDirectory.runtime:
188+
// not a great fallback
189+
case _BaseDirectory.cache:
190+
return path.join(_home, '.cache');
191+
case _BaseDirectory.config:
192+
return path.join(_home, '.config');
193+
case _BaseDirectory.data:
194+
return path.join(_home, '.local', 'share');
195+
case _BaseDirectory.state:
196+
return path.join(_home, '.local', 'state');
197+
}
69198
}
70199

200+
String get _home => _requireEnv('HOME');
201+
71202
String _requireEnv(String name) =>
72203
_env[name] ?? (throw EnvironmentNotFoundException(name));
73204

test/cli_util_test.dart

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,33 @@ void main() {
1616
});
1717
});
1818

19-
group('applicationConfigHome', () {
20-
test('returns a non-empty string', () {
21-
expect(applicationConfigHome('dart'), isNotEmpty);
22-
});
19+
final functions = {
20+
'applicationCacheHome': applicationCacheHome,
21+
'applicationConfigHome': applicationConfigHome,
22+
'applicationDataHome': applicationDataHome,
23+
'applicationRuntimeDir': applicationRuntimeDir,
24+
'applicationStateHome': applicationStateHome,
25+
};
26+
functions.forEach((name, fn) {
27+
group(name, () {
28+
test('returns a non-empty string', () {
29+
expect(fn('dart'), isNotEmpty);
30+
});
2331

24-
test('has an ancestor folder that exists', () {
25-
final path = p.split(applicationConfigHome('dart'));
26-
// We expect that first two segments of the path exist. This is really
27-
// just a dummy check that some part of the path exists.
28-
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
29-
});
32+
test('has an ancestor folder that exists', () {
33+
final path = p.split(fn('dart'));
34+
// We expect that first two segments of the path exist. This is really
35+
// just a dummy check that some part of the path exists.
36+
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
37+
});
3038

31-
test('empty environment throws exception', () async {
32-
expect(() {
33-
runZoned(() => applicationConfigHome('dart'), zoneValues: {
34-
#environmentOverrides: <String, String>{},
35-
});
36-
}, throwsA(isA<EnvironmentNotFoundException>()));
39+
test('empty environment throws exception', () async {
40+
expect(() {
41+
runZoned(() => fn('dart'), zoneValues: {
42+
#environmentOverrides: <String, String>{},
43+
});
44+
}, throwsA(isA<EnvironmentNotFoundException>()));
45+
});
3746
});
3847
});
3948
}

0 commit comments

Comments
 (0)