Skip to content

Commit

Permalink
Add utilities for different base directories
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
4e554c4c committed Jun 29, 2024
1 parent c37d5e1 commit a625dad
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 44 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
# Name/Organization <email address>

Google Inc.
Calvin Lee <[email protected]>
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
## 0.4.2-wip

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

- Introduce `applicationCacheHome`, `applicationDataHome`,
`applicationRuntimeDir` and `applicationStateHome`.

## 0.4.1

- Fix a broken link in the readme.
Expand Down
185 changes: 158 additions & 27 deletions lib/cli_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,24 @@
// 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.

/// Utilities to locate the Dart SDK.
/// Utilities for CLI programs written in dart.
///
/// This library contains information for returning the location of the dart
/// SDK, and other directories that command-line applications may need to
/// access. This library aims follows best practices for each platform, honoring
/// the [XDG Base Directory Specification][1] on Linux and
/// [File System Basics][2] on Mac OS.
///
/// Many functions require a `productName`, as data should be stored in a
/// directory unique to your application, as to not avoid clashes with other
/// programs on the same machine. For example, if you are writing a command-line
/// application named 'zinger' then `productName` on Linux could be `zinger`. On
/// MacOS, this should be your bundle identifier (for example,
/// `com.example.Zinger`).
///
/// [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
library cli_util;

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

/// The user-specific application configuration folder for the current platform.
// Executables are also mentioned in the XDG spec, but these do not have as well
// defined of locations on Windows and MacOS.
enum _BaseDirectory { cache, config, data, runtime, state }

/// Get the user-specific application cache folder for the current platform.
///
/// This is a location appropriate for storing non-essential files that may be
/// removed at any point. This method won't create the directory; It will merely
/// return the recommended location.
///
/// The folder location depends on the platform:
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
/// * `$HOME/Library/Caches/<productName>` on **Mac OS**,
/// * `$XDG_CACHE_HOME/<productName>` on **Linux**
/// (if `$XDG_CACHE_HOME` is defined), and,
/// * `$HOME/.cache/` otherwise.
///
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
/// are undefined.
String applicationCacheHome(String productName) =>
path.join(_baseDirectory(_BaseDirectory.cache), productName);

/// Get the user-specific application configuration folder for the current
/// platform.
///
/// This is a location appropriate for storing application specific
/// configuration for the current user. The [productName] should be unique to
/// avoid clashes with other applications on the same machine. This method won't
/// actually create the folder, merely return the recommended location for
/// storing user-specific application configuration.
/// configuration for the current user. This method won't create the directory;
/// It will merely return the recommended location.
///
/// The folder location depends on the platform:
/// * `%APPDATA%\<productName>` on **Windows**,
Expand All @@ -32,42 +70,135 @@ String getSdkPath() => sdkPath;
/// (if `$XDG_CONFIG_HOME` is defined), and,
/// * `$HOME/.config/<productName>` otherwise.
///
/// The chosen location aims to follow best practices for each platform,
/// honoring the [XDG Base Directory Specification][1] on Linux and
/// [File System Basics][2] on Mac OS.
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
/// are undefined.
String applicationConfigHome(String productName) =>
path.join(_baseDirectory(_BaseDirectory.config), productName);

/// Get the user-specific application data folder for the current platform.
///
/// Throws an [EnvironmentNotFoundException] if an environment entry,
/// `%APPDATA%` or `$HOME`, is needed and not available.
/// This is a location appropriate for storing application specific
/// semi-permanent data for the current user. This method won't create the
/// directory; It will merely return the recommended location.
///
/// [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
String applicationConfigHome(String productName) =>
path.join(_configHome, productName);
/// The folder location depends on the platform:
/// * `%APPDATA%\<productName>` on **Windows**,
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
/// (if `$XDG_DATA_HOME` is defined), and,
/// * `$HOME/.local/share/<productName>` otherwise.
///
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
/// are undefined.
String applicationDataHome(String productName) =>
path.join(_baseDirectory(_BaseDirectory.data), productName);

String get _configHome {
/// Get the runtime data folder for the current platform.
///
/// This is a location appropriate for storing runtime data for the current
/// session. This method won't create the directory; It will merely return the
/// recommended location.
///
/// The folder location depends on the platform:
/// * `%LOCALAPPDATA%\<productName>` on **Windows**,
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
/// (if `$XDG_DATA_HOME` is defined), and,
/// * `$HOME/.local/share/<productName>` otherwise.
///
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
/// are undefined.
String applicationRuntimeDir(String productName) =>
path.join(_baseDirectory(_BaseDirectory.runtime), productName);

/// Get the user-specific application state folder for the current platform.
///
/// This is a location appropriate for storing application specific state
/// for the current user. This differs from [applicationDataHome] insomuch as it
/// should contain data which should persist restarts, but is not important
/// enough to be backed up. This method won't create the directory;
// It will merely return the recommended location.
///
/// The folder location depends on the platform:
/// * `%APPDATA%\<productName>` on **Windows**,
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
/// * `$XDG_DATA_HOME/<productName>` on **Linux**
/// (if `$XDG_DATA_HOME` is defined), and,
/// * `$HOME/.local/share/<productName>` otherwise.
///
/// Throws an [EnvironmentNotFoundException] if necessary environment variables
/// are undefined.
String applicationStateHome(String productName) =>
path.join(_baseDirectory(_BaseDirectory.state), productName);

String _baseDirectory(_BaseDirectory dir) {
if (Platform.isWindows) {
return _requireEnv('APPDATA');
switch (dir) {
case _BaseDirectory.config:
case _BaseDirectory.data:
return _requireEnv('APPDATA');
case _BaseDirectory.cache:
case _BaseDirectory.runtime:
case _BaseDirectory.state:
return _requireEnv('LOCALAPPDATA');
}
}

if (Platform.isMacOS) {
return path.join(_requireEnv('HOME'), 'Library', 'Application Support');
switch (dir) {
case _BaseDirectory.config:
case _BaseDirectory.data:
case _BaseDirectory.state:
return path.join(_home, 'Library', 'Application Support');
case _BaseDirectory.cache:
return path.join(_home, 'Library', 'Caches');
case _BaseDirectory.runtime:
// https://stackoverflow.com/a/76799489
return path.join(_home, 'Library', 'Caches', 'TemporaryItems');
}
}

if (Platform.isLinux) {
final xdgConfigHome = _env['XDG_CONFIG_HOME'];
if (xdgConfigHome != null) {
return xdgConfigHome;
String xdgEnv;
switch (dir) {
case _BaseDirectory.config:
xdgEnv = 'XDG_CONFIG_HOME';
break;
case _BaseDirectory.data:
xdgEnv = 'XDG_DATA_HOME';
break;
case _BaseDirectory.state:
xdgEnv = 'XDG_STATE_HOME';
break;
case _BaseDirectory.cache:
xdgEnv = 'XDG_CACHE_HOME';
break;
case _BaseDirectory.runtime:
xdgEnv = 'XDG_RUNTIME_HOME';
break;
}
final val = _env[xdgEnv];
if (val != null) {
return val;
}
// XDG Base Directory Specification says to use $HOME/.config/ when
// $XDG_CONFIG_HOME isn't defined.
return path.join(_requireEnv('HOME'), '.config');
}

// We have no guidelines, perhaps we should just do: $HOME/.config/
// same as XDG specification would specify as fallback.
return path.join(_requireEnv('HOME'), '.config');
switch (dir) {
case _BaseDirectory.runtime:
// not a great fallback
case _BaseDirectory.cache:
return path.join(_home, '.cache');
case _BaseDirectory.config:
return path.join(_home, '.config');
case _BaseDirectory.data:
return path.join(_home, '.local', 'share');
case _BaseDirectory.state:
return path.join(_home, '.local', 'state');
}
}

String get _home => _requireEnv('HOME');

String _requireEnv(String name) =>
_env[name] ?? (throw EnvironmentNotFoundException(name));

Expand Down
41 changes: 25 additions & 16 deletions test/cli_util_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,33 @@ void main() {
});
});

group('applicationConfigHome', () {
test('returns a non-empty string', () {
expect(applicationConfigHome('dart'), isNotEmpty);
});
final functions = {
'applicationCacheHome': applicationCacheHome,
'applicationConfigHome': applicationConfigHome,
'applicationDataHome': applicationDataHome,
'applicationRuntimeDir': applicationRuntimeDir,
'applicationStateHome': applicationStateHome,
};
functions.forEach((name, fn) {
group(name, () {
test('returns a non-empty string', () {
expect(fn('dart'), isNotEmpty);
});

test('has an ancestor folder that exists', () {
final path = p.split(applicationConfigHome('dart'));
// We expect that first two segments of the path exist. This is really
// just a dummy check that some part of the path exists.
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
});
test('has an ancestor folder that exists', () {
final path = p.split(fn('dart'));
// We expect that first two segments of the path exist. This is really
// just a dummy check that some part of the path exists.
expect(Directory(p.joinAll(path.take(2))).existsSync(), isTrue);
});

test('empty environment throws exception', () async {
expect(() {
runZoned(() => applicationConfigHome('dart'), zoneValues: {
#environmentOverrides: <String, String>{},
});
}, throwsA(isA<EnvironmentNotFoundException>()));
test('empty environment throws exception', () async {
expect(() {
runZoned(() => fn('dart'), zoneValues: {
#environmentOverrides: <String, String>{},
});
}, throwsA(isA<EnvironmentNotFoundException>()));
});
});
});
}

0 comments on commit a625dad

Please sign in to comment.