diff --git a/lib/pub.dart b/lib/pub.dart index b78cbca..841e57d 100644 --- a/lib/pub.dart +++ b/lib/pub.dart
@@ -9,10 +9,15 @@ import 'src/pub_embeddable_command.dart'; export 'src/executable.dart' show getExecutableForCommand, CommandResolutionFailedException; +export 'src/pub_embeddable_command.dart' show PubAnalytics; /// Returns a [Command] for pub functionality that can be used by an embedding /// CommandRunner. -Command<int> pubCommand() => PubEmbeddableCommand(); +/// +/// If [analytics] is given, pub will use that analytics instance to send +/// statistics about resolutions. +Command<int> pubCommand({PubAnalytics analytics}) => + PubEmbeddableCommand(analytics); /// Support for the `pub` toplevel command. @Deprecated('Use [pubCommand] instead.')
diff --git a/lib/src/authentication/client.dart b/lib/src/authentication/client.dart index 3de5b2e..2ede545 100644 --- a/lib/src/authentication/client.dart +++ b/lib/src/authentication/client.dart
@@ -77,7 +77,9 @@ challenge.scheme == 'bearer' && challenge.parameters['realm'] == 'pub' && challenge.parameters['message'] != null); - serverMessage = challenge?.parameters['message']; + if (challenge?.parameters != null) { + serverMessage = challenge.parameters['message']; + } } on FormatException { // Ignore errors might be caused when parsing invalid header values }
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/command.dart b/lib/src/command.dart index cd4a524..503d174 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart
@@ -117,6 +117,8 @@ return _pubEmbeddableCommand ?? (runner as PubCommandRunner); } + PubAnalytics get analytics => _pubEmbeddableCommand?.analytics; + @override String get invocation { var command = this;
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index 728f8ea..0775325 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart
@@ -150,11 +150,10 @@ // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output await Entrypoint.global(newRoot, entrypoint.lockFile, cache, solveResult: solveResult) - .acquireDependencies( - SolveType.GET, - dryRun: true, - precompile: argResults['precompile'], - ); + .acquireDependencies(SolveType.GET, + dryRun: true, + precompile: argResults['precompile'], + analytics: analytics); } else { /// Update the `pubspec.yaml` before calling [acquireDependencies] to /// ensure that the modification timestamp on `pubspec.lock` and @@ -165,14 +164,18 @@ /// Create a new [Entrypoint] since we have to reprocess the updated /// pubspec file. final updatedEntrypoint = Entrypoint(directory, cache); - await updatedEntrypoint.acquireDependencies(SolveType.GET, - precompile: argResults['precompile']); + await updatedEntrypoint.acquireDependencies( + SolveType.GET, + precompile: argResults['precompile'], + analytics: analytics, + ); if (argResults['example'] && entrypoint.example != null) { await entrypoint.example.acquireDependencies( SolveType.GET, precompile: argResults['precompile'], onlyReportSuccessOrFailure: true, + analytics: analytics, ); } }
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart index 2d0840a..2e26bc4 100644 --- a/lib/src/command/downgrade.dart +++ b/lib/src/command/downgrade.dart
@@ -53,16 +53,21 @@ 'The --packages-dir flag is no longer used and does nothing.')); } var dryRun = argResults['dry-run']; + await entrypoint.acquireDependencies( SolveType.DOWNGRADE, unlock: argResults.rest, dryRun: dryRun, + analytics: analytics, ); if (argResults['example'] && entrypoint.example != null) { - await entrypoint.example.acquireDependencies(SolveType.GET, - unlock: argResults.rest, - dryRun: dryRun, - onlyReportSuccessOrFailure: true); + await entrypoint.example.acquireDependencies( + SolveType.GET, + unlock: argResults.rest, + dryRun: dryRun, + onlyReportSuccessOrFailure: true, + analytics: analytics, + ); } if (isOffline) {
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart index 2709238..23efab3 100644 --- a/lib/src/command/get.dart +++ b/lib/src/command/get.dart
@@ -51,14 +51,21 @@ log.warning(log.yellow( 'The --packages-dir flag is no longer used and does nothing.')); } - await entrypoint.acquireDependencies(SolveType.GET, - dryRun: argResults['dry-run'], precompile: argResults['precompile']); + await entrypoint.acquireDependencies( + SolveType.GET, + dryRun: argResults['dry-run'], + precompile: argResults['precompile'], + analytics: analytics, + ); if (argResults['example'] && entrypoint.example != null) { - await entrypoint.example.acquireDependencies(SolveType.GET, - dryRun: argResults['dry-run'], - precompile: argResults['precompile'], - onlyReportSuccessOrFailure: true); + await entrypoint.example.acquireDependencies( + SolveType.GET, + dryRun: argResults['dry-run'], + precompile: argResults['precompile'], + onlyReportSuccessOrFailure: true, + analytics: analytics, + ); } } }
diff --git a/lib/src/command/global_activate.dart b/lib/src/command/global_activate.dart index 2959e6e..abd0ee3 100644 --- a/lib/src/command/global_activate.dart +++ b/lib/src/command/global_activate.dart
@@ -139,8 +139,12 @@ var path = readArg('No package to activate given.'); validateNoExtraArgs(); - return globals.activatePath(path, executables, - overwriteBinStubs: overwrite); + return globals.activatePath( + path, + executables, + overwriteBinStubs: overwrite, + analytics: analytics, + ); } throw StateError('unreachable');
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index 151ee79..0e7a725 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart
@@ -156,7 +156,8 @@ // explicitly have to define mock servers as official server to test // publish command with oauth2 credentials. if (runningFromTest && - Platform.environment.containsKey('PUB_HOSTED_URL')) + Platform.environment.containsKey('PUB_HOSTED_URL') && + Platform.environment['_PUB_TEST_AUTH_METHOD'] == 'oauth2') Platform.environment['PUB_HOSTED_URL'], };
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart index f505785..2072931 100644 --- a/lib/src/command/remove.dart +++ b/lib/src/command/remove.dart
@@ -68,11 +68,10 @@ final newRoot = Package.inMemory(newPubspec); await Entrypoint.global(newRoot, entrypoint.lockFile, cache) - .acquireDependencies( - SolveType.GET, - precompile: argResults['precompile'], - dryRun: true, - ); + .acquireDependencies(SolveType.GET, + precompile: argResults['precompile'], + dryRun: true, + analytics: null); } else { /// Update the pubspec. _writeRemovalToPubspec(packages); @@ -80,13 +79,19 @@ /// Create a new [Entrypoint] since we have to reprocess the updated /// pubspec file. final updatedEntrypoint = Entrypoint(directory, cache); - await updatedEntrypoint.acquireDependencies(SolveType.GET, - precompile: argResults['precompile']); + await updatedEntrypoint.acquireDependencies( + SolveType.GET, + precompile: argResults['precompile'], + analytics: analytics, + ); if (argResults['example'] && entrypoint.example != null) { - await entrypoint.example.acquireDependencies(SolveType.GET, - precompile: argResults['precompile'], - onlyReportSuccessOrFailure: true); + await entrypoint.example.acquireDependencies( + SolveType.GET, + precompile: argResults['precompile'], + onlyReportSuccessOrFailure: true, + analytics: analytics, + ); } } }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 39a42c7..b74cbe7 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -122,11 +122,14 @@ } Future<void> _runUpgrade(Entrypoint e, {bool onlySummary = false}) async { - await e.acquireDependencies(SolveType.UPGRADE, - unlock: argResults.rest, - dryRun: _dryRun, - precompile: _precompile, - onlyReportSuccessOrFailure: onlySummary); + await e.acquireDependencies( + SolveType.UPGRADE, + unlock: argResults.rest, + dryRun: _dryRun, + precompile: _precompile, + onlyReportSuccessOrFailure: onlySummary, + analytics: analytics, + ); _showOfflineWarning(); } @@ -233,6 +236,7 @@ SolveType.UPGRADE, dryRun: true, precompile: _precompile, + analytics: null, // No analytics for dry-run ); } else { await _updatePubspec(changes); @@ -243,6 +247,7 @@ await Entrypoint(directory, cache).acquireDependencies( SolveType.UPGRADE, precompile: _precompile, + analytics: analytics, ); } @@ -326,6 +331,7 @@ SolveType.UPGRADE, dryRun: true, precompile: _precompile, + analytics: null, ); } else { await _updatePubspec(changes); @@ -336,6 +342,7 @@ await Entrypoint(directory, cache).acquireDependencies( SolveType.UPGRADE, precompile: _precompile, + analytics: analytics, ); }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 8820c33..763e707 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -28,6 +28,7 @@ import 'package_graph.dart'; import 'package_name.dart'; import 'packages_file.dart' as packages_file; +import 'pub_embeddable_command.dart'; import 'pubspec.dart'; import 'sdk.dart'; import 'solver.dart'; @@ -247,6 +248,7 @@ Iterable<String> unlock, bool dryRun = false, bool precompile = false, + @required PubAnalytics analytics, bool onlyReportSuccessOrFailure = false, }) async { final suffix = root.dir == null || root.dir == '.' ? '' : ' in ${root.dir}'; @@ -307,6 +309,10 @@ } if (!dryRun) { + if (analytics != null) { + result.sendAnalytics(analytics); + } + /// Build a package graph from the version solver results so we don't /// have to reload and reparse all the pubspecs. _packageGraph = PackageGraph.fromSolveResult(this, result);
diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 08722e5..ed2927b 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart
@@ -19,6 +19,7 @@ import 'isolate.dart' as isolate; import 'log.dart' as log; import 'log.dart'; +import 'pub_embeddable_command.dart'; import 'solver/type.dart'; import 'system_cache.dart'; import 'utils.dart'; @@ -236,7 +237,9 @@ /// [CommandResolutionFailedException]. /// /// * Otherwise if the current package resolution is outdated do an implicit -/// `pub get`, if that fails, throw a [CommandResolutionFailedException]. +/// `pub get`, if that fails, throw a [CommandResolutionFailedException]. +/// +/// This pub get will send analytics events to [analytics] if provided. /// /// * Otherwise let `<current>` be the name of the package at [root], and /// interpret [descriptor] as `[<package>][:<command>]`. @@ -269,6 +272,7 @@ bool allowSnapshot = true, String root, String pubCacheDir, + PubAnalytics analytics, }) async { root ??= p.current; var asPath = descriptor; @@ -294,7 +298,11 @@ entrypoint.assertUpToDate(); } on DataException { await warningsOnlyUnlessTerminal( - () => entrypoint.acquireDependencies(SolveType.GET)); + () => entrypoint.acquireDependencies( + SolveType.GET, + analytics: analytics, + ), + ); } String command;
diff --git a/lib/src/git.dart b/lib/src/git.dart index 0633169..5fa7536 100644 --- a/lib/src/git.dart +++ b/lib/src/git.dart
@@ -5,6 +5,8 @@ /// Helper functionality for invoking Git. import 'dart:async'; +import 'package:path/path.dart' as p; + import 'exceptions.dart'; import 'io.dart'; import 'log.dart' as log; @@ -103,6 +105,22 @@ String? _commandCache; +/// Returns the root of the git repo [dir] belongs to. Returns `null` if not +/// in a git repo or git is not installed. +String? repoRoot(String dir) { + if (isInstalled) { + try { + return p.normalize( + runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first, + ); + } on GitException { + // Not in a git folder. + return null; + } + } + return null; +} + /// Checks whether [command] is the Git command for this computer. bool _tryGitCommand(String command) { // If "git --version" prints something familiar, git is working.
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index 49d681f..072065e 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart
@@ -21,6 +21,7 @@ import 'log.dart' as log; import 'package.dart'; import 'package_name.dart'; +import 'pub_embeddable_command.dart'; import 'pubspec.dart'; import 'sdk.dart'; import 'solver.dart'; @@ -144,11 +145,11 @@ /// existing binstubs in other packages will be overwritten by this one's. /// Otherwise, the previous ones will be preserved. Future<void> activatePath(String path, List<String> executables, - {bool overwriteBinStubs}) async { + {bool overwriteBinStubs, @required PubAnalytics analytics}) async { var entrypoint = Entrypoint(path, cache); // Get the package's dependencies. - await entrypoint.acquireDependencies(SolveType.GET); + await entrypoint.acquireDependencies(SolveType.GET, analytics: analytics); var name = entrypoint.root.name; try {
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/lib/src/package.dart b/lib/src/package.dart index 5c46709..1aeb9dc 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart
@@ -224,16 +224,7 @@ // An in-memory package has no files. if (dir == null) return []; - var root = dir; - if (git.isInstalled) { - try { - root = p.normalize( - git.runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first, - ); - } on git.GitException { - // Not in a git folder. - } - } + var root = git.repoRoot(dir) ?? dir; beneath = p .toUri(p.normalize(p.relative(p.join(dir, beneath ?? '.'), from: root))) .path;
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart index 4240ad1..aebe06c 100644 --- a/lib/src/pub_embeddable_command.dart +++ b/lib/src/pub_embeddable_command.dart
@@ -3,7 +3,10 @@ // BSD-style license that can be found in the LICENSE file. // @dart=2.10 +import 'package:meta/meta.dart'; +import 'package:usage/usage.dart'; +import 'command.dart' show PubCommand, PubTopLevel; import 'command.dart'; import 'command/add.dart'; import 'command/build.dart'; @@ -24,6 +27,16 @@ import 'log.dart' as log; import 'log.dart'; +/// The information needed for the embedded pub command to send analytics. +@sealed +class PubAnalytics { + /// Name of the custom dimension of the dependency kind. + final String dependencyKindCustomDimensionName; + final Analytics analytics; + PubAnalytics(this.analytics, + {@required this.dependencyKindCustomDimensionName}); +} + /// Exposes the `pub` commands as a command to be embedded in another command /// runner such as `dart pub`. class PubEmbeddableCommand extends PubCommand implements PubTopLevel { @@ -35,9 +48,12 @@ String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-global'; @override + final PubAnalytics analytics; + + @override String get directory => argResults['directory']; - PubEmbeddableCommand() : super() { + PubEmbeddableCommand(this.analytics) : super() { argParser.addFlag('trace', help: 'Print debugging information when an error occurs.'); argParser.addFlag('verbose',
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index ebcdfd7..c3c98d4 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart
@@ -7,10 +7,14 @@ import 'package:collection/collection.dart'; import 'package:pub_semver/pub_semver.dart'; +import '../io.dart'; import '../lock_file.dart'; +import '../log.dart' as log; import '../package.dart'; import '../package_name.dart'; +import '../pub_embeddable_command.dart'; import '../pubspec.dart'; +import '../source/hosted.dart'; import '../source_registry.dart'; import '../system_cache.dart'; import 'report.dart'; @@ -44,6 +48,9 @@ /// because it found an invalid solution. final int attemptedSolutions; + /// The wall clock time the resolution took. + final Duration resolutionTime; + /// The [LockFile] representing the packages selected by this version /// resolution. LockFile get lockFile { @@ -89,8 +96,15 @@ .toSet()); } - SolveResult(this._sources, this._root, this._previousLockFile, this.packages, - this.pubspecs, this.availableVersions, this.attemptedSolutions); + SolveResult( + this._sources, + this._root, + this._previousLockFile, + this.packages, + this.pubspecs, + this.availableVersions, + this.attemptedSolutions, + this.resolutionTime); /// Displays a report of what changes were made to the lockfile. /// @@ -118,6 +132,44 @@ } } + /// Send analytics about the package resolution. + void sendAnalytics(PubAnalytics analytics) { + ArgumentError.checkNotNull(analytics); + + for (final package in packages) { + final source = package.source; + // Only send analytics for packages from pub.dev. + if (source is HostedSource && + (runningFromTest || + package.description['url'] == HostedSource.pubDevUrl)) { + final dependencyKind = const { + DependencyType.dev: 'dev', + DependencyType.direct: 'direct', + DependencyType.none: 'transitive' + }[_root.dependencyType(package.name)]; + analytics.analytics.sendEvent( + 'pub-get', + package.name, + label: package.version.canonicalizedVersion, + value: 1, + parameters: { + 'ni': '1', // We consider a pub-get a non-interactive event. + analytics.dependencyKindCustomDimensionName: dependencyKind, + }, + ); + log.fine( + 'Sending analytics hit for "pub-get" of ${package.name} version ${package.version} as dependency-kind $dependencyKind'); + } + } + analytics.analytics.sendTiming( + 'resolution', + resolutionTime.inMilliseconds, + category: 'pub-get', + ); + log.fine( + 'Sending analytics timing "pub-get" took ${resolutionTime.inMilliseconds} miliseconds'); + } + @override String toString() => 'Took $attemptedSolutions tries to resolve to\n' '- ${packages.join("\n- ")}';
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart index ee1541d..4e08a6b 100644 --- a/lib/src/solver/version_solver.dart +++ b/lib/src/solver/version_solver.dart
@@ -78,6 +78,8 @@ /// The set of packages for which the lockfile should be ignored. final Set<String> _unlock; + final _stopwatch = Stopwatch(); + VersionSolver(this._type, this._systemCache, this._root, this._lockFile, Iterable<String> unlock) : _dependencyOverrides = _root.pubspec.dependencyOverrides, @@ -86,8 +88,7 @@ /// Finds a set of dependencies that match the root package's constraints, or /// throws an error if no such set is available. Future<SolveResult> solve() async { - var stopwatch = Stopwatch()..start(); - + _stopwatch.start(); _addIncompatibility(Incompatibility( [Term(PackageRange.root(_root), false)], IncompatibilityCause.root)); @@ -103,7 +104,7 @@ }); } finally { // Gather some solving metrics. - log.solver('Version solving took ${stopwatch.elapsed} seconds.\n' + log.solver('Version solving took ${_stopwatch.elapsed} seconds.\n' 'Tried ${_solution.attemptedSolutions} solutions.'); } } @@ -418,13 +419,15 @@ } return SolveResult( - _systemCache.sources, - _root, - _lockFile, - decisions, - pubspecs, - await _getAvailableVersions(decisions), - _solution.attemptedSolutions); + _systemCache.sources, + _root, + _lockFile, + decisions, + pubspecs, + await _getAvailableVersions(decisions), + _solution.attemptedSolutions, + _stopwatch.elapsed, + ); } /// Generates a map containing all of the known available versions for each
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 9d98397..986dfa6 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -97,6 +97,8 @@ ? _OfflineHostedSource(this, systemCache) : BoundHostedSource(this, systemCache); + static String pubDevUrl = 'https://pub.dartlang.org'; + /// Gets the default URL for the package server for hosted dependencies. Uri get defaultUrl { // Changing this to pub.dev raises the following concerns:
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart index df01214..0986fed 100644 --- a/lib/src/validator/gitignore.dart +++ b/lib/src/validator/gitignore.dart
@@ -31,18 +31,25 @@ '--exclude-standard', '--recurse-submodules' ], workingDir: entrypoint.root.dir); + final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir; + var beneath = p.posix.joinAll( + p.split(p.normalize(p.relative(entrypoint.root.dir, from: root)))); + if (beneath == './') { + beneath = ''; + } String resolve(String path) { if (Platform.isWindows) { - return p.joinAll([entrypoint.root.dir, ...p.posix.split(path)]); + return p.joinAll([root, ...p.posix.split(path)]); } - return p.join(entrypoint.root.dir, path); + return p.join(root, path); } final unignoredByGitignore = Ignore.listFiles( + beneath: beneath, listDir: (dir) { var contents = Directory(resolve(dir)).listSync(); - return contents.map((entity) => p.posix.joinAll( - p.split(p.relative(entity.path, from: entrypoint.root.dir)))); + return contents.map((entity) => + p.posix.joinAll(p.split(p.relative(entity.path, from: root)))); }, ignoreForDir: (dir) { final gitIgnore = resolve('$dir/.gitignore'); @@ -52,8 +59,12 @@ return rules.isEmpty ? null : Ignore(rules); }, isDir: (dir) => dirExists(resolve(dir)), - ).toSet(); - + ).map((file) { + final relative = p.relative(resolve(file), from: entrypoint.root.dir); + return Platform.isWindows + ? p.posix.joinAll(p.split(relative)) + : relative; + }).toSet(); final ignoredFilesCheckedIn = checkedIntoGit .where((file) => !unignoredByGitignore.contains(file)) .toList();
diff --git a/lib/src/validator/pubspec_typo.dart b/lib/src/validator/pubspec_typo.dart index 2e10ed6..a770b76 100644 --- a/lib/src/validator/pubspec_typo.dart +++ b/lib/src/validator/pubspec_typo.dart
@@ -73,4 +73,6 @@ 'publish_to', 'false_secrets', 'flutter', + 'screenshots', + 'platforms', ];
diff --git a/pubspec.yaml b/pubspec.yaml index 23fc0c8..3bfe095 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 @@ -20,10 +20,11 @@ path: ^1.8.0 pedantic: ^1.11.0 pool: ^1.5.0 - pub_semver: ^2.0.0 + pub_semver: ^2.1.0 shelf: ^1.1.1 source_span: ^1.8.1 stack_trace: ^1.10.0 + usage: ^4.0.2 yaml: ^3.1.0 yaml_edit: ^2.0.0
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart index 257bf4b..dbe6c89 100644 --- a/test/embedding/embedding_test.dart +++ b/test/embedding/embedding_test.dart
@@ -4,6 +4,7 @@ // @dart=2.10 +import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -24,8 +25,11 @@ dynamic exitCode = 0}) async { final process = await TestProcess.start( Platform.resolvedExecutable, - [snapshot, ...args], - environment: environment, + ['--enable-asserts', snapshot, ...args], + environment: { + ...getPubTestEnvironment(), + ...?environment, + }, workingDirectory: workingDirextory, ); await process.shouldExit(exitCode); @@ -71,14 +75,14 @@ d.pubspec({ 'name': 'myapp', 'environment': { - 'sdk': '>=2.0.0 <3.0.0', + 'sdk': '0.1.2+3', }, }), d.dir('bin', [ d.file('main.dart', ''' import 'dart:io'; main() { - print("Hi"); + print('Hi'); exit(123); } ''') @@ -100,4 +104,71 @@ 'test/embedding/goldens/run.txt', ); }); + + test('analytics', () async { + await servePackages((b) => b + ..serve('foo', '1.0.0', deps: {'bar': 'any'}) + ..serve('bar', '1.0.0')); + await d.dir('dep', [ + d.pubspec({ + 'name': 'dep', + 'environment': {'sdk': '>=0.0.0 <3.0.0'} + }) + ]).create(); + final app = d.dir(appPath, [ + d.appPubspec({ + 'foo': '1.0.0', + // The path dependency should not go to analytics. + 'dep': {'path': '../dep'} + }) + ]); + await app.create(); + + final buffer = StringBuffer(); + + await runEmbedding( + ['pub', 'get'], + buffer, + workingDirextory: app.io.path, + environment: {...getPubTestEnvironment(), '_PUB_LOG_ANALYTICS': 'true'}, + ); + final analytics = buffer + .toString() + .split('\n') + .where((line) => line.startsWith('[E] [analytics]: ')) + .map((line) => json.decode(line.substring('[E] [analytics]: '.length))); + expect(analytics, { + { + 'hitType': 'event', + 'message': { + 'category': 'pub-get', + 'action': 'foo', + 'label': '1.0.0', + 'value': 1, + 'cd1': 'direct', + 'ni': '1', + } + }, + { + 'hitType': 'event', + 'message': { + 'category': 'pub-get', + 'action': 'bar', + 'label': '1.0.0', + 'value': 1, + 'cd1': 'transitive', + 'ni': '1', + } + }, + { + 'hitType': 'timing', + 'message': { + 'variableName': 'resolution', + 'time': isA<int>(), + 'category': 'pub-get', + 'label': null + } + }, + }); + }); }
diff --git a/test/test_pub.dart b/test/test_pub.dart index 356b959..3c6b94b 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); @@ -379,14 +387,19 @@ /// package server. /// /// Any futures in [args] will be resolved before the process is started. -Future<PubProcess> startPublish(PackageServer server, - {List<String> args}) async { +Future<PubProcess> startPublish( + PackageServer server, { + List<String> args, + String authMethod = 'oauth2', + Map<String, String> environment, +}) async { var tokenEndpoint = Uri.parse(server.url).resolve('/token').toString(); args = ['lish', ...?args]; - return await startPub( - args: args, - tokenEndpoint: tokenEndpoint, - environment: {'PUB_HOSTED_URL': server.url}); + return await startPub(args: args, tokenEndpoint: tokenEndpoint, environment: { + 'PUB_HOSTED_URL': server.url, + '_PUB_TEST_AUTH_METHOD': authMethod, + if (environment != null) ...environment, + }); } /// Handles the beginning confirmation process for uploading a packages. @@ -460,21 +473,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 +496,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/error_message_test.dart b/test/token/error_message_test.dart new file mode 100644 index 0000000..c5ebc80 --- /dev/null +++ b/test/token/error_message_test.dart
@@ -0,0 +1,87 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// 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. + +// @dart=2.10 + +import 'package:shelf/shelf.dart' as shelf; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void respondWithWwwAuthenticate(String headerValue) { + globalPackageServer.expect('GET', '/api/packages/versions/new', (request) { + return shelf.Response(403, headers: {'www-authenticate': headerValue}); + }); +} + +Future<void> expectPubErrorMessage(dynamic matcher) { + return runPub( + args: ['lish'], + environment: { + 'PUB_HOSTED_URL': globalPackageServer.url, + '_PUB_TEST_AUTH_METHOD': 'token', + }, + exitCode: 65, + input: ['y'], + error: matcher, + ); +} + +void main() { + setUp(() async { + await d.validPackage.create(); + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'token': 'access token'}, + ] + }).create(); + }); + + test('prints www-authenticate message', () async { + respondWithWwwAuthenticate('bearer realm="pub", message="custom message"'); + await expectPubErrorMessage(contains('custom message')); + }); + + test('sanitizes and prints dirty www-authenticate message', () { + // Unable to test this case because shelf does not allow characters [1] + // that pub cli supposed to sanitize. + // + // [1] https://github.com/dart-lang/sdk/blob/main/sdk/lib/_http/http_headers.dart#L653-L662 + }); + + test('trims and prints long www-authenticate message', () async { + var message = List.generate(2048, (_) => 'a').join(); + + respondWithWwwAuthenticate('bearer realm="pub", message="$message"'); + await expectPubErrorMessage(allOf( + isNot(contains(message)), + contains(message.substring(0, 1024)), + )); + }); + + test('does not prints message if realm is not equals to pub', () async { + respondWithWwwAuthenticate('bearer realm="web", message="custom message"'); + await expectPubErrorMessage(isNot(contains('custom message'))); + }); + + test('does not prints message if challenge is not equals to bearer', + () async { + respondWithWwwAuthenticate('basic realm="pub", message="custom message"'); + await expectPubErrorMessage(isNot(contains('custom message'))); + }); + + test('prints message for bearer challenge for pub realm only', () async { + respondWithWwwAuthenticate( + 'basic realm="pub", message="enter username and password", ' + 'newAuth message="use web portal to login", ' + 'bearer realm="api", message="contact IT dept to enroll", ' + 'bearer realm="pub", ' + 'bearer realm="pub", message="pub realm message"', + ); + await expectPubErrorMessage(contains('pub realm message')); + }); +}
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, + ); + }); }
diff --git a/test/token/token_authentication_test.dart b/test/token/token_authentication_test.dart new file mode 100644 index 0000000..94574c7 --- /dev/null +++ b/test/token/token_authentication_test.dart
@@ -0,0 +1,48 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// 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. + +// @dart=2.10 + +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../lish/utils.dart'; +import '../test_pub.dart'; + +void main() { + setUp(d.validPackage.create); + + test('with a pre existing environment token authenticates', () async { + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'env': 'TOKEN'}, + ] + }).create(); + var pub = await startPublish(globalPackageServer, + authMethod: 'token', environment: {'TOKEN': 'access token'}); + await confirmPublish(pub); + + handleUploadForm(globalPackageServer); + + await pub.shouldExit(1); + }); + + test('with a pre existing opaque token authenticates', () async { + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'token': 'access token'}, + ] + }).create(); + var pub = await startPublish(globalPackageServer, authMethod: 'token'); + await confirmPublish(pub); + + handleUploadForm(globalPackageServer); + + await pub.shouldExit(1); + }); +}
diff --git a/test/token/when_receives_401_removes_token_test.dart b/test/token/when_receives_401_removes_token_test.dart new file mode 100644 index 0000000..cb1f3e4 --- /dev/null +++ b/test/token/when_receives_401_removes_token_test.dart
@@ -0,0 +1,35 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// 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. + +// @dart=2.10 + +import 'package:shelf/shelf.dart' as shelf; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() { + setUp(d.validPackage.create); + + test('when receives 401 response removes saved token', () async { + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'token': 'access token'}, + ] + }).create(); + var pub = await startPublish(globalPackageServer, authMethod: 'token'); + await confirmPublish(pub); + + globalPackageServer.expect('GET', '/api/packages/versions/new', (request) { + return shelf.Response(401); + }); + + await pub.shouldExit(65); + + await d.tokensFile({'version': 1, 'hosted': []}).validate(); + }); +}
diff --git a/test/token/when_receives_403_persists_saved_token_test.dart b/test/token/when_receives_403_persists_saved_token_test.dart new file mode 100644 index 0000000..4035e5f --- /dev/null +++ b/test/token/when_receives_403_persists_saved_token_test.dart
@@ -0,0 +1,40 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// 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. + +// @dart=2.10 + +import 'package:shelf/shelf.dart' as shelf; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() { + setUp(d.validPackage.create); + + test('when receives 403 response persists saved token', () async { + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'token': 'access token'}, + ] + }).create(); + var pub = await startPublish(globalPackageServer, authMethod: 'token'); + await confirmPublish(pub); + + globalPackageServer.expect('GET', '/api/packages/versions/new', (request) { + return shelf.Response(403); + }); + + await pub.shouldExit(65); + + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalPackageServer.url, 'token': 'access token'}, + ] + }).validate(); + }); +}
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart index d6140f5..06d3be4 100644 --- a/test/validator/gitignore_test.dart +++ b/test/validator/gitignore_test.dart
@@ -4,18 +4,23 @@ // @dart=2.10 +import 'package:path/path.dart' as p; import 'package:pub/src/exit_codes.dart' as exit_codes; import 'package:test/test.dart'; import '../descriptor.dart' as d; import '../test_pub.dart'; -Future<void> expectValidation(error, int exitCode) async { +Future<void> expectValidation( + error, + int exitCode, { + String workingDirectory, +}) async { await runPub( error: error, args: ['publish', '--dry-run'], environment: {'_PUB_TEST_SDK_VERSION': '2.12.0'}, - workingDirectory: d.path(appPath), + workingDirectory: workingDirectory ?? d.path(appPath), exitCode: exitCode, ); } @@ -46,4 +51,37 @@ ]), exit_codes.DATA); }); + + test('Should also consider gitignores from above the package root', () async { + await d.git('reporoot', [ + d.dir( + 'myapp', + [ + d.file('foo.txt'), + ...d.validPackage.contents, + ], + ), + ]).create(); + final packageRoot = p.join(d.sandbox, 'reporoot', 'myapp'); + await pubGet( + environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'}, + workingDirectory: packageRoot); + + await expectValidation(contains('Package has 0 warnings.'), 0, + workingDirectory: packageRoot); + + await d.dir('reporoot', [ + d.file('.gitignore', '*.txt'), + ]).create(); + + await expectValidation( + allOf([ + contains('Package has 1 warning.'), + contains('foo.txt'), + contains( + 'Consider adjusting your `.gitignore` files to not ignore those files'), + ]), + exit_codes.DATA, + workingDirectory: packageRoot); + }); }
diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart index 2470a91..810c925 100644 --- a/tool/test-bin/pub_command_runner.dart +++ b/tool/test-bin/pub_command_runner.dart
@@ -1,22 +1,28 @@ -// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file // 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. -// @dart=2.10 - +/// A trivial embedding of the pub command. Used from tests. +// @dart = 2.11 +import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; +import 'package:pub/pub.dart'; import 'package:pub/src/exit_codes.dart' as exit_codes; import 'package:pub/src/log.dart' as log; -import 'package:pub/src/pub_embeddable_command.dart'; +import 'package:usage/usage.dart'; class Runner extends CommandRunner<int> { ArgResults _options; Runner() : super('pub_command_runner', 'Tests the embeddable pub command.') { - addCommand(PubEmbeddableCommand()); + final analytics = Platform.environment['_PUB_LOG_ANALYTICS'] == 'true' + ? PubAnalytics(_LoggingAnalytics(), + dependencyKindCustomDimensionName: 'cd1') + : null; + addCommand(pubCommand(analytics: analytics)); } @override @@ -40,3 +46,51 @@ Future<void> main(List<String> arguments) async { exitCode = await Runner().run(arguments); } + +class _LoggingAnalytics extends AnalyticsMock { + _LoggingAnalytics() { + onSend.listen((event) { + stderr.writeln('[analytics]${json.encode(event)}'); + }); + } + + @override + bool get firstRun => false; + + @override + Future sendScreenView(String viewName, {Map<String, String> parameters}) { + parameters ??= <String, String>{}; + parameters['viewName'] = viewName; + return _log('screenView', parameters); + } + + @override + Future sendEvent(String category, String action, + {String label, int value, Map<String, String> parameters}) { + parameters ??= <String, String>{}; + return _log( + 'event', + {'category': category, 'action': action, 'label': label, 'value': value} + ..addAll(parameters)); + } + + @override + Future sendSocial(String network, String action, String target) => + _log('social', {'network': network, 'action': action, 'target': target}); + + @override + Future sendTiming(String variableName, int time, + {String category, String label}) { + return _log('timing', { + 'variableName': variableName, + 'time': time, + 'category': category, + 'label': label + }); + } + + Future<void> _log(String hitType, Map message) async { + final encoded = json.encode({'hitType': hitType, 'message': message}); + stderr.writeln('[analytics]: $encoded'); + } +}