From a625dad00e5be97ed23b824a199005b918c0a9c0 Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Fri, 15 Dec 2023 22:28:13 +0000 Subject: [PATCH] Add utilities for different base directories This should be used to eventually fix dart-lang/sdk#41560. See https://github.com/dart-lang/sdk/issues/49166#issuecomment-1223943236 Test plan: ``` $ dart test 00:01 +16: All tests passed! ``` run `dart doc` and inspect docs for correctness. --- AUTHORS | 1 + CHANGELOG.md | 4 +- lib/cli_util.dart | 185 ++++++++++++++++++++++++++++++++++------ test/cli_util_test.dart | 41 +++++---- 4 files changed, 187 insertions(+), 44 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7a6d1d9..64f2a7d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,4 @@ # Name/Organization Google Inc. +Calvin Lee diff --git a/CHANGELOG.md b/CHANGELOG.md index bab56a9..52a7269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/lib/cli_util.dart b/lib/cli_util.dart index 82ce9ea..65c8241 100644 --- a/lib/cli_util.dart +++ b/lib/cli_util.dart @@ -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'; @@ -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%\` on **Windows**, +/// * `$HOME/Library/Caches/` on **Mac OS**, +/// * `$XDG_CACHE_HOME/` 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%\` on **Windows**, @@ -32,42 +70,135 @@ String getSdkPath() => sdkPath; /// (if `$XDG_CONFIG_HOME` is defined), and, /// * `$HOME/.config/` 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%\` on **Windows**, +/// * `$HOME/Library/Application Support/` on **Mac OS**, +/// * `$XDG_DATA_HOME/` on **Linux** +/// (if `$XDG_DATA_HOME` is defined), and, +/// * `$HOME/.local/share/` 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%\` on **Windows**, +/// * `$HOME/Library/Application Support/` on **Mac OS**, +/// * `$XDG_DATA_HOME/` on **Linux** +/// (if `$XDG_DATA_HOME` is defined), and, +/// * `$HOME/.local/share/` 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%\` on **Windows**, +/// * `$HOME/Library/Application Support/` on **Mac OS**, +/// * `$XDG_DATA_HOME/` on **Linux** +/// (if `$XDG_DATA_HOME` is defined), and, +/// * `$HOME/.local/share/` 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)); diff --git a/test/cli_util_test.dart b/test/cli_util_test.dart index e16bc59..fddf408 100644 --- a/test/cli_util_test.dart +++ b/test/cli_util_test.dart @@ -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: {}, - }); - }, throwsA(isA())); + test('empty environment throws exception', () async { + expect(() { + runZoned(() => fn('dart'), zoneValues: { + #environmentOverrides: {}, + }); + }, throwsA(isA())); + }); }); }); }