Use applicationConfigHome from package:cli_util (#3164)

Also handle missing config-dir:
* When trying to read a file from the config-dir we just ignore it (no file existed).
* When trying to write we throw a DataException.
diff --git a/lib/src/authentication/token_store.dart b/lib/src/authentication/token_store.dart
index 24f70fc..8431088 100644
--- a/lib/src/authentication/token_store.dart
+++ b/lib/src/authentication/token_store.dart
@@ -5,9 +5,12 @@
 // @dart=2.11
 
 import 'dart:convert';
+import 'dart:io';
 
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
 
+import '../exceptions.dart';
 import '../io.dart';
 import '../log.dart' as log;
 import 'credential.dart';
@@ -28,9 +31,9 @@
   /// Reads "pub-tokens.json" and parses / deserializes it into list of
   /// [Credential].
   List<Credential> _loadCredentials() {
-    final result = List<Credential>.empty(growable: true);
+    final result = <Credential>[];
     final path = _tokensFile;
-    if (!fileExists(path)) {
+    if (path == null || !fileExists(path)) {
       return result;
     }
 
@@ -90,11 +93,21 @@
     return result;
   }
 
+  @alwaysThrows
+  void missingConfigDir() {
+    final variable = Platform.isWindows ? '%APPDATA%' : r'$HOME';
+    throw DataException('No config dir found. Check that $variable is set');
+  }
+
   /// Writes [credentials] into "pub-tokens.json".
   void _saveCredentials(List<Credential> credentials) {
-    ensureDir(path.dirname(_tokensFile));
+    final tokensFile = _tokensFile;
+    if (tokensFile == null) {
+      missingConfigDir();
+    }
+    ensureDir(path.dirname(tokensFile));
     writeTextFile(
-        _tokensFile,
+        tokensFile,
         jsonEncode(<String, dynamic>{
           'version': 1,
           'hosted': credentials.map((it) => it.toJson()).toList(),
@@ -161,10 +174,18 @@
 
   /// Deletes pub-tokens.json file from the disk.
   void deleteTokensFile() {
-    deleteEntry(_tokensFile);
-    log.message('pub-tokens.json is deleted.');
+    final tokensFile = _tokensFile;
+    if (tokensFile == null) {
+      missingConfigDir();
+    } else if (!fileExists(tokensFile)) {
+      log.message('No credentials file found at "$tokensFile"');
+    } else {
+      deleteEntry(_tokensFile);
+      log.message('pub-tokens.json is deleted.');
+    }
   }
 
   /// Full path to the "pub-tokens.json" file.
-  String get _tokensFile => path.join(configDir, 'pub-tokens.json');
+  String get _tokensFile =>
+      configDir == null ? null : path.join(configDir, 'pub-tokens.json');
 }
diff --git a/lib/src/io.dart b/lib/src/io.dart
index f95408d..b4898ff 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -8,6 +8,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:cli_util/cli_util.dart'
+    show EnvironmentNotFoundException, applicationConfigHome;
 import 'package:http/http.dart' show ByteStream;
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:meta/meta.dart';
@@ -1025,22 +1027,16 @@
 }
 
 /// The location for dart-specific configuration.
-final String dartConfigDir = () {
-  // TODO: Migrate to new value from cli_util
-  if (runningFromTest) {
+///
+/// `null` if no config dir could be found.
+final String? dartConfigDir = () {
+  if (runningFromTest &&
+      Platform.environment.containsKey('_PUB_TEST_CONFIG_DIR')) {
     return Platform.environment['_PUB_TEST_CONFIG_DIR'];
   }
-  String configDir;
-  if (Platform.isLinux) {
-    configDir = Platform.environment['XDG_CONFIG_HOME'] ??
-        path.join(Platform.environment['HOME']!, '.config');
-  } else if (Platform.isWindows) {
-    configDir = Platform.environment['APPDATA']!;
-  } else if (Platform.isMacOS) {
-    configDir = path.join(
-        Platform.environment['HOME']!, 'Library', 'Application Support');
-  } else {
-    configDir = path.join(Platform.environment['HOME']!, '.config');
+  try {
+    return applicationConfigHome('dart');
+  } on EnvironmentNotFoundException {
+    return null;
   }
-  return path.join(configDir, 'dart');
-}()!;
+}();
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index c1284e9..20d32c1 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -191,10 +191,17 @@
 /// best place for storing secrets, as it might be shared.
 ///
 /// To provide backwards compatibility we use the legacy file if only it exists.
+///
+/// Returns `null` if there is no good place for the file.
 String _credentialsFile(SystemCache cache) {
-  final newCredentialsFile = path.join(dartConfigDir, 'pub-credentials.json');
-  return [newCredentialsFile, _legacyCredentialsFile(cache)]
-      .firstWhere(fileExists, orElse: () => newCredentialsFile);
+  final configDir = dartConfigDir;
+
+  final newCredentialsFile =
+      configDir == null ? null : path.join(configDir, 'pub-credentials.json');
+  return [
+    if (newCredentialsFile != null) newCredentialsFile,
+    _legacyCredentialsFile(cache)
+  ].firstWhere(fileExists, orElse: () => newCredentialsFile);
 }
 
 String _legacyCredentialsFile(SystemCache cache) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 23fc0c8..d3e643b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,7 +9,7 @@
   analyzer: ^1.5.0
   args: ^2.1.0
   async: ^2.6.1
-  cli_util: ^0.3.0
+  cli_util: ^0.3.5
   collection: ^1.15.0
   crypto: ^3.0.1
   frontend_server_client: ^2.0.0
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 356b959..b7bb581 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -138,6 +138,7 @@
   int exitCode,
   Map<String, String> environment,
   String workingDirectory,
+  includeParentEnvironment = true,
 }) async {
   if (error != null && warning != null) {
     throw ArgumentError("Cannot pass both 'error' and 'warning'.");
@@ -161,7 +162,8 @@
       silent: silent,
       exitCode: exitCode,
       environment: environment,
-      workingDirectory: workingDirectory);
+      workingDirectory: workingDirectory,
+      includeParentEnvironment: includeParentEnvironment);
 }
 
 Future<void> pubAdd({
@@ -192,6 +194,7 @@
   int exitCode,
   Map<String, String> environment,
   String workingDirectory,
+  bool includeParentEnvironment = true,
 }) async =>
     await pubCommand(
       RunCommand.get,
@@ -202,6 +205,7 @@
       exitCode: exitCode,
       environment: environment,
       workingDirectory: workingDirectory,
+      includeParentEnvironment: includeParentEnvironment,
     );
 
 Future<void> pubUpgrade(
@@ -330,23 +334,27 @@
 ///
 /// If [environment] is given, any keys in it will override the environment
 /// variables passed to the spawned process.
-Future<void> runPub({
-  List<String> args,
-  output,
-  error,
-  outputJson,
-  silent,
-  int exitCode,
-  String workingDirectory,
-  Map<String, String> environment,
-  List<String> input,
-}) async {
+Future<void> runPub(
+    {List<String> args,
+    output,
+    error,
+    outputJson,
+    silent,
+    int exitCode,
+    String workingDirectory,
+    Map<String, String> environment,
+    List<String> input,
+    includeParentEnvironment = true}) async {
   exitCode ??= exit_codes.SUCCESS;
   // Cannot pass both output and outputJson.
   assert(output == null || outputJson == null);
 
   var pub = await startPub(
-      args: args, workingDirectory: workingDirectory, environment: environment);
+    args: args,
+    workingDirectory: workingDirectory,
+    environment: environment,
+    includeParentEnvironment: includeParentEnvironment,
+  );
 
   if (input != null) {
     input.forEach(pub.stdin.writeln);
@@ -460,21 +468,12 @@
     String tokenEndpoint,
     String workingDirectory,
     Map<String, String> environment,
-    bool verbose = true}) async {
+    bool verbose = true,
+    includeParentEnvironment = true}) async {
   args ??= [];
 
   ensureDir(_pathInSandbox(appPath));
 
-  // Find a Dart executable we can use to spawn. Use the same one that was
-  // used to run this script itself.
-  var dartBin = Platform.executable;
-
-  // If the executable looks like a path, get its full path. That way we
-  // can still find it when we spawn it with a different working directory.
-  if (dartBin.contains(Platform.pathSeparator)) {
-    dartBin = p.absolute(dartBin);
-  }
-
   // If there's a snapshot for "pub" available we use it. If the snapshot is
   // out-of-date local source the tests will be useless, therefore it is
   // recommended to use a temporary file with a unique name for each test run.
@@ -492,11 +491,20 @@
     ..addAll([pubPath, if (verbose) '--verbose'])
     ..addAll(args);
 
-  return await PubProcess.start(dartBin, dartArgs,
-      environment: getPubTestEnvironment(tokenEndpoint)
-        ..addAll(environment ?? {}),
+  final mergedEnvironment = getPubTestEnvironment(tokenEndpoint);
+  for (final e in (environment ?? {}).entries) {
+    if (e.value == null) {
+      mergedEnvironment.remove(e.key);
+    } else {
+      mergedEnvironment[e.key] = e.value;
+    }
+  }
+
+  return await PubProcess.start(Platform.resolvedExecutable, dartArgs,
+      environment: mergedEnvironment,
       workingDirectory: workingDirectory ?? _pathInSandbox(appPath),
-      description: args.isEmpty ? 'pub' : 'pub ${args.first}');
+      description: args.isEmpty ? 'pub' : 'pub ${args.first}',
+      includeParentEnvironment: includeParentEnvironment);
 }
 
 /// A subclass of [TestProcess] that parses pub's verbose logging output and
diff --git a/test/token/add_token_test.dart b/test/token/add_token_test.dart
index edce8fd..5e6b1f7 100644
--- a/test/token/add_token_test.dart
+++ b/test/token/add_token_test.dart
@@ -141,4 +141,15 @@
 
     await d.dir(configPath, [d.nothing('pub-tokens.json')]).validate();
   });
+
+  test('with empty environment gives error message', () async {
+    await runPub(
+      args: ['token', 'add', 'https://mypub.com'],
+      input: ['auth-token'],
+      error: contains('No config dir found.'),
+      exitCode: exit_codes.DATA,
+      environment: {'_PUB_TEST_CONFIG_DIR': null},
+      includeParentEnvironment: false,
+    );
+  });
 }
diff --git a/test/token/remove_token_test.dart b/test/token/remove_token_test.dart
index f9ce67d..24fefa2 100644
--- a/test/token/remove_token_test.dart
+++ b/test/token/remove_token_test.dart
@@ -61,4 +61,14 @@
 
     await d.dir(configPath, [d.nothing('pub-tokens.json')]).validate();
   });
+
+  test('with empty environment gives error message', () async {
+    await runPub(
+      args: ['token', 'remove', 'http://mypub.com'],
+      error: contains('No config dir found.'),
+      exitCode: exit_codes.DATA,
+      environment: {'_PUB_TEST_CONFIG_DIR': null},
+      includeParentEnvironment: false,
+    );
+  });
 }