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

Add utilities for different base directories #94

Closed
wants to merge 2 commits into from
Closed
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
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>()));
});
});
});
}