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

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 Dec 15, 2023
1 parent 1fd87c1 commit ff8a7f2
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 50 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]>
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.4.2

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

## 0.4.1

- Fix a broken link in the readme.
Expand Down
191 changes: 158 additions & 33 deletions lib/cli_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@
// 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 return the Dart SDK location.
/// 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 @@ -13,14 +29,34 @@ import 'package:path/path.dart' as path;
/// Return the path to the current Dart SDK.
String getSdkPath() => path.dirname(path.dirname(Platform.resolvedExecutable));

// executable are alo mentioned in the XDG spec, but these do not have as well
// defined of locations on Windows, 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 `%LOCALAPPDATA%` or `$HOME` is
/// needed but 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 @@ -29,54 +65,143 @@ String getSdkPath() => path.dirname(path.dirname(Platform.resolvedExecutable));
/// (if `$XDG_CONFIG_HOME` is defined), and,
/// * `$HOME/.config/<productName>` otherwise.
///
/// This aims follows 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 `%APPDATA%` or `$HOME` is needed
/// but undefined.
/// 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);

/// 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);

String get _configHome {
/// 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) {
final appdata = _env['APPDATA'];
if (appdata == null) {
throw EnvironmentNotFoundException(
'Environment variable %APPDATA% is not defined!');
switch (dir) {
case _BaseDirectory.config:
case _BaseDirectory.data:
return _fetchEnvRequired('APPDATA');
case _BaseDirectory.cache:
case _BaseDirectory.runtime:
case _BaseDirectory.state:
return _fetchEnvRequired('LOCALAPPDATA');
}
return appdata;
}

if (Platform.isMacOS) {
return path.join(_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(_home, '.config');
}

// We have no guidelines, perhaps we should just do: $HOME/.config/
// same as XDG specification would specify as fallback.
return path.join(_home, '.config');
switch (dir) {
case _BaseDirectory.runtime:
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 {
final home = _env['HOME'];
if (home == null) {
String get _home => _fetchEnvRequired('HOME');

String _fetchEnvRequired(String name) {
final v = _env[name];
if (v == null) {
throw EnvironmentNotFoundException(
r'Environment variable $HOME is not defined!');
'Environment variable \$$name is not defined!');
}
return home;
return v;
}

class EnvironmentNotFoundException implements Exception {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: cli_util
version: 0.4.1
version: 0.4.2
description: A library to help in building Dart command-line apps.
repository: https://github.com/dart-lang/cli_util

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 @@ -18,24 +18,33 @@ void defineTests() {
});
});

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(applicationConfigHome('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(() => applicationConfigHome('dart'), zoneValues: {
#environmentOverrides: <String, String>{},
});
}, throwsA(isA<EnvironmentNotFoundException>()));
});
});
});
}

0 comments on commit ff8a7f2

Please sign in to comment.