diff --git a/analysis_options.yaml b/analysis_options.yaml index f07a060..6177eba 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml
@@ -31,6 +31,7 @@ - sort_pub_dependencies - test_types_in_equals - throw_in_finally + - unawaited_futures - unnecessary_lambdas - unnecessary_null_aware_assignments - unnecessary_parenthesis
diff --git a/doc/repository-spec-v2.md b/doc/repository-spec-v2.md index aa65e14..6ddfd0f 100644 --- a/doc/repository-spec-v2.md +++ b/doc/repository-spec-v2.md
@@ -227,7 +227,7 @@ "replacedBy": "<package>", /* optional field, if isDiscontinued == true */ "latest": { "version": "<version>", - "isRetracted": true || false, /* optional field, false if omitted */ + "retracted": true || false, /* optional field, false if omitted */ "archive_url": "https://.../archive.tar.gz", "pubspec": { /* pubspec contents as JSON object */ @@ -236,7 +236,7 @@ "versions": [ { "version": "<package>", - "isRetracted": true || false, /* optional field, false if omitted */ + "retracted": true || false, /* optional field, false if omitted */ "archive_url": "https://.../archive.tar.gz", "pubspec": { /* pubspec contents as JSON object */
diff --git a/lib/pub.dart b/lib/pub.dart index 5b3054f..71de9d6 100644 --- a/lib/pub.dart +++ b/lib/pub.dart
@@ -18,8 +18,12 @@ /// /// If [analytics] is given, pub will use that analytics instance to send /// statistics about resolutions. -Command<int> pubCommand({PubAnalytics? analytics}) => - PubEmbeddableCommand(analytics); +/// +/// [isVerbose] should return `true` (after argument resolution) if the +/// embedding top-level is in verbose mode. +Command<int> pubCommand( + {PubAnalytics? analytics, required bool Function() isVerbose}) => + PubEmbeddableCommand(analytics, isVerbose); /// Support for the `pub` toplevel command. @Deprecated('Use [pubCommand] instead.')
diff --git a/lib/src/command.dart b/lib/src/command.dart index 3f06db1..556c04f 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart
@@ -10,6 +10,7 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; import 'authentication/token_store.dart'; import 'command_runner.dart'; @@ -55,7 +56,11 @@ return a; } - String get directory => argResults['directory'] ?? _pubTopLevel.directory; + String get directory => + (argResults.options.contains('directory') + ? argResults['directory'] + : null) ?? + _pubTopLevel.directory; late final SystemCache cache = SystemCache(isOffline: isOffline); @@ -170,12 +175,11 @@ @nonVirtual FutureOr<int> run() async { computeCommand(_pubTopLevel.argResults); - if (_pubTopLevel.trace) { - log.recordTranscript(); - } + log.verbosity = _pubTopLevel.verbosity; log.fine('Pub ${sdk.version}'); + var crashed = false; try { await captureErrors<void>(() async => runProtected(), captureStackChains: _pubTopLevel.captureStackChains); @@ -187,8 +191,24 @@ log.exception(error, chain); if (_pubTopLevel.trace) { - log.dumpTranscript(); + log.dumpTranscriptToStdErr(); } else if (!isUserFacingException(error)) { + log.error(''' +This is an unexpected error. The full log and other details are collected in: + + $transcriptPath + +Consider creating an issue on https://github.com/dart-lang/pub/issues/new +and attaching the relevant parts of that log file. +'''); + crashed = true; + } + return _chooseExitCode(error); + } finally { + final verbose = _pubTopLevel.verbosity == log.Verbosity.all; + + // Write the whole log transcript to file. + if (verbose || crashed) { // Escape the argument for users to copy-paste in bash. // Wrap with single quotation, and use '\'' to insert single quote, as // long as we have no spaces this doesn't create a new argument. @@ -196,16 +216,23 @@ RegExp(r'^[a-zA-Z0-9-_]+$').stringMatch(x) == null ? "'${x.replaceAll("'", r"'\''")}'" : x; - log.error(""" -This is an unexpected error. Please run - dart pub --trace ${_topCommand.name} ${_topCommand.argResults!.arguments.map(protectArgument).join(' ')} + late final Entrypoint? e; + try { + e = entrypoint; + } on ApplicationException { + e = null; + } + log.dumpTranscriptToFile( + transcriptPath, + 'dart pub ${_topCommand.argResults!.arguments.map(protectArgument).join(' ')}', + e, + ); -and include the logs in an issue on https://github.com/dart-lang/pub/issues/new -"""); + if (!crashed) { + log.message('Logs written to $transcriptPath.'); + } } - return _chooseExitCode(error); - } finally { httpClient.close(); } } @@ -288,6 +315,10 @@ } _command = list.join(' '); } + + String get transcriptPath { + return p.join(cache.rootDir, 'log', 'pub_log.txt'); + } } abstract class PubTopLevel {
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart index f268381..328d935 100644 --- a/lib/src/command/add.dart +++ b/lib/src/command/add.dart
@@ -13,6 +13,7 @@ import '../exceptions.dart'; import '../git.dart'; import '../io.dart'; +import '../language_version.dart'; import '../log.dart' as log; import '../package.dart'; import '../package_name.dart'; @@ -33,9 +34,10 @@ @override String get name => 'add'; @override - String get description => 'Add a dependency to pubspec.yaml.'; + String get description => 'Add dependencies to pubspec.yaml.'; @override - String get argumentsDescription => '<package>[:<constraint>] [options]'; + String get argumentsDescription => + '<package>[:<constraint>] [<package2>[:<constraint2>]...] [options]'; @override String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add'; @override @@ -53,19 +55,24 @@ bool get hasGitOptions => gitUrl != null || gitRef != null || gitPath != null; bool get hasHostOptions => hostUrl != null; + bool get isHosted => !hasGitOptions && path == null && path == null; + AddCommand() { argParser.addFlag('dev', abbr: 'd', negatable: false, - help: 'Adds package to the development dependencies instead.'); + help: 'Adds to the development dependencies instead.'); argParser.addOption('git-url', help: 'Git URL of the package'); argParser.addOption('git-ref', help: 'Git branch or commit to be retrieved'); argParser.addOption('git-path', help: 'Path of git package in repository'); argParser.addOption('hosted-url', help: 'URL of package host server'); - argParser.addOption('path', help: 'Local path'); - argParser.addOption('sdk', help: 'SDK source for package'); + argParser.addOption('path', help: 'Add package from local path'); + argParser.addOption('sdk', + help: 'add package from SDK source', + allowed: ['flutter'], + valueHelp: '[flutter]'); argParser.addFlag( 'example', help: @@ -84,23 +91,28 @@ argParser.addFlag('precompile', help: 'Build executables in immediate dependencies.'); argParser.addOption('directory', - abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir'); + abbr: 'C', help: 'Run this in the directory <dir>.', valueHelp: 'dir'); } @override Future<void> runProtected() async { if (argResults.rest.isEmpty) { - usageException('Must specify a package to be added.'); - } else if (argResults.rest.length > 1) { - usageException('Takes only a single argument.'); + usageException('Must specify at least one package to be added.'); + } else if (argResults.rest.length > 1 && gitUrl != null) { + usageException('Can only add a single git package at a time.'); + } else if (argResults.rest.length > 1 && path != null) { + usageException('Can only add a single local package at a time.'); } + final languageVersion = entrypoint.root.pubspec.languageVersion; + final updates = + argResults.rest.map((p) => _parsePackage(p, languageVersion)).toList(); - final packageInformation = _parsePackage(argResults.rest.first); - final package = packageInformation.first; - - /// Perform version resolution in-memory. - final updatedPubSpec = - await _addPackageToPubspec(entrypoint.root.pubspec, package); + var updatedPubSpec = entrypoint.root.pubspec; + for (final update in updates) { + /// Perform version resolution in-memory. + updatedPubSpec = + await _addPackageToPubspec(updatedPubSpec, update.packageRange); + } late SolveResult solveResult; @@ -113,7 +125,9 @@ solveResult = await resolveVersions( SolveType.upgrade, cache, Package.inMemory(updatedPubSpec)); } on GitException { - dataError('Unable to resolve package "${package.name}" with the given ' + final packageRange = updates.first.packageRange; + dataError( + 'Unable to resolve package "${packageRange.name}" with the given ' 'git parameters.'); } on SolveFailure catch (e) { dataError(e.message); @@ -122,33 +136,34 @@ dataError(e.message); } - final resultPackage = solveResult.packages - .firstWhere((packageId) => packageId.name == package.name); + /// Verify the results for each package. + for (final update in updates) { + final packageRange = update.packageRange; + final name = packageRange.name; + final resultPackage = solveResult.packages + .firstWhere((packageId) => packageId.name == name); - /// Assert that [resultPackage] is within the original user's expectations. - var constraint = package.constraint; - if (!constraint.allows(resultPackage.version)) { - var dependencyOverrides = updatedPubSpec.dependencyOverrides; - if (dependencyOverrides.isNotEmpty) { - dataError( - '"${package.name}" resolved to "${resultPackage.version}" which ' - 'does not satisfy constraint "${package.constraint}". This could be ' - 'caused by "dependency_overrides".'); + /// Assert that [resultPackage] is within the original user's expectations. + var constraint = packageRange.constraint; + if (!constraint.allows(resultPackage.version)) { + var dependencyOverrides = updatedPubSpec.dependencyOverrides; + if (dependencyOverrides.isNotEmpty) { + dataError('"$name" resolved to "${resultPackage.version}" which ' + 'does not satisfy constraint "$constraint". This could be ' + 'caused by "dependency_overrides".'); + } + dataError('"$name" resolved to "${resultPackage.version}" which ' + 'does not satisfy constraint "$constraint".'); } - dataError( - '"${package.name}" resolved to "${resultPackage.version}" which ' - 'does not satisfy constraint "${package.constraint}".'); } - if (isDryRun) { /// Even if it is a dry run, run `acquireDependencies` so that the user /// gets a report on the other packages that might change version due /// to this new dependency. final newRoot = Package.inMemory(updatedPubSpec); - // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output - await Entrypoint.global(newRoot, entrypoint.lockFile, cache, - solveResult: solveResult) + await Entrypoint.inMemory(newRoot, cache, + solveResult: solveResult, lockFile: entrypoint.lockFile) .acquireDependencies(SolveType.get, dryRun: true, precompile: argResults['precompile'], @@ -158,7 +173,7 @@ /// ensure that the modification timestamp on `pubspec.lock` and /// `.dart_tool/package_config.json` is newer than `pubspec.yaml`, /// ensuring that [entrypoint.assertUptoDate] will pass. - _updatePubspec(resultPackage, packageInformation, isDev); + _updatePubspec(solveResult.packages, updates, isDev); /// Create a new [Entrypoint] since we have to reprocess the updated /// pubspec file. @@ -274,9 +289,7 @@ /// /// If any of the other git options are defined when `--git-url` is not /// defined, an error will be thrown. - Pair<PackageRange, dynamic> _parsePackage(String package) { - ArgumentError.checkNotNull(package, 'package'); - + _ParseResult _parsePackage(String package, LanguageVersion languageVersion) { final _conflictingFlagSets = [ ['git-url', 'git-ref', 'git-path'], ['hosted-url'], @@ -380,13 +393,18 @@ .withConstraint(constraint ?? VersionConstraint.any); pubspecInformation = {'sdk': sdk}; } else { - final hostInfo = - hasHostOptions ? {'url': hostUrl, 'name': packageName} : null; - - if (hostInfo == null) { - pubspecInformation = constraint?.toString(); + // Hosted + final Object? hostInfo; + if (hasHostOptions) { + hostInfo = languageVersion.supportsShorterHostedSyntax + ? hostUrl + : {'url': hostUrl, 'name': packageName}; + pubspecInformation = { + 'hosted': hostInfo, + }; } else { - pubspecInformation = {'hosted': hostInfo}; + hostInfo = null; + pubspecInformation = constraint?.toString(); } packageRange = cache.hosted.source @@ -408,66 +426,64 @@ }; } - return Pair(packageRange, pubspecInformation); + return _ParseResult(packageRange, pubspecInformation); } /// Writes the changes to the pubspec file. - void _updatePubspec(PackageId resultPackage, - Pair<PackageRange, dynamic> packageInformation, bool isDevelopment) { - ArgumentError.checkNotNull(resultPackage, 'resultPackage'); - ArgumentError.checkNotNull(packageInformation, 'pubspecInformation'); - - final package = packageInformation.first; - var pubspecInformation = packageInformation.last; - - if ((sdk != null || hasHostOptions) && - pubspecInformation is Map && - pubspecInformation['version'] == null) { - /// We cannot simply assign the value of version since it is likely that - /// [pubspecInformation] takes on the type - /// [Map<String, Map<String, String>>] - pubspecInformation = { - ...pubspecInformation, - 'version': '^${resultPackage.version}' - }; - } - - final dependencyKey = isDevelopment ? 'dev_dependencies' : 'dependencies'; - final packagePath = [dependencyKey, package.name]; - + void _updatePubspec(List<PackageId> resultPackages, + List<_ParseResult> updates, bool isDevelopment) { final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath)); log.io('Reading ${entrypoint.pubspecPath}.'); log.fine('Contents:\n$yamlEditor'); - /// Handle situations where the user might not have the dependencies or - /// dev_dependencies map. - if (yamlEditor.parseAt([dependencyKey], - orElse: () => YamlScalar.wrap(null)).value == - null) { - yamlEditor.update([dependencyKey], - {package.name: pubspecInformation ?? '^${resultPackage.version}'}); - } else { - yamlEditor.update( - packagePath, pubspecInformation ?? '^${resultPackage.version}'); - } + for (final update in updates) { + final packageRange = update.packageRange; + final name = packageRange.name; + final resultId = resultPackages.firstWhere((id) => id.name == name); + var description = update.description; - log.fine('Added ${package.name} to "$dependencyKey".'); - - /// Remove the package from dev_dependencies if we are adding it to - /// dependencies. Refer to [_addPackageToPubspec] for additional discussion. - if (!isDevelopment) { - final devDependenciesNode = yamlEditor - .parseAt(['dev_dependencies'], orElse: () => YamlScalar.wrap(null)); - - if (devDependenciesNode is YamlMap && - devDependenciesNode.containsKey(package.name)) { - if (devDependenciesNode.length == 1) { - yamlEditor.remove(['dev_dependencies']); - } else { - yamlEditor.remove(['dev_dependencies', package.name]); + if (isHosted) { + final inferredConstraint = + VersionConstraint.compatibleWith(resultId.version).toString(); + if (description == null) { + description = inferredConstraint; + } else if (description is Map && description['version'] == null) { + /// We cannot simply assign the value of version since it is likely that + /// [description] takes on the type + /// [Map<String, Map<String, String>>] + description = {...description, 'version': '^${resultId.version}'}; } + } - log.fine('Removed ${package.name} from "dev_dependencies".'); + final dependencyKey = isDevelopment ? 'dev_dependencies' : 'dependencies'; + final packagePath = [dependencyKey, name]; + + /// Ensure we have a [dependencyKey] map in the `pubspec.yaml`. + if (yamlEditor.parseAt([dependencyKey], + orElse: () => YamlScalar.wrap(null)).value == + null) { + yamlEditor.update([dependencyKey], {}); + } + yamlEditor.update(packagePath, description); + + log.fine('Added ${packageRange.name} to "$dependencyKey".'); + + /// Remove the package from dev_dependencies if we are adding it to + /// dependencies. Refer to [_addPackageToPubspec] for additional discussion. + if (!isDevelopment) { + final devDependenciesNode = yamlEditor + .parseAt(['dev_dependencies'], orElse: () => YamlScalar.wrap(null)); + + if (devDependenciesNode is YamlMap && + devDependenciesNode.containsKey(name)) { + if (devDependenciesNode.length == 1) { + yamlEditor.remove(['dev_dependencies']); + } else { + yamlEditor.remove(['dev_dependencies', name]); + } + + log.fine('Removed $name from "dev_dependencies".'); + } } } @@ -475,3 +491,9 @@ writeTextFile(entrypoint.pubspecPath, yamlEditor.toString()); } } + +class _ParseResult { + PackageRange packageRange; + Object? description; + _ParseResult(this.packageRange, this.description); +}
diff --git a/lib/src/command/cache_repair.dart b/lib/src/command/cache_repair.dart index 41bae2b..74968be 100644 --- a/lib/src/command/cache_repair.dart +++ b/lib/src/command/cache_repair.dart
@@ -23,6 +23,8 @@ @override Future<void> runProtected() async { + // Delete any eventual temp-files left in the cache. + cache.deleteTempDir(); // Repair every cached source. final repairResults = (await Future.wait( cache.sources.all.map(cache.source).map((source) async {
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index b226c55..c927e07 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart
@@ -91,7 +91,7 @@ try { await log.progress('Uploading', () async { - var newUri = server.resolve('/api/packages/versions/new'); + var newUri = server.resolve('api/packages/versions/new'); var response = await client.get(newUri, headers: pubApiHeaders); var parameters = parseJsonResponse(response);
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart index af91d8c..f3b27bd 100644 --- a/lib/src/command/remove.dart +++ b/lib/src/command/remove.dart
@@ -65,7 +65,7 @@ final newPubspec = _removePackagesFromPubspec(rootPubspec, packages); final newRoot = Package.inMemory(newPubspec); - await Entrypoint.global(newRoot, entrypoint.lockFile, cache) + await Entrypoint.inMemory(newRoot, cache, lockFile: entrypoint.lockFile) .acquireDependencies(SolveType.get, precompile: argResults['precompile'], dryRun: true,
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 34e061d..3063770 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -222,11 +222,10 @@ if (_dryRun) { // Even if it is a dry run, run `acquireDependencies` so that the user // gets a report on changes. - // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output - await Entrypoint.global( + await Entrypoint.inMemory( Package.inMemory(resolvablePubspec), - entrypoint.lockFile, cache, + lockFile: entrypoint.lockFile, solveResult: solveResult, ).acquireDependencies( SolveType.upgrade, @@ -317,10 +316,10 @@ // Even if it is a dry run, run `acquireDependencies` so that the user // gets a report on changes. // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output - await Entrypoint.global( + await Entrypoint.inMemory( Package.inMemory(nullsafetyPubspec), - entrypoint.lockFile, cache, + lockFile: entrypoint.lockFile, solveResult: solveResult, ).acquireDependencies( SolveType.upgrade,
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 02faaf9..21f568b 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart
@@ -72,6 +72,7 @@ default: // No specific verbosity given, so check for the shortcut. if (argResults['verbose']) return log.Verbosity.all; + if (runningFromTest) return log.Verbosity.testing; return log.Verbosity.normal; } }
diff --git a/lib/src/dart.dart b/lib/src/dart.dart index 97219dc..f4f8686 100644 --- a/lib/src/dart.dart +++ b/lib/src/dart.dart
@@ -146,11 +146,14 @@ String toString() => errors.join('\n'); } -/// Precompiles the Dart executable at [executablePath] to a kernel file at -/// [outputPath]. +/// Precompiles the Dart executable at [executablePath]. /// -/// This file is also cached at [incrementalDillOutputPath] which is used to -/// initialize the compiler on future runs. +/// If the compilation succeeds it is saved to a kernel file at [outputPath]. +/// +/// If compilation fails, the output is cached at [incrementalDillOutputPath]. +/// +/// Whichever of [incrementalDillOutputPath] and [outputPath] already exists is +/// used to initialize the compiler run. /// /// The [packageConfigPath] should point at the package config file to be used /// for `package:` uri resolution. @@ -158,39 +161,65 @@ /// The [name] is used to describe the executable in logs and error messages. Future<void> precompile({ required String executablePath, - required String incrementalDillOutputPath, + required String incrementalDillPath, required String name, required String outputPath, required String packageConfigPath, }) async { ensureDir(p.dirname(outputPath)); - ensureDir(p.dirname(incrementalDillOutputPath)); + ensureDir(p.dirname(incrementalDillPath)); + const platformDill = 'lib/_internal/vm_platform_strong.dill'; final sdkRoot = p.relative(p.dirname(p.dirname(Platform.resolvedExecutable))); - var client = await FrontendServerClient.start( - executablePath, - incrementalDillOutputPath, - platformDill, - sdkRoot: sdkRoot, - packagesJson: packageConfigPath, - printIncrementalDependencies: false, - ); + String? tempDir; + FrontendServerClient? client; try { - var result = await client.compile(); + tempDir = createTempDir(p.dirname(incrementalDillPath), 'tmp'); + // To avoid potential races we copy the incremental data to a temporary file + // for just this compilation. + final temporaryIncrementalDill = + p.join(tempDir, '${p.basename(incrementalDillPath)}.incremental.dill'); + try { + if (fileExists(incrementalDillPath)) { + copyFile(incrementalDillPath, temporaryIncrementalDill); + } else if (fileExists(outputPath)) { + copyFile(outputPath, temporaryIncrementalDill); + } + } on FileSystemException { + // Not able to copy existing file, compilation will start from scratch. + } + + client = await FrontendServerClient.start( + executablePath, + temporaryIncrementalDill, + platformDill, + sdkRoot: sdkRoot, + packagesJson: packageConfigPath, + printIncrementalDependencies: false, + ); + final result = await client.compile(); final highlightedName = log.bold(name); if (result?.errorCount == 0) { log.message('Built $highlightedName.'); - await File(incrementalDillOutputPath).copy(outputPath); + // By using rename we ensure atomicity. An external observer will either + // see the old or the new snapshot. + renameFile(temporaryIncrementalDill, outputPath); } else { - // Don't leave partial results. - deleteEntry(outputPath); + // By using rename we ensure atomicity. An external observer will either + // see the old or the new snapshot. + renameFile(temporaryIncrementalDill, incrementalDillPath); + // If compilation failed we don't want to leave an incorrect snapshot. + tryDeleteEntry(outputPath); throw ApplicationException( log.yellow('Failed to build $highlightedName:\n') + (result?.compilerOutputLines.join('\n') ?? '')); } } finally { - client.kill(); + client?.kill(); + if (tempDir != null) { + tryDeleteEntry(tempDir); + } } }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index 453ef56..26a8243 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -10,12 +10,12 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; import 'command_runner.dart'; import 'dart.dart' as dart; import 'exceptions.dart'; import 'executable.dart'; -import 'http.dart' as http; import 'io.dart'; import 'language_version.dart'; import 'lock_file.dart'; @@ -72,8 +72,14 @@ /// but may be the entrypoint when you're running its tests. class Entrypoint { /// The root package this entrypoint is associated with. + /// + /// For a global package, this is the activated package. final Package root; + /// For a global package, this is the directory that the package is installed + /// in. Non-global packages have null. + final String? globalDir; + /// The system-wide cache which caches packages that need to be fetched over /// the network. final SystemCache cache; @@ -82,7 +88,8 @@ bool get isCached => !root.isInMemory && p.isWithin(cache.rootDir, root.dir); /// Whether this is an entrypoint for a globally-activated package. - final bool isGlobal; + // final bool isGlobal; + bool get isGlobal => globalDir != null; /// The lockfile for the entrypoint. /// @@ -122,8 +129,7 @@ /// /// Global packages (except those from path source) /// store these in the global cache. - String? get _configRoot => - isCached ? p.join(cache.rootDir, 'global_packages', root.name) : root.dir; + String? get _configRoot => isCached ? globalDir : root.dir; /// The path to the entrypoint's ".packages" file. /// @@ -152,11 +158,7 @@ /// but the configuration is stored at the package itself. String get cachePath { if (isGlobal) { - return p.join( - cache.rootDir, - 'global_packages', - root.name, - ); + return globalDir!; } else { var newPath = root.path('.dart_tool/pub'); var oldPath = root.path('.pub'); @@ -173,15 +175,25 @@ String get _incrementalDillsPath => p.join(cachePath, 'incremental'); /// Loads the entrypoint from a package at [rootDir]. - Entrypoint(String rootDir, this.cache) - : root = Package.load(null, rootDir, cache.sources), - isGlobal = false; + Entrypoint( + String rootDir, + this.cache, + ) : root = Package.load(null, rootDir, cache.sources), + globalDir = null; + + Entrypoint.inMemory(this.root, this.cache, + {required LockFile? lockFile, SolveResult? solveResult}) + : _lockFile = lockFile, + globalDir = null { + if (solveResult != null) { + _packageGraph = PackageGraph.fromSolveResult(this, solveResult); + } + } /// Creates an entrypoint given package and lockfile objects. /// If a SolveResult is already created it can be passed as an optimization. - Entrypoint.global(this.root, this._lockFile, this.cache, - {SolveResult? solveResult}) - : isGlobal = true { + Entrypoint.global(this.globalDir, this.root, this._lockFile, this.cache, + {SolveResult? solveResult}) { if (solveResult != null) { _packageGraph = PackageGraph.fromSolveResult(this, solveResult); } @@ -202,18 +214,20 @@ /// Writes .packages and .dart_tool/package_config.json Future<void> writePackagesFiles() async { + final entrypointName = isGlobal ? null : root.name; writeTextFile( packagesFile, lockFile.packagesFile(cache, - entrypoint: root.name, relativeFrom: root.dir)); + entrypoint: entrypointName, + relativeFrom: isGlobal ? null : root.dir)); ensureDir(p.dirname(packageConfigFile)); writeTextFile( packageConfigFile, await lockFile.packageConfigFile(cache, - entrypoint: root.name, + entrypoint: entrypointName, entrypointSdkConstraint: root.pubspec.sdkConstraints[sdk.identifier], - relativeFrom: root.dir)); + relativeFrom: isGlobal ? null : root.dir)); } /// Gets all dependencies of the [root] package. @@ -250,8 +264,7 @@ SolveResult result; try { result = await log.progress('Resolving dependencies$suffix', () async { - // We require an SDK constraint lower-bound as of Dart 2.12.0 - _checkSdkConstraintIsDefined(root.pubspec); + _checkSdkConstraint(root.pubspec); return resolveVersions( type, cache, @@ -294,7 +307,7 @@ await result.showReport(type, cache); } if (!dryRun) { - await Future.wait(result.packages.map(_get)); + await result.downloadCachedPackages(cache); _saveLockFile(result); } if (onlyReportSuccessOrFailure) { @@ -387,10 +400,11 @@ Future<void> _precompileExecutable(Executable executable) async { final package = executable.package; + await dart.precompile( executablePath: resolveExecutable(executable), outputPath: pathOfExecutable(executable), - incrementalDillOutputPath: incrementalDillPathOfExecutable(executable), + incrementalDillPath: incrementalDillPathOfExecutable(executable), packageConfigPath: packageConfigFile, name: '$package:${p.basenameWithoutExtension(executable.relativePath)}'); @@ -470,21 +484,6 @@ } } - /// Makes sure the package at [id] is locally available. - /// - /// This automatically downloads the package to the system-wide cache as well - /// if it requires network access to retrieve (specifically, if the package's - /// source is a [CachedSource]). - Future<void> _get(PackageId id) async { - return await http.withDependencyType(root.dependencyType(id.name), - () async { - if (id.isRoot) return; - - var source = cache.source(id.source); - if (source is CachedSource) await source.downloadToSystemCache(id); - }); - } - /// Throws a [DataError] if the `.dart_tool/package_config.json` file doesn't /// exist or if it's out-of-date relative to the lockfile or the pubspec. /// @@ -872,7 +871,9 @@ } /// We require an SDK constraint lower-bound as of Dart 2.12.0 - void _checkSdkConstraintIsDefined(Pubspec pubspec) { + /// + /// We don't allow unknown sdks. + void _checkSdkConstraint(Pubspec pubspec) { final dartSdkConstraint = pubspec.sdkConstraints['dart']; if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) { // Suggest version range '>=2.10.0 <3.0.0', we avoid using: @@ -902,6 +903,24 @@ See https://dart.dev/go/sdk-constraint '''); } + for (final sdk in pubspec.sdkConstraints.keys) { + if (!sdks.containsKey(sdk)) { + final environment = pubspec.fields.nodes['environment'] as YamlMap; + final keyNode = environment.nodes.entries + .firstWhere((e) => (e.key as YamlNode).value == sdk) + .key as YamlNode; + throw PubspecException(''' +$pubspecPath refers to an unknown sdk '$sdk'. + +Did you mean to add it as a dependency? + +Either remove the constraint, or upgrade to a version of pub that supports the +given sdk. + +See https://dart.dev/go/sdk-constraint +''', keyNode.span); + } + } } }
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index a868b4b..773f2ec 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart
@@ -12,7 +12,6 @@ import 'entrypoint.dart'; import 'exceptions.dart'; import 'executable.dart' as exec; -import 'http.dart' as http; import 'io.dart'; import 'lock_file.dart'; import 'log.dart' as log; @@ -62,6 +61,8 @@ /// The directory where the lockfiles for activated packages are stored. String get _directory => p.join(cache.rootDir, 'global_packages'); + String _packageDir(String packageName) => p.join(_directory, packageName); + /// The directory where binstubs for global package executables are stored. String get _binStubDir => p.join(cache.rootDir, 'bin'); @@ -151,26 +152,19 @@ // Get the package's dependencies. await entrypoint.acquireDependencies(SolveType.get, analytics: analytics); var name = entrypoint.root.name; - - try { - var originalLockFile = - LockFile.load(_getLockFilePath(name), cache.sources); - // Call this just to log what the current active package is, if any. - _describeActive(originalLockFile, name); - } on IOException { - // Couldn't read the lock file. It probably doesn't exist. - } + _describeActive(name, cache); // Write a lockfile that points to the local package. var fullPath = canonicalize(entrypoint.root.dir); var id = cache.path.source.idFor(name, entrypoint.root.version, fullPath); + final tempDir = cache.createTempDir(); // TODO(rnystrom): Look in "bin" and display list of binaries that // user can run. - _writeLockFile(name, LockFile([id])); + _writeLockFile(tempDir, LockFile([id])); - var binDir = p.join(_directory, name, 'bin'); - if (dirExists(binDir)) deleteEntry(binDir); + tryDeleteEntry(_packageDir(name)); + tryRenameDir(tempDir, _packageDir(name)); _updateBinStubs(entrypoint, entrypoint.root, executables, overwriteBinStubs: overwriteBinStubs); @@ -178,17 +172,12 @@ } /// Installs the package [dep] and its dependencies into the system cache. + /// + /// If [silent] less logging will be printed. Future<void> _installInCache(PackageRange dep, List<String>? executables, - {required bool overwriteBinStubs}) async { - LockFile? originalLockFile; - try { - originalLockFile = - LockFile.load(_getLockFilePath(dep.name), cache.sources); - // Call this just to log what the current active package is, if any. - _describeActive(originalLockFile, dep.name); - } on IOException { - // Couldn't read the lock file. It probably doesn't exist. - } + {required bool overwriteBinStubs, bool silent = false}) async { + final name = dep.name; + LockFile? originalLockFile = _describeActive(name, cache); // Create a dummy package with just [dep] so we can do resolution on it. var root = Package.inMemory(Pubspec('pub global activate', @@ -200,103 +189,89 @@ // being available, report that as a [dataError]. SolveResult result; try { - result = await log.progress('Resolving dependencies', - () => resolveVersions(SolveType.get, cache, root)); + result = await log.spinner( + 'Resolving dependencies', + () => resolveVersions(SolveType.get, cache, root), + condition: !silent, + ); } on SolveFailure catch (error) { for (var incompatibility in error.incompatibility.externalIncompatibilities) { if (incompatibility.cause != IncompatibilityCause.noVersions) continue; - if (incompatibility.terms.single.package.name != dep.name) continue; + if (incompatibility.terms.single.package.name != name) continue; dataError(error.toString()); } rethrow; } + // We want the entrypoint to be rooted at 'dep' not the dummy-package. + result.packages.removeWhere((id) => id.name == 'pub global activate'); final sameVersions = originalLockFile != null && originalLockFile.samePackageIds(result.lockFile); + final PackageId id = result.lockFile.packages[name]!; if (sameVersions) { log.message(''' -The package ${dep.name} is already activated at newest available version. -To recompile executables, first run `$topLevelProgram pub global deactivate ${dep.name}`. +The package $name is already activated at newest available version. +To recompile executables, first run `$topLevelProgram pub global deactivate $name`. '''); } else { - await result.showReport(SolveType.get, cache); + // Only precompile binaries if we have a new resolution. + if (!silent) await result.showReport(SolveType.get, cache); + + await result.downloadCachedPackages(cache); + + final lockFile = result.lockFile; + final tempDir = cache.createTempDir(); + _writeLockFile(tempDir, lockFile); + + // Load the package graph from [result] so we don't need to re-parse all + // the pubspecs. + final entrypoint = Entrypoint.global( + tempDir, + cache.loadCached(id), + lockFile, + cache, + solveResult: result, + ); + + await entrypoint.writePackagesFiles(); + + await entrypoint.precompileExecutables(); + + tryDeleteEntry(_packageDir(name)); + tryRenameDir(tempDir, _packageDir(name)); } - - // Make sure all of the dependencies are locally installed. - await Future.wait(result.packages.map((id) { - return http.withDependencyType(root.dependencyType(id.name), () async { - if (id.isRoot) return; - - var source = cache.source(id.source); - if (source is CachedSource) await source.downloadToSystemCache(id); - }); - })); - - var lockFile = result.lockFile; - _writeLockFile(dep.name, lockFile); - await _writePackageConfigFiles(dep.name, lockFile); - - // We want the entrypoint to be rooted at 'dep' not the dummy-package. - result.packages.removeWhere((id) => id.name == 'pub global activate'); - - var id = lockFile.packages[dep.name]!; - // Load the package graph from [result] so we don't need to re-parse all - // the pubspecs. final entrypoint = Entrypoint.global( - Package( - result.pubspecs[dep.name]!, - (cache.source(dep.source) as CachedSource).getDirectoryInCache(id), - ), - lockFile, + _packageDir(id.name), + cache.loadCached(id), + result.lockFile, cache, solveResult: result, ); - if (!sameVersions) { - // Only precompile binaries if we have a new resolution. - await entrypoint.precompileExecutables(); - } - _updateBinStubs( entrypoint, cache.load(entrypoint.lockFile.packages[dep.name]!), executables, overwriteBinStubs: overwriteBinStubs, ); - - log.message('Activated ${_formatPackage(id)}.'); - } - - Future<void> _writePackageConfigFiles( - String package, LockFile lockFile) async { - // TODO(sigurdm): Use [Entrypoint.writePackagesFiles] instead. - final packagesFilePath = _getPackagesFilePath(package); - final packageConfigFilePath = _getPackageConfigFilePath(package); - final dir = p.dirname(packagesFilePath); - writeTextFile( - packagesFilePath, lockFile.packagesFile(cache, relativeFrom: dir)); - ensureDir(p.dirname(packageConfigFilePath)); - writeTextFile(packageConfigFilePath, - await lockFile.packageConfigFile(cache, relativeFrom: dir)); + if (!silent) log.message('Activated ${_formatPackage(id)}.'); } /// Finishes activating package [package] by saving [lockFile] in the cache. - void _writeLockFile(String package, LockFile lockFile) { - ensureDir(p.join(_directory, package)); - - // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it - // when Dart 1.6 is old enough that we don't think anyone will have these - // lockfiles anymore (issue 20703). - var oldPath = p.join(_directory, '$package.lock'); - if (fileExists(oldPath)) deleteEntry(oldPath); - - writeTextFile(_getLockFilePath(package), - lockFile.serialize(p.join(_directory, package))); + void _writeLockFile(String dir, LockFile lockFile) { + writeTextFile(p.join(dir, 'pubspec.lock'), lockFile.serialize(null)); } /// Shows the user the currently active package with [name], if any. - void _describeActive(LockFile lockFile, String? name) { + LockFile? _describeActive(String name, SystemCache cache) { + late final LockFile lockFile; + try { + lockFile = LockFile.load(_getLockFilePath(name), cache.sources); + } on IOException { + // Couldn't read the lock file. It probably doesn't exist. + return null; + } var id = lockFile.packages[name]!; var source = id.source; @@ -312,6 +287,7 @@ log.message('Package ${log.bold(name)} is currently active at version ' '${log.bold(id.version)}.'); } + return lockFile; } /// Deactivates a previously-activated package named [name]. @@ -341,22 +317,8 @@ try { lockFile = LockFile.load(lockFilePath, cache.sources); } on IOException { - var oldLockFilePath = p.join(_directory, '$name.lock'); - try { - // TODO(nweiz): This looks for Dart 1.6's old lockfile location. - // Remove it when Dart 1.6 is old enough that we don't think anyone - // will have these lockfiles anymore (issue 20703). - lockFile = LockFile.load(oldLockFilePath, cache.sources); - } on IOException { - // If we couldn't read the lock file, it's not activated. - dataError('No active package ${log.bold(name)}.'); - } - - // Move the old lockfile to its new location. - ensureDir(p.dirname(lockFilePath)); - File(oldLockFilePath).renameSync(lockFilePath); - // Just make sure these files are created as well. - await _writePackageConfigFiles(name, lockFile); + // If we couldn't read the lock file, it's not activated. + dataError('No active package ${log.bold(name)}.'); } // Remove the package itself from the lockfile. We put it in there so we @@ -370,7 +332,8 @@ if (source is CachedSource) { // For cached sources, the package itself is in the cache and the // lockfile is the one we just loaded. - entrypoint = Entrypoint.global(cache.loadCached(id), lockFile, cache); + entrypoint = Entrypoint.global( + _packageDir(id.name), cache.loadCached(id), lockFile, cache); } else { // For uncached sources (i.e. path), the ID just points to the real // directory for the package. @@ -446,16 +409,6 @@ String _getLockFilePath(String name) => p.join(_directory, name, 'pubspec.lock'); - /// Gets the path to the .packages file for an activated cached package with - /// [name]. - String _getPackagesFilePath(String name) => - p.join(_directory, name, '.packages'); - - /// Gets the path to the `package_config.json` file for an - /// activated cached package with [name]. - String _getPackageConfigFilePath(String name) => - p.join(_directory, name, '.dart_tool', 'package_config.json'); - /// Shows the user a formatted list of globally activated packages. void listActivePackages() { if (!dirExists(_directory)) return; @@ -542,17 +495,24 @@ log.message('Reactivating ${log.bold(id.name)} ${id.version}...'); var entrypoint = await find(id.name); + final packageExecutables = executables.remove(id.name) ?? []; - await _writePackageConfigFiles(id.name, entrypoint.lockFile); - await entrypoint.precompileExecutables(); - var packageExecutables = executables.remove(id.name) ?? []; - _updateBinStubs( - entrypoint, - cache.load(id), - packageExecutables, - overwriteBinStubs: true, - suggestIfNotOnPath: false, - ); + if (entrypoint.isCached) { + deleteEntry(entrypoint.globalDir!); + await _installInCache( + id.toRange(), + packageExecutables, + overwriteBinStubs: true, + silent: true, + ); + } else { + await activatePath( + entrypoint.root.dir, + packageExecutables, + overwriteBinStubs: true, + analytics: null, + ); + } successes.add(id.name); } catch (error, stackTrace) { var message = 'Failed to reactivate ' @@ -706,10 +666,7 @@ // Show errors for any missing scripts. // TODO(rnystrom): This can print false positives since a script may be // produced by a transformer. Do something better. - var binFiles = package - .listFiles(beneath: 'bin', recursive: false) - .map(package.relative) - .toList(); + var binFiles = package.executablePaths; for (var executable in installed) { var script = package.pubspec.executables[executable]; var scriptPath = p.join('bin', '$script.dart'); @@ -761,6 +718,7 @@ // If the script was built to a snapshot, just try to invoke that // directly and skip pub global run entirely. String invocation; + late String binstub; if (Platform.isWindows) { if (fileExists(snapshot)) { // We expect absolute paths from the precompiler since relative ones @@ -786,7 +744,7 @@ } else { invocation = 'dart pub global run ${package.name}:$script %*'; } - var batch = ''' + binstub = ''' @echo off rem This file was created by pub v${sdk.version}. rem Package: ${package.name} @@ -795,7 +753,6 @@ rem Script: $script $invocation '''; - writeTextFile(binStubPath, batch); } else { if (fileExists(snapshot)) { // We expect absolute paths from the precompiler since relative ones @@ -818,7 +775,7 @@ } else { invocation = 'dart pub global run ${package.name}:$script "\$@"'; } - var bash = ''' + binstub = ''' #!/usr/bin/env sh # This file was created by pub v${sdk.version}. # Package: ${package.name} @@ -827,25 +784,31 @@ # Script: $script $invocation '''; + } - // Write this as the system encoding since the system is going to execute - // it and it might contain non-ASCII characters in the pathnames. - writeTextFile(binStubPath, bash, encoding: const SystemEncoding()); + // Write the binstub to a temporary location, make it executable and move + // it into place afterwards to avoid races. + final tempDir = cache.createTempDir(); + try { + final tmpPath = p.join(tempDir, binStubPath); - // Make it executable. - var result = Process.runSync('chmod', ['+x', binStubPath]); - if (result.exitCode != 0) { - // Couldn't make it executable so don't leave it laying around. - try { - deleteEntry(binStubPath); - } on IOException catch (err) { - // Do nothing. We're going to fail below anyway. - log.fine('Could not delete binstub:\n$err'); + // Write this as the system encoding since the system is going to + // execute it and it might contain non-ASCII characters in the + // pathnames. + writeTextFile(tmpPath, binstub, encoding: const SystemEncoding()); + + if (Platform.isLinux || Platform.isMacOS) { + // Make it executable. + var result = Process.runSync('chmod', ['+x', tmpPath]); + if (result.exitCode != 0) { + // Couldn't make it executable so don't leave it laying around. + fail('Could not make "$tmpPath" executable (exit code ' + '${result.exitCode}):\n${result.stderr}'); } - - fail('Could not make "$binStubPath" executable (exit code ' - '${result.exitCode}):\n${result.stderr}'); } + File(tmpPath).renameSync(binStubPath); + } finally { + deleteEntry(tempDir); } return previousPackage;
diff --git a/lib/src/ignore.dart b/lib/src/ignore.dart index fbd93e8..ff22dff 100644 --- a/lib/src/ignore.dart +++ b/lib/src/ignore.dart
@@ -464,6 +464,9 @@ } else { expr += '.*'; } + } else if (peekChar() == '/' || peekChar() == null) { + // /a/* should not match '/a/' + expr += '[^/]+'; } else { // Handle a single '*' expr += '[^/]*'; @@ -520,7 +523,6 @@ expr = '$expr/\$'; } else { expr = '$expr/?\$'; - // expr = '$expr\$'; } try { return _IgnoreParseResult(
diff --git a/lib/src/io.dart b/lib/src/io.dart index 231aba5..280dcbd 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart
@@ -361,52 +361,54 @@ /// when we try to delete or move something while it's being scanned. To /// mitigate that, on Windows, this will retry the operation a few times if it /// fails. -void _attempt(String description, void Function() operation) { +/// +/// For some operations it makes sense to handle ERROR_DIR_NOT_EMPTY +/// differently. They can pass [ignoreEmptyDir] = `true`. +void _attempt(String description, void Function() operation, + {bool ignoreEmptyDir = false}) { if (!Platform.isWindows) { operation(); return; } String? getErrorReason(FileSystemException error) { + // ERROR_ACCESS_DENIED if (error.osError?.errorCode == 5) { return 'access was denied'; } + // ERROR_SHARING_VIOLATION if (error.osError?.errorCode == 32) { return 'it was in use by another process'; } - if (error.osError?.errorCode == 145) { + // ERROR_DIR_NOT_EMPTY + if (!ignoreEmptyDir && _isDirectoryNotEmptyException(error)) { return 'of dart-lang/sdk#25353'; } return null; } - for (var i = 0; i < 2; i++) { + for (var i = 0; i < 3; i++) { try { operation(); - return; + break; } on FileSystemException catch (error) { var reason = getErrorReason(error); if (reason == null) rethrow; - log.io('Pub failed to $description because $reason. ' - 'Retrying in 50ms.'); - sleep(Duration(milliseconds: 50)); + if (i < 2) { + log.io('Pub failed to $description because $reason. ' + 'Retrying in 50ms.'); + sleep(Duration(milliseconds: 50)); + } else { + fail('Pub failed to $description because $reason.\n' + 'This may be caused by a virus scanner or having a file\n' + 'in the directory open in another application.'); + } } } - - try { - operation(); - } on FileSystemException catch (error) { - var reason = getErrorReason(error); - if (reason == null) rethrow; - - fail('Pub failed to $description because $reason.\n' - 'This may be caused by a virus scanner or having a file\n' - 'in the directory open in another application.'); - } } /// Deletes whatever's at [path], whether it's a file, directory, or symlink. @@ -451,14 +453,53 @@ void renameDir(String from, String to) { _attempt('rename directory', () { log.io('Renaming directory $from to $to.'); - try { - Directory(from).renameSync(to); - } on IOException { - // Ensure that [to] isn't left in an inconsistent state. See issue 12436. - if (entryExists(to)) deleteEntry(to); + Directory(from).renameSync(to); + }, ignoreEmptyDir: true); +} + +/// Renames directory [from] to [to]. +/// If it fails with "destination not empty" we log and continue, assuming +/// another process got there before us. +void tryRenameDir(String from, String to) { + ensureDir(path.dirname(to)); + try { + renameDir(from, to); + } on FileSystemException catch (e) { + tryDeleteEntry(from); + if (!_isDirectoryNotEmptyException(e)) { rethrow; } - }); + log.fine(''' +Destination directory $to already existed. +Assuming a concurrent pub invocation installed it.'''); + } +} + +void copyFile(String from, String to) { + log.io('Copying "$from" to "$to".'); + File(from).copySync(to); +} + +void renameFile(String from, String to) { + log.io('Renaming "$from" to "$to".'); + File(from).renameSync(to); +} + +bool _isDirectoryNotEmptyException(FileSystemException e) { + final errorCode = e.osError?.errorCode; + return + // On Linux rename will fail with ENOTEMPTY if directory exists: + // https://man7.org/linux/man-pages/man2/rename.2.html + // #define ENOTEMPTY 39 /* Directory not empty */ + // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno.h#n20 + (Platform.isLinux && errorCode == 39) || + // On Windows this may fail with ERROR_DIR_NOT_EMPTY + // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- + (Platform.isWindows && errorCode == 145) || + // On MacOS rename will fail with ENOTEMPTY if directory exists. + // #define ENOTEMPTY 66 /* Directory not empty */ + // https://github.com/apple-oss-distributions/xnu/blob/bb611c8fecc755a0d8e56e2fa51513527c5b7a0e/bsd/sys/errno.h#L190 + (Platform.isMacOS && errorCode == 66); } /// Creates a new symlink at path [symlink] that points to [target]. @@ -966,10 +1007,10 @@ log.fine(buffer.toString()); ArgumentError.checkNotNull(baseDir, 'baseDir'); - baseDir = path.absolute(baseDir); + baseDir = path.normalize(path.absolute(baseDir)); final tarContents = Stream.fromIterable(contents.map((entry) { - entry = path.absolute(entry); + entry = path.normalize(path.absolute(entry)); if (!path.isWithin(baseDir, entry)) { throw ArgumentError('Entry $entry is not inside $baseDir.'); }
diff --git a/lib/src/language_version.dart b/lib/src/language_version.dart index e294014..ad331b2 100644 --- a/lib/src/language_version.dart +++ b/lib/src/language_version.dart
@@ -69,6 +69,21 @@ bool get supportsNullSafety => this >= firstVersionWithNullSafety; + /// Minimum language version at which short hosted syntax is supported. + /// + /// This allows `hosted` dependencies to be expressed as: + /// ```yaml + /// dependencies: + /// foo: + /// hosted: https://some-pub.com/path + /// version: ^1.0.0 + /// ``` + /// + /// At older versions, `hosted` dependencies had to be a map with a `url` and + /// a `name` key. + bool get supportsShorterHostedSyntax => + this >= firstVersionWithShorterHostedSyntax; + @override int compareTo(LanguageVersion other) { if (major != other.major) return major.compareTo(other.major); @@ -89,6 +104,7 @@ static const defaultLanguageVersion = LanguageVersion(2, 7); static const firstVersionWithNullSafety = LanguageVersion(2, 12); + static const firstVersionWithShorterHostedSyntax = LanguageVersion(2, 15); /// Transform language version to string that can be parsed with /// [LanguageVersion.parse].
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart index e2723c7..8f563ab 100644 --- a/lib/src/lock_file.dart +++ b/lib/src/lock_file.dart
@@ -218,7 +218,7 @@ String packagesFile( SystemCache cache, { String? entrypoint, - required String relativeFrom, + String? relativeFrom, }) { var header = ''' This file is deprecated. Tools should instead consume @@ -256,7 +256,7 @@ SystemCache cache, { String? entrypoint, VersionConstraint? entrypointSdkConstraint, - required String relativeFrom, + String? relativeFrom, }) async { final entries = <PackageConfigEntry>[]; for (final name in ordered(packages.keys)) { @@ -306,8 +306,9 @@ /// Returns the serialized YAML text of the lock file. /// /// [packageDir] is the containing directory of the root package, used to - /// properly serialize package descriptions. - String serialize(String packageDir) { + /// serialize relative path package descriptions. If it is null, they will be + /// serialized as absolute. + String serialize(String? packageDir) { // Convert the dependencies to a simple object. var packageMap = {}; packages.forEach((name, package) {
diff --git a/lib/src/log.dart b/lib/src/log.dart index 930e0fb..db24d7a 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart
@@ -12,9 +12,11 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; +import 'entrypoint.dart'; import 'exceptions.dart'; import 'io.dart'; import 'progress.dart'; +import 'sdk.dart'; import 'transcript.dart'; import 'utils.dart'; @@ -36,7 +38,7 @@ /// The list of recorded log messages. Will only be recorded if /// [recordTranscript()] is called. -Transcript<_Entry>? _transcript; +final Transcript<_Entry> _transcript = Transcript(_maxTranscript); /// The currently-animated progress indicator, if any. /// @@ -168,6 +170,16 @@ Level.fine: _logToStderrWithLabel }); + /// Shows all logs. + static const testing = Verbosity._('testing', { + Level.error: _logToStderrWithLabel, + Level.warning: _logToStderrWithLabel, + Level.message: _logToStdoutWithLabel, + Level.io: _logToStderrWithLabel, + Level.solver: _logToStderrWithLabel, + Level.fine: _logToStderrWithLabel + }); + const Verbosity._(this.name, this._loggers); final String name; @@ -233,7 +245,7 @@ var logFn = verbosity._loggers[level]; if (logFn != null) logFn(entry); - if (_transcript != null) _transcript!.add(entry); + _transcript.add(entry); } /// Logs the spawning of an [executable] process with [arguments] at [io] @@ -313,18 +325,10 @@ } } -/// Enables recording of log entries. -void recordTranscript() { - _transcript = Transcript<_Entry>(_maxTranscript); -} - -/// If [recordTranscript()] was called, then prints the previously recorded log -/// transcript to stderr. -void dumpTranscript() { - if (_transcript == null) return; - +/// Prints the recorded log transcript to stderr. +void dumpTranscriptToStdErr() { stderr.writeln('---- Log transcript ----'); - _transcript!.forEach((entry) { + _transcript.forEach((entry) { _printToStream(stderr, entry, showLabel: true); }, (discarded) { stderr.writeln('---- ($discarded discarded) ----'); @@ -332,6 +336,68 @@ stderr.writeln('---- End log transcript ----'); } +String _limit(String input, int limit) { + const snip = '[...]'; + if (input.length < limit - snip.length) return input; + return '${input.substring(0, limit ~/ 2 - snip.length)}' + '$snip' + '${input.substring(limit)}'; +} + +/// Prints relevant system information and the log transcript to [path]. +void dumpTranscriptToFile(String path, String command, Entrypoint? entrypoint) { + final buffer = StringBuffer(); + buffer.writeln(''' +Information about the latest pub run. + +If you believe something is not working right, you can go to +https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file. + +Before making this file public, make sure to remove any sensitive information! + +Pub version: ${sdk.version} +Created: ${DateTime.now().toIso8601String()} +FLUTTER_ROOT: ${Platform.environment['FLUTTER_ROOT'] ?? '<not set>'} +PUB_HOSTED_URL: ${Platform.environment['PUB_HOSTED_URL'] ?? '<not set>'} +PUB_CACHE: "${Platform.environment['PUB_CACHE'] ?? '<not set>'}" +Command: $command +Platform: ${Platform.operatingSystem} +'''); + + if (entrypoint != null) { + buffer.writeln('---- ${p.absolute(entrypoint.pubspecPath)} ----'); + if (fileExists(entrypoint.pubspecPath)) { + buffer.writeln(_limit(readTextFile(entrypoint.pubspecPath), 5000)); + } else { + buffer.writeln('<No pubspec.yaml>'); + } + buffer.writeln('---- End pubspec.yaml ----'); + buffer.writeln('---- ${p.absolute(entrypoint.lockFilePath)} ----'); + if (fileExists(entrypoint.lockFilePath)) { + buffer.writeln(_limit(readTextFile(entrypoint.lockFilePath), 5000)); + } else { + buffer.writeln('<No pubspec.lock>'); + } + buffer.writeln('---- End pubspec.lock ----'); + } + + buffer.writeln('---- Log transcript ----'); + + _transcript.forEach((entry) { + _printToStream(buffer, entry, showLabel: true); + }, (discarded) { + buffer.writeln('---- ($discarded entries discarded) ----'); + }); + buffer.writeln('---- End log transcript ----'); + ensureDir(p.dirname(path)); + try { + writeTextFile(path, buffer.toString(), dontLogContents: true); + } on IOException catch (e) { + stderr.writeln('Failed writing log to `$path` ($e), writing it to stderr:'); + dumpTranscriptToStdErr(); + } +} + /// Filter out normal pub output when not attached to a terminal /// /// Unless the user has overriden the verbosity, @@ -492,7 +558,7 @@ _printToStream(sink, entry, showLabel: showLabel); } -void _printToStream(IOSink sink, _Entry entry, {required bool showLabel}) { +void _printToStream(StringSink sink, _Entry entry, {required bool showLabel}) { _stopProgress(); var firstLine = true;
diff --git a/lib/src/package.dart b/lib/src/package.dart index 0bc6f89..d100157 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart
@@ -83,10 +83,12 @@ ..addAll(dependencyOverrides); } - /// Returns a list of asset ids for all Dart executables in this package's bin + /// Returns a list of paths to all Dart executables in this package's bin /// directory. List<String> get executablePaths { - return ordered(listFiles(beneath: 'bin', recursive: false)) + final binDir = p.join(dir, 'bin'); + if (!dirExists(binDir)) return <String>[]; + return ordered(listDir(p.join(dir, 'bin'), includeDirs: false)) .where((executable) => p.extension(executable) == '.dart') .map((executable) => p.relative(executable, from: dir)) .toList();
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart index a46e3f5..b9e59c6 100644 --- a/lib/src/pub_embeddable_command.dart +++ b/lib/src/pub_embeddable_command.dart
@@ -58,11 +58,13 @@ @override final PubAnalytics? analytics; - PubEmbeddableCommand(this.analytics) : super() { + final bool Function() isVerbose; + + PubEmbeddableCommand(this.analytics, this.isVerbose) : super() { argParser.addFlag('trace', help: 'Print debugging information when an error occurs.'); argParser.addFlag('verbose', - abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".'); + abbr: 'v', negatable: false, help: 'Print detailed logging.'); argParser.addOption( 'directory', abbr: 'C', @@ -101,12 +103,15 @@ } @override - bool get captureStackChains => argResults['verbose']; + bool get captureStackChains => _isVerbose; @override - Verbosity get verbosity => - argResults['verbose'] ? Verbosity.all : Verbosity.normal; + Verbosity get verbosity => _isVerbose ? Verbosity.all : Verbosity.normal; @override - bool get trace => argResults['verbose']; + bool get trace => _isVerbose; + + bool get _isVerbose { + return argResults['verbose'] || isVerbose(); + } }
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index b8ff0e5..bc01162 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:pub_semver/pub_semver.dart'; +import '../http.dart'; import '../io.dart'; import '../lock_file.dart'; import '../log.dart' as log; @@ -12,6 +13,7 @@ import '../package_name.dart'; import '../pub_embeddable_command.dart'; import '../pubspec.dart'; +import '../source/cached.dart'; import '../source/hosted.dart'; import '../source_registry.dart'; import '../system_cache.dart'; @@ -78,6 +80,18 @@ final LockFile _previousLockFile; + /// Downloads all cached packages in [packages]. + Future<void> downloadCachedPackages(SystemCache cache) async { + await Future.wait(packages.map((id) async { + if (id.source == null) return; + final source = cache.source(id.source); + if (source is! CachedSource) return; + return await withDependencyType(_root.dependencyType(id.name), () async { + await source.downloadToSystemCache(id); + }); + })); + } + /// Returns the names of all packages that were changed. /// /// This includes packages that were added or removed.
diff --git a/lib/src/source.dart b/lib/src/source.dart index add55fa..ecbc5eb 100644 --- a/lib/src/source.dart +++ b/lib/src/source.dart
@@ -116,7 +116,7 @@ /// [description] in the right format. /// /// [containingPath] is the containing directory of the root package. - dynamic serializeDescription(String containingPath, description) { + dynamic serializeDescription(String? containingPath, description) { return description; }
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart index 0d98ae9..3be752c 100644 --- a/lib/src/source/git.dart +++ b/lib/src/source/git.dart
@@ -108,10 +108,10 @@ /// For the descriptions where `relative` attribute is `true`, tries to make /// `url` relative to the specified [containingPath]. @override - dynamic serializeDescription(String containingPath, description) { + dynamic serializeDescription(String? containingPath, description) { final copy = Map.from(description); copy.remove('relative'); - if (description['relative'] == true) { + if (description['relative'] == true && containingPath != null) { copy['url'] = p.url.relative(description['url'], from: Uri.file(containingPath).toString()); }
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart index 1cb1950..2cfc52a 100644 --- a/lib/src/source/hosted.dart +++ b/lib/src/source/hosted.dart
@@ -159,7 +159,7 @@ } @override - dynamic serializeDescription(String containingPath, description) { + dynamic serializeDescription(String? containingPath, description) { final desc = _asDescription(description); return _serializedDescriptionFor(desc.packageName, desc.uri); } @@ -232,8 +232,7 @@ return _HostedDescription(packageName, defaultUrl); } - final canUseShorthandSyntax = - languageVersion >= _minVersionForShorterHostedSyntax; + final canUseShorthandSyntax = languageVersion.supportsShorterHostedSyntax; if (description is String) { // Old versions of pub (pre Dart 2.15) interpret `hosted: foo` as @@ -256,7 +255,7 @@ } else { throw FormatException( 'Using `hosted: <url>` is only supported with a minimum SDK ' - 'constraint of $_minVersionForShorterHostedSyntax.', + 'constraint of ${LanguageVersion.firstVersionWithShorterHostedSyntax}.', ); } } @@ -271,7 +270,7 @@ if (name is! String) { throw FormatException("The 'name' key must have a string value without " - 'a minimum Dart SDK constraint of $_minVersionForShorterHostedSyntax.0 or higher.'); + 'a minimum Dart SDK constraint of ${LanguageVersion.firstVersionWithShorterHostedSyntax}.0 or higher.'); } var url = defaultUrl; @@ -286,21 +285,6 @@ return _HostedDescription(name, url); } - /// Minimum language version at which short hosted syntax is supported. - /// - /// This allows `hosted` dependencies to be expressed as: - /// ```yaml - /// dependencies: - /// foo: - /// hosted: https://some-pub.com/path - /// version: ^1.0.0 - /// ``` - /// - /// At older versions, `hosted` dependencies had to be a map with a `url` and - /// a `name` key. - static const LanguageVersion _minVersionForShorterHostedSyntax = - LanguageVersion(2, 15); - static final RegExp _looksLikePackageName = RegExp(r'^[a-zA-Z_]+[a-zA-Z0-9_]*$'); } @@ -704,6 +688,7 @@ packages.map((package) async { var id = source.idFor(package.name, package.version, url: url); try { + deleteEntry(package.dir); await _download(id, package.dir); return RepairResult(id, success: true); } catch (error, stackTrace) { @@ -812,14 +797,13 @@ var tempDir = systemCache.createTempDir(); await extractTarGz(readBinaryFileAsSream(archivePath), tempDir); - // Remove the existing directory if it exists. This will happen if - // we're forcing a download to repair the cache. - if (dirExists(destPath)) deleteEntry(destPath); - // Now that the get has succeeded, move it to the real location in the - // cache. This ensures that we don't leave half-busted ghost - // directories in the user's pub cache if a get fails. - renameDir(tempDir, destPath); + // cache. + // + // If this fails with a "directory not empty" exception we assume that + // another pub process has installed the same package version while we + // downloaded. + tryRenameDir(tempDir, destPath); }); }
diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart index 0401ebd..4679f98 100644 --- a/lib/src/source/path.dart +++ b/lib/src/source/path.dart
@@ -130,9 +130,11 @@ /// /// For the descriptions where `relative` attribute is `true`, tries to make /// `path` relative to the specified [containingPath]. + /// + /// If [containingPath] is `null` they are serialized as absolute. @override - dynamic serializeDescription(String containingPath, description) { - if (description['relative']) { + dynamic serializeDescription(String? containingPath, description) { + if (description['relative'] == true && containingPath != null) { return { 'path': relativePathWithPosixSeparators( p.relative(description['path'], from: containingPath)),
diff --git a/lib/src/validator/changelog.dart b/lib/src/validator/changelog.dart index 694ace2..614eba2 100644 --- a/lib/src/validator/changelog.dart +++ b/lib/src/validator/changelog.dart
@@ -21,7 +21,7 @@ final changelog = entrypoint.root.changelogPath; if (changelog == null) { - warnings.add('Please add a`CHANGELOG.md` to your package. ' + warnings.add('Please add a `CHANGELOG.md` to your package. ' 'See https://dart.dev/tools/pub/publishing#important-files.'); return; }
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart index 78bf9aa..4fb49b1 100644 --- a/lib/src/validator/gitignore.dart +++ b/lib/src/validator/gitignore.dart
@@ -71,7 +71,7 @@ if (ignoredFilesCheckedIn.isNotEmpty) { warnings.add(''' -${ignoredFilesCheckedIn.length} checked in ${pluralize('file', ignoredFilesCheckedIn.length)} ${ignoredFilesCheckedIn.length == 1 ? 'is' : 'are'} ignored by a `.gitignore`. +${ignoredFilesCheckedIn.length} checked-in ${pluralize('file', ignoredFilesCheckedIn.length)} ${ignoredFilesCheckedIn.length == 1 ? 'is' : 'are'} ignored by a `.gitignore`. Previous versions of Pub would include those in the published package. Consider adjusting your `.gitignore` files to not ignore those files, and if you do not wish to
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart index 69f33fe..1a1e9df 100644 --- a/test/add/common/add_test.dart +++ b/test/add/common/add_test.dart
@@ -37,29 +37,6 @@ }); group('normally', () { - test('fails if extra arguments are passed', () async { - final server = await servePackages(); - server.serve('foo', '1.2.2'); - - await d.dir(appPath, [ - d.pubspec({'name': 'myapp'}) - ]).create(); - - await pubAdd( - args: ['foo', '^1.2.2'], - exitCode: exit_codes.USAGE, - error: contains('Takes only a single argument.')); - - await d.dir(appPath, [ - d.pubspec({ - 'name': 'myapp', - }), - d.nothing('.dart_tool/package_config.json'), - d.nothing('pubspec.lock'), - d.nothing('.packages'), - ]).validate(); - }); - test('adds a package from a pub server', () async { final server = await servePackages(); server.serve('foo', '1.2.3'); @@ -73,6 +50,24 @@ await d.appDir({'foo': '1.2.3'}).validate(); }); + test('adds multiple package from a pub server', () async { + final server = await servePackages(); + server.serve('foo', '1.2.3'); + server.serve('bar', '1.1.0'); + server.serve('baz', '2.5.3'); + + await d.appDir({}).create(); + + await pubAdd(args: ['foo:1.2.3', 'bar:1.1.0', 'baz:2.5.3']); + + await d.cacheDir( + {'foo': '1.2.3', 'bar': '1.1.0', 'baz': '2.5.3'}).validate(); + await d.appPackagesFile( + {'foo': '1.2.3', 'bar': '1.1.0', 'baz': '2.5.3'}).validate(); + await d + .appDir({'foo': '1.2.3', 'bar': '1.1.0', 'baz': '2.5.3'}).validate(); + }); + test( 'does not remove empty dev_dependencies while adding to normal dependencies', () async {
diff --git a/test/add/git/git_test.dart b/test/add/git/git_test.dart index 7bc51f0..00bd334 100644 --- a/test/add/git/git_test.dart +++ b/test/add/git/git_test.dart
@@ -184,4 +184,18 @@ }) ]).validate(); }); + + test('fails if multiple packages passed for git source', () async { + ensureGit(); + + await d.git( + 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create(); + + await d.appDir({}).create(); + + await pubAdd( + args: ['foo', 'bar', 'baz', '--git-url', '../foo.git'], + exitCode: exit_codes.USAGE, + error: contains('Can only add a single git package at a time.')); + }); }
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart index eea6c09..0a83ac5 100644 --- a/test/add/hosted/non_default_pub_server_test.dart +++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -35,6 +35,46 @@ }).validate(); }); + test('adds multiple packages from a non-default pub server', () async { + // Make the default server serve errors. Only the custom server should + // be accessed. + (await servePackages()).serveErrors(); + + final server = await servePackages(); + server.serve('foo', '1.1.0'); + server.serve('foo', '1.2.3'); + server.serve('bar', '0.2.5'); + server.serve('bar', '3.2.3'); + server.serve('baz', '0.1.3'); + server.serve('baz', '1.3.5'); + + await d.appDir({}).create(); + + final url = server.url; + + await pubAdd( + args: ['foo:1.2.3', 'bar:3.2.3', 'baz:1.3.5', '--hosted-url', url]); + + await d.cacheDir({'foo': '1.2.3', 'bar': '3.2.3', 'baz': '1.3.5'}, + port: server.port).validate(); + await d.appPackagesFile( + {'foo': '1.2.3', 'bar': '3.2.3', 'baz': '1.3.5'}).validate(); + await d.appDir({ + 'foo': { + 'version': '1.2.3', + 'hosted': {'name': 'foo', 'url': url} + }, + 'bar': { + 'version': '3.2.3', + 'hosted': {'name': 'bar', 'url': url} + }, + 'baz': { + 'version': '1.3.5', + 'hosted': {'name': 'baz', 'url': url} + } + }).validate(); + }); + test('fails when adding from an invalid url', () async { ensureGit();
diff --git a/test/add/path/absolute_path_test.dart b/test/add/path/absolute_path_test.dart index 5b7e726..b15ec2d 100644 --- a/test/add/path/absolute_path_test.dart +++ b/test/add/path/absolute_path_test.dart
@@ -41,6 +41,28 @@ }).validate(); }); + test('fails when adding multiple packages through local path', () async { + ensureGit(); + + await d.git( + 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create(); + + await d.appDir({}).create(); + final absolutePath = path.join(d.sandbox, 'foo'); + + await pubAdd( + args: ['foo:2.0.0', 'bar:0.1.3', 'baz:1.3.1', '--path', absolutePath], + error: contains('Can only add a single local package at a time.'), + exitCode: exit_codes.USAGE); + + await d.appDir({}).validate(); + await d.dir(appPath, [ + d.nothing('.dart_tool/package_config.json'), + d.nothing('pubspec.lock'), + d.nothing('.packages'), + ]).validate(); + }); + test('fails when adding with an invalid version constraint', () async { ensureGit();
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart index ccb16f6..a9795ea 100644 --- a/test/embedding/embedding_test.dart +++ b/test/embedding/embedding_test.dart
@@ -6,8 +6,10 @@ import 'dart:io'; import 'package:path/path.dart' as path; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; + import '../descriptor.dart' as d; import '../golden_file.dart'; import '../test_pub.dart'; @@ -16,6 +18,8 @@ late String snapshot; +final logFile = p.join(d.sandbox, cachePath, 'log', 'pub_log.txt'); + /// Runs `dart tool/test-bin/pub_command_runner.dart [args]` and appends the output to [buffer]. Future<void> runEmbeddingToBuffer( List<String> args, @@ -37,12 +41,9 @@ buffer.writeln([ '\$ $_commandRunner ${args.join(' ')}', - ...await process.stdout.rest.toList(), + ...await process.stdout.rest.map(_filter).toList(), + ...await process.stderr.rest.map((e) => '[E] ${_filter(e)}').toList(), ].join('\n')); - final stdErr = await process.stderr.rest.toList(); - if (stdErr.isNotEmpty) { - buffer.writeln(stdErr.map((e) => '[E] $e').join('\n')); - } buffer.write('\n'); } @@ -51,7 +52,7 @@ /// next section in golden file. Future<void> runEmbedding( List<String> args, { - String? workingDirextory, + String? workingDirectory, Map<String, String>? environment, dynamic exitCode = 0, }) async { @@ -59,7 +60,7 @@ await runEmbeddingToBuffer( args, buffer, - workingDirectory: workingDirextory, + workingDirectory: workingDirectory, environment: environment, exitCode: exitCode, ); @@ -82,6 +83,7 @@ }); testWithGolden('run works, though hidden', (ctx) async { + await servePackages(); await d.dir(appPath, [ d.pubspec({ 'name': 'myapp', @@ -101,12 +103,53 @@ ]).create(); await ctx.runEmbedding( ['pub', 'get'], - workingDirextory: d.path(appPath), + workingDirectory: d.path(appPath), ); await ctx.runEmbedding( ['pub', 'run', 'bin/main.dart'], exitCode: 123, - workingDirextory: d.path(appPath), + workingDirectory: d.path(appPath), + ); + }); + + testWithGolden( + 'logfile is written with --verbose and on unexpected exceptions', + (context) async { + final server = await servePackages(); + server.serve('foo', '1.0.0'); + await d.appDir({'foo': 'any'}).create(); + + // TODO(sigurdm) This logs the entire verbose trace to a golden file. + // + // This is fragile, and can break for all sorts of small reasons. We think + // this might be worth while having to have at least minimal testing of the + // verbose stack trace. + // + // But if you, future contributor, think this test is annoying: feel free to + // remove it, or rewrite it to filter out the stack-trace itself, only + // testing for creation of the file. + // + // It is a fragile test, and we acknowledge that it's usefulness can be + // debated... + await context.runEmbedding( + ['pub', '--verbose', 'get'], + workingDirectory: d.path(appPath), + ); + context.expectNextSection( + _filter( + File(logFile).readAsStringSync(), + ), + ); + await d.dir('empty').create(); + await context.runEmbedding( + ['pub', 'fail'], + workingDirectory: d.path('empty'), + exitCode: 1, + ); + context.expectNextSection( + _filter( + File(logFile).readAsStringSync(), + ), ); }); @@ -175,5 +218,113 @@ } }, }); + // Don't write the logs to file on a normal run. + expect(File(logFile).existsSync(), isFalse); }); + + test('`embedding --verbose pub` is verbose', () async { + await servePackages(); + final buffer = StringBuffer(); + await runEmbeddingToBuffer(['--verbose', 'pub', 'logout'], buffer); + expect(buffer.toString(), contains('FINE: Pub 0.1.2+3')); + }); +} + +String _filter(String input) { + return input + .replaceAll(p.toUri(d.sandbox).toString(), r'file://$SANDBOX') + .replaceAll(d.sandbox, r'$SANDBOX') + .replaceAll(Platform.pathSeparator, '/') + .replaceAll(Platform.operatingSystem, r'$OS') + .replaceAll(globalServer.port.toString(), r'$PORT') + .replaceAll( + RegExp(r'^Created:(.*)$', multiLine: true), + r'Created: $TIME', + ) + .replaceAll( + RegExp(r'Generated by pub on (.*)$', multiLine: true), + r'Generated by pub on $TIME', + ) + .replaceAll( + RegExp(r'X-Pub-Session-ID(.*)$', multiLine: true), + r'X-Pub-Session-ID: $ID', + ) + .replaceAll( + RegExp(r'took (.*)$', multiLine: true), + r'took: $TIME', + ) + .replaceAll( + RegExp(r'date: (.*)$', multiLine: true), + r'date: $TIME', + ) + .replaceAll( + RegExp(r'Creating (.*) from stream\.$', multiLine: true), + r'Creating $FILE from stream', + ) + .replaceAll( + RegExp(r'Created (.*) from stream\.$', multiLine: true), + r'Created $FILE from stream', + ) + .replaceAll( + RegExp(r'Renaming directory $SANDBOX/cache/_temp/(.*?) to', + multiLine: true), + r'Renaming directory $SANDBOX/cache/_temp/', + ) + .replaceAll( + RegExp(r'Extracting .tar.gz stream to (.*?)$', multiLine: true), + r'Extracting .tar.gz stream to $DIR', + ) + .replaceAll( + RegExp(r'Extracted .tar.gz to (.*?)$', multiLine: true), + r'Extracted .tar.gz to $DIR', + ) + .replaceAll( + RegExp(r'Reading binary file (.*?)$', multiLine: true), + r'Reading binary file $FILE.', + ) + .replaceAll( + RegExp(r'Deleting directory (.*)$', multiLine: true), + r'Deleting directory $DIR', + ) + .replaceAll( + RegExp(r'Deleting directory (.*)$', multiLine: true), + r'Deleting directory $DIR', + ) + .replaceAll( + RegExp(r'Resolving dependencies finished (.*)$', multiLine: true), + r'Resolving dependencies finished ($TIME)', + ) + .replaceAll( + RegExp(r'Created temp directory (.*)$', multiLine: true), + r'Created temp directory $DIR', + ) + .replaceAll( + RegExp(r'Renaming directory (.*)$', multiLine: true), + r'Renaming directory $A to $B', + ) + .replaceAll( + RegExp(r'"_fetchedAt":"(.*)"}$', multiLine: true), + r'"_fetchedAt": "$TIME"}', + ) + .replaceAll( + RegExp(r'"generated": "(.*)",$', multiLine: true), + r'"generated": "$TIME",', + ) + .replaceAll( + RegExp(r'( |^)(/|[A-Z]:)(.*)/tool/test-bin/pub_command_runner.dart', + multiLine: true), + r' tool/test-bin/pub_command_runner.dart', + ) + .replaceAll( + RegExp(r'[ ]{4,}', multiLine: true), + r' ', + ) + .replaceAll( + RegExp(r' [\d]+:[\d]+ ', multiLine: true), + r' $LINE:$COL ', + ) + .replaceAll( + RegExp(r'Writing \d+ characters', multiLine: true), + r'Writing $N characters', + ); }
diff --git a/test/get/unknown_sdk_test.dart b/test/get/unknown_sdk_test.dart new file mode 100644 index 0000000..d0c1de3 --- /dev/null +++ b/test/get/unknown_sdk_test.dart
@@ -0,0 +1,25 @@ +// 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. + +import 'package:pub/src/exit_codes.dart' as exit_codes; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() { + test('pub get barks at unknown sdk', () async { + await d.dir(appPath, [ + d.pubspec({ + 'environment': {'foo': '>=1.2.4 <2.0.0'} + }) + ]).create(); + + await pubGet( + error: contains( + "Error on line 1, column 40 of pubspec.yaml: pubspec.yaml refers to an unknown sdk 'foo'."), + exitCode: exit_codes.DATA, + ); + }); +}
diff --git a/test/global/activate/activate_hosted_after_git_test.dart b/test/global/activate/activate_hosted_after_git_test.dart index 58c65fb..1546280 100644 --- a/test/global/activate/activate_hosted_after_git_test.dart +++ b/test/global/activate/activate_hosted_after_git_test.dart
@@ -2,6 +2,7 @@ // 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. +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import '../../descriptor.dart' as d; @@ -21,8 +22,9 @@ await runPub(args: ['global', 'activate', '-sgit', '../foo.git']); + final locationUri = p.toUri(p.join(d.sandbox, 'foo.git')); await runPub(args: ['global', 'activate', 'foo'], output: ''' - Package foo is currently active from Git repository "../foo.git". + Package foo is currently active from Git repository "$locationUri". Resolving dependencies... + foo 2.0.0 Downloading foo 2.0.0...
diff --git a/test/global/activate/activate_hosted_twice_test.dart b/test/global/activate/activate_hosted_twice_test.dart index 9baed5b..a4d1337 100644 --- a/test/global/activate/activate_hosted_twice_test.dart +++ b/test/global/activate/activate_hosted_twice_test.dart
@@ -24,15 +24,7 @@ d.dir('lib', [d.file('bar.dart', 'final version = "1.0.0";')]) ]); - await runPub(args: ['global', 'activate', 'foo'], output: ''' -Resolving dependencies... -+ bar 1.0.0 -+ foo 1.0.0 -Downloading foo 1.0.0... -Downloading bar 1.0.0... -Building package executables... -Built foo:foo. -Activated foo 1.0.0.'''); + await runPub(args: ['global', 'activate', 'foo'], output: anything); await runPub(args: ['global', 'activate', 'foo'], output: ''' Package foo is currently active at version 1.0.0.
diff --git a/test/global/activate/activate_path_after_hosted_test.dart b/test/global/activate/activate_path_after_hosted_test.dart index cfc28fd..01d493d 100644 --- a/test/global/activate/activate_path_after_hosted_test.dart +++ b/test/global/activate/activate_path_after_hosted_test.dart
@@ -10,7 +10,7 @@ import '../../test_pub.dart'; void main() { - test('activating a hosted package deactivates the path one', () async { + test('activating a path package deactivates the hosted one', () async { final server = await servePackages(); server.serve('foo', '1.0.0', contents: [ d.dir('bin', [d.file('foo.dart', "main(args) => print('hosted');")])
diff --git a/test/global/activate/reactivating_git_upgrades_test.dart b/test/global/activate/reactivating_git_upgrades_test.dart index 100aaea..68102da 100644 --- a/test/global/activate/reactivating_git_upgrades_test.dart +++ b/test/global/activate/reactivating_git_upgrades_test.dart
@@ -2,6 +2,7 @@ // 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. +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import '../../descriptor.dart' as d; @@ -29,11 +30,12 @@ await d.git('foo.git', [d.libPubspec('foo', '1.0.1')]).commit(); // Activating it again pulls down the latest commit. + final locationUri = p.toUri(p.join(d.sandbox, 'foo.git')); await runPub( args: ['global', 'activate', '-sgit', '../foo.git'], output: allOf( startsWith('Package foo is currently active from Git repository ' - '"../foo.git".\n' + '"$locationUri".\n' 'Resolving dependencies...\n' '+ foo 1.0.1 from git ../foo.git at '), // Specific revision number goes here.
diff --git a/test/global/activate/removes_old_lockfile_test.dart b/test/global/activate/removes_old_lockfile_test.dart deleted file mode 100644 index 3392aff..0000000 --- a/test/global/activate/removes_old_lockfile_test.dart +++ /dev/null
@@ -1,33 +0,0 @@ -// Copyright (c) 2014, 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. - -import 'package:test/test.dart'; - -import '../../descriptor.dart' as d; -import '../../test_pub.dart'; - -void main() { - test('removes the 1.6-style lockfile', () async { - final server = await servePackages(); - server.serve('foo', '1.0.0'); - - await d.dir(cachePath, [ - d.dir('global_packages', [ - d.file( - 'foo.lock', - 'packages: {foo: {description: foo, source: hosted, ' - 'version: "1.0.0"}}}') - ]) - ]).create(); - - await runPub(args: ['global', 'activate', 'foo']); - - await d.dir(cachePath, [ - d.dir('global_packages', [ - d.nothing('foo.lock'), - d.dir('foo', [d.file('pubspec.lock', contains('1.0.0'))]) - ]) - ]).validate(); - }); -}
diff --git a/test/global/deactivate/git_package_test.dart b/test/global/deactivate/git_package_test.dart index f06ce53..7fc2855 100644 --- a/test/global/deactivate/git_package_test.dart +++ b/test/global/deactivate/git_package_test.dart
@@ -2,6 +2,7 @@ // 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. +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import '../../descriptor.dart' as d; @@ -18,9 +19,10 @@ await runPub(args: ['global', 'activate', '-sgit', '../foo.git']); + final locationUri = p.toUri(p.join(d.sandbox, 'foo.git')); await runPub( args: ['global', 'deactivate', 'foo'], output: - 'Deactivated package foo 1.0.0 from Git repository "../foo.git".'); + 'Deactivated package foo 1.0.0 from Git repository "$locationUri".'); }); }
diff --git a/test/global/list_test.dart b/test/global/list_test.dart index 8d4d02e..00ca22c 100644 --- a/test/global/list_test.dart +++ b/test/global/list_test.dart
@@ -30,9 +30,10 @@ await runPub(args: ['global', 'activate', '-sgit', '../foo.git']); + final locationUri = p.toUri(p.join(d.sandbox, 'foo.git')); await runPub( args: ['global', 'list'], - output: 'foo 1.0.0 from Git repository "../foo.git"'); + output: 'foo 1.0.0 from Git repository "$locationUri"'); }); test('lists an activated Path package', () async {
diff --git a/test/global/run/uses_old_lockfile_test.dart b/test/global/run/uses_old_lockfile_test.dart deleted file mode 100644 index 1afab81..0000000 --- a/test/global/run/uses_old_lockfile_test.dart +++ /dev/null
@@ -1,54 +0,0 @@ -// Copyright (c) 2014, 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. - -import 'package:test/test.dart'; - -import '../../descriptor.dart' as d; -import '../../test_pub.dart'; - -void main() { - test('uses the 1.6-style lockfile if necessary', () async { - await servePackages() - ..serve('bar', '1.0.0') - ..serve('foo', '1.0.0', deps: { - 'bar': 'any' - }, contents: [ - d.dir('bin', [ - d.file('script.dart', """ - import 'package:bar/bar.dart' as bar; - - main(args) => print(bar.main());""") - ]) - ]); - - await runPub(args: ['cache', 'add', 'foo']); - await runPub(args: ['cache', 'add', 'bar']); - - await d.dir(cachePath, [ - d.dir('global_packages', [ - d.file('foo.lock', ''' -packages: - foo: - description: foo - source: hosted - version: "1.0.0" - bar: - description: bar - source: hosted - version: "1.0.0"''') - ]) - ]).create(); - - var pub = await pubRun(global: true, args: ['foo:script']); - expect(pub.stdout, emitsThrough('bar 1.0.0')); - await pub.shouldExit(); - - await d.dir(cachePath, [ - d.dir('global_packages', [ - d.nothing('foo.lock'), - d.dir('foo', [d.file('pubspec.lock', contains('1.0.0'))]) - ]) - ]).validate(); - }); -}
diff --git a/test/ignore_test.dart b/test/ignore_test.dart index 42a189a..a8ab8ac 100644 --- a/test/ignore_test.dart +++ b/test/ignore_test.dart
@@ -977,6 +977,13 @@ 'folder/a.txt': true, }), + TestData('folder/* does not ignore `folder` itself', { + '.': ['folder/*', '!folder/a.txt'], + }, { + 'folder/a.txt': false, + 'folder/b.txt': true, + }), + // Case sensitivity TestData( 'simple',
diff --git a/test/lish/archives_and_uploads_a_package_test.dart b/test/lish/archives_and_uploads_a_package_test.dart index 5bbac54..88e4862 100644 --- a/test/lish/archives_and_uploads_a_package_test.dart +++ b/test/lish/archives_and_uploads_a_package_test.dart
@@ -37,6 +37,36 @@ await pub.shouldExit(exit_codes.SUCCESS); }); + test('publishes to hosted-url with path', () async { + await servePackages(); + await d.tokensFile({ + 'version': 1, + 'hosted': [ + {'url': globalServer.url + '/sub/folder', 'env': 'TOKEN'}, + ] + }).create(); + var pub = await startPublish( + globalServer, + path: '/sub/folder', + authMethod: 'token', + environment: {'TOKEN': 'access token'}, + ); + + await confirmPublish(pub); + handleUploadForm(globalServer, path: '/sub/folder'); + handleUpload(globalServer); + + globalServer.expect('GET', '/create', (request) { + return shelf.Response.ok(jsonEncode({ + 'success': {'message': 'Package test_pkg 1.0.0 uploaded!'} + })); + }); + + expect(pub.stdout, emits(startsWith('Uploading...'))); + expect(pub.stdout, emits('Package test_pkg 1.0.0 uploaded!')); + await pub.shouldExit(exit_codes.SUCCESS); + }); + // This is a regression test for #1679. We create a submodule that's not // checked out to ensure that file listing doesn't choke on the empty // directory.
diff --git a/test/lish/dot_folder_name_test.dart b/test/lish/dot_folder_name_test.dart new file mode 100644 index 0000000..9f64839 --- /dev/null +++ b/test/lish/dot_folder_name_test.dart
@@ -0,0 +1,28 @@ +// Copyright (c) 2022, 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. + +import 'package:pub/src/exit_codes.dart' as exit_codes; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +void main() { + test('Can publish files in a .folder', () async { + await d.git(appPath).create(); + await d.validPackage.create(); + await d.dir(appPath, [ + d.dir('.vscode', [d.file('a')]), + d.file('.pubignore', '!.vscode/') + ]).create(); + + await runPub( + args: ['lish', '--dry-run'], + output: contains(''' +|-- .vscode +| '-- a'''), + exitCode: exit_codes.SUCCESS, + ); + }); +}
diff --git a/test/lish/upload_form_fields_has_a_non_string_value_test.dart b/test/lish/upload_form_fields_has_a_non_string_value_test.dart index 290e807..78a54fa 100644 --- a/test/lish/upload_form_fields_has_a_non_string_value_test.dart +++ b/test/lish/upload_form_fields_has_a_non_string_value_test.dart
@@ -24,7 +24,7 @@ 'url': 'http://example.com/upload', 'fields': {'field': 12} }; - handleUploadForm(globalServer, body); + handleUploadForm(globalServer, body: body); expect(pub.stderr, emits('Invalid server response:')); expect(pub.stderr, emits(jsonEncode(body))); await pub.shouldExit(1);
diff --git a/test/lish/upload_form_fields_is_not_a_map_test.dart b/test/lish/upload_form_fields_is_not_a_map_test.dart index ed87da6..d18c1ee 100644 --- a/test/lish/upload_form_fields_is_not_a_map_test.dart +++ b/test/lish/upload_form_fields_is_not_a_map_test.dart
@@ -21,7 +21,7 @@ await confirmPublish(pub); var body = {'url': 'http://example.com/upload', 'fields': 12}; - handleUploadForm(globalServer, body); + handleUploadForm(globalServer, body: body); expect(pub.stderr, emits('Invalid server response:')); expect(pub.stderr, emits(jsonEncode(body))); await pub.shouldExit(1);
diff --git a/test/lish/upload_form_is_missing_fields_test.dart b/test/lish/upload_form_is_missing_fields_test.dart index 79b6e0b..b0032f8 100644 --- a/test/lish/upload_form_is_missing_fields_test.dart +++ b/test/lish/upload_form_is_missing_fields_test.dart
@@ -21,7 +21,7 @@ await confirmPublish(pub); var body = {'url': 'http://example.com/upload'}; - handleUploadForm(globalServer, body); + handleUploadForm(globalServer, body: body); expect(pub.stderr, emits('Invalid server response:')); expect(pub.stderr, emits(jsonEncode(body))); await pub.shouldExit(1);
diff --git a/test/lish/upload_form_is_missing_url_test.dart b/test/lish/upload_form_is_missing_url_test.dart index e3986cb..eae43ea 100644 --- a/test/lish/upload_form_is_missing_url_test.dart +++ b/test/lish/upload_form_is_missing_url_test.dart
@@ -24,7 +24,7 @@ 'fields': {'field1': 'value1', 'field2': 'value2'} }; - handleUploadForm(globalServer, body); + handleUploadForm(globalServer, body: body); expect(pub.stderr, emits('Invalid server response:')); expect(pub.stderr, emits(jsonEncode(body))); await pub.shouldExit(1);
diff --git a/test/lish/upload_form_url_is_not_a_string_test.dart b/test/lish/upload_form_url_is_not_a_string_test.dart index 784289f..f69fd1f 100644 --- a/test/lish/upload_form_url_is_not_a_string_test.dart +++ b/test/lish/upload_form_url_is_not_a_string_test.dart
@@ -25,7 +25,7 @@ 'fields': {'field1': 'value1', 'field2': 'value2'} }; - handleUploadForm(globalServer, body); + handleUploadForm(globalServer, body: body); expect(pub.stderr, emits('Invalid server response:')); expect(pub.stderr, emits(jsonEncode(body))); await pub.shouldExit(1);
diff --git a/test/lish/utils.dart b/test/lish/utils.dart index 8e1ed5f..f134e4a 100644 --- a/test/lish/utils.dart +++ b/test/lish/utils.dart
@@ -9,8 +9,8 @@ import '../test_pub.dart'; -void handleUploadForm(PackageServer server, [Map? body]) { - server.expect('GET', '/api/packages/versions/new', (request) { +void handleUploadForm(PackageServer server, {Map? body, String path = ''}) { + server.expect('GET', '$path/api/packages/versions/new', (request) { expect( request.headers, containsPair('authorization', 'Bearer access token'));
diff --git a/test/test_pub.dart b/test/test_pub.dart index e180f3d..fbf63de 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart
@@ -373,11 +373,12 @@ List<String>? args, String authMethod = 'oauth2', Map<String, String>? environment, + String path = '', }) 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, + 'PUB_HOSTED_URL': server.url + path, '_PUB_TEST_AUTH_METHOD': authMethod, if (environment != null) ...environment, }); @@ -465,7 +466,7 @@ var dartArgs = ['--packages=$dotPackagesPath', '--enable-asserts']; dartArgs - ..addAll([pubPath, if (verbose) '--verbose']) + ..addAll([pubPath, if (!verbose) '--verbosity=normal']) ..addAll(args); final mergedEnvironment = getPubTestEnvironment(tokenEndpoint);
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt new file mode 100644 index 0000000..1d3a5f5 --- /dev/null +++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -0,0 +1,328 @@ +# GENERATED BY: test/embedding/embedding_test.dart + +$ tool/test-bin/pub_command_runner.dart pub --verbose get +MSG : Resolving dependencies... +MSG : + foo 1.0.0 +MSG : Downloading foo 1.0.0... +MSG : Changed 1 dependency! +MSG : Logs written to $SANDBOX/cache/log/pub_log.txt. +[E] FINE: Pub 0.1.2+3 +[E] SLVR: fact: myapp is 0.0.0 +[E] SLVR: derived: myapp +[E] SLVR: fact: myapp depends on foo any +[E] SLVR: selecting myapp +[E] SLVR: derived: foo any +[E] IO : Get versions from http://localhost:$PORT/api/packages/foo. +[E] IO : HTTP GET http://localhost:$PORT/api/packages/foo +[E] | Accept: application/vnd.pub.v2+json +[E] | X-Pub-OS: $OS +[E] | X-Pub-Command: get +[E] | X-Pub-Session-ID: $ID +[E] | X-Pub-Environment: test-environment +[E] | X-Pub-Reason: direct +[E] | user-agent: Dart pub 0.1.2+3 +[E] IO : HTTP response 200 OK for GET http://localhost:$PORT/api/packages/foo +[E] | took: $TIME +[E] | date: $TIME +[E] | content-length: 197 +[E] | x-frame-options: SAMEORIGIN +[E] | content-type: text/plain; charset=utf-8 +[E] | x-xss-protection: 1; mode=block +[E] | x-content-type-options: nosniff +[E] | server: dart:io with Shelf +[E] IO : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json. +[E] FINE: Contents: +[E] | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"} +[E] SLVR: selecting foo 1.0.0 +[E] SLVR: Version solving took: $TIME +[E] | Tried 1 solutions. +[E] FINE: Resolving dependencies finished ($TIME) +[E] IO : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz. +[E] IO : Created temp directory $DIR +[E] IO : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz +[E] | X-Pub-OS: $OS +[E] | X-Pub-Command: get +[E] | X-Pub-Session-ID: $ID +[E] | X-Pub-Environment: test-environment +[E] | X-Pub-Reason: direct +[E] | user-agent: Dart pub 0.1.2+3 +[E] IO : HTTP response 200 OK for GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz +[E] | took: $TIME +[E] | transfer-encoding: chunked +[E] | date: $TIME +[E] | x-frame-options: SAMEORIGIN +[E] | content-type: text/plain; charset=utf-8 +[E] | x-xss-protection: 1; mode=block +[E] | x-content-type-options: nosniff +[E] | server: dart:io with Shelf +[E] IO : Creating $FILE from stream +[E] FINE: Created $FILE from stream +[E] IO : Created temp directory $DIR +[E] IO : Reading binary file $FILE. +[E] FINE: Extracting .tar.gz stream to $DIR +[E] IO : Creating $FILE from stream +[E] FINE: Created $FILE from stream +[E] IO : Creating $FILE from stream +[E] FINE: Created $FILE from stream +[E] FINE: Extracted .tar.gz to $DIR +[E] IO : Renaming directory $A to $B +[E] IO : Deleting directory $DIR +[E] IO : Writing $N characters to text file pubspec.lock. +[E] FINE: Contents: +[E] | # Generated by pub +[E] | # See https://dart.dev/tools/pub/glossary#lockfile +[E] | packages: +[E] | foo: +[E] | dependency: "direct main" +[E] | description: +[E] | name: foo +[E] | url: "http://localhost:$PORT" +[E] | source: hosted +[E] | version: "1.0.0" +[E] | sdks: +[E] | dart: ">=0.1.2 <1.0.0" +[E] IO : Writing $N characters to text file .packages. +[E] FINE: Contents: +[E] | # This file is deprecated. Tools should instead consume +[E] | # `.dart_tool/package_config.json`. +[E] | # +[E] | # For more info see: https://dart.dev/go/dot-packages-deprecation +[E] | # +[E] | # Generated by pub on $TIME +[E] | foo:file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0/lib/ +[E] | myapp:lib/ +[E] IO : Writing $N characters to text file .dart_tool/package_config.json. +[E] FINE: Contents: +[E] | { +[E] | "configVersion": 2, +[E] | "packages": [ +[E] | { +[E] | "name": "foo", +[E] | "rootUri": "file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0", +[E] | "packageUri": "lib/", +[E] | "languageVersion": "2.7" +[E] | }, +[E] | { +[E] | "name": "myapp", +[E] | "rootUri": "../", +[E] | "packageUri": "lib/", +[E] | "languageVersion": "0.1" +[E] | } +[E] | ], +[E] | "generated": "$TIME", +[E] | "generator": "pub", +[E] | "generatorVersion": "0.1.2+3" +[E] | } +[E] IO : Writing $N characters to text file $SANDBOX/cache/log/pub_log.txt. + +-------------------------------- END OF OUTPUT --------------------------------- + +Information about the latest pub run. + +If you believe something is not working right, you can go to +https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file. + +Before making this file public, make sure to remove any sensitive information! + +Pub version: 0.1.2+3 +Created: $TIME +FLUTTER_ROOT: <not set> +PUB_HOSTED_URL: http://localhost:$PORT +PUB_CACHE: "$SANDBOX/cache" +Command: dart pub --verbose get +Platform: $OS + +---- $SANDBOX/myapp/pubspec.yaml ---- +{"name":"myapp","environment":{"sdk":">=0.1.2 <1.0.0"},"dependencies":{"foo":"any"}} +---- End pubspec.yaml ---- +---- $SANDBOX/myapp/pubspec.lock ---- +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + foo: + dependency: "direct main" + description: + name: foo + url: "http://localhost:$PORT" + source: hosted + version: "1.0.0" +sdks: + dart: ">=0.1.2 <1.0.0" + +---- End pubspec.lock ---- +---- Log transcript ---- +FINE: Pub 0.1.2+3 +MSG : Resolving dependencies... +SLVR: fact: myapp is 0.0.0 +SLVR: derived: myapp +SLVR: fact: myapp depends on foo any +SLVR: selecting myapp +SLVR: derived: foo any +IO : Get versions from http://localhost:$PORT/api/packages/foo. +IO : HTTP GET http://localhost:$PORT/api/packages/foo + | Accept: application/vnd.pub.v2+json + | X-Pub-OS: $OS + | X-Pub-Command: get + | X-Pub-Session-ID: $ID + | X-Pub-Environment: test-environment + | X-Pub-Reason: direct + | user-agent: Dart pub 0.1.2+3 +IO : HTTP response 200 OK for GET http://localhost:$PORT/api/packages/foo + | took: $TIME + | date: $TIME + | content-length: 197 + | x-frame-options: SAMEORIGIN + | content-type: text/plain; charset=utf-8 + | x-xss-protection: 1; mode=block + | x-content-type-options: nosniff + | server: dart:io with Shelf +IO : Writing $N characters to text file $SANDBOX/cache/hosted/localhost%58$PORT/.cache/foo-versions.json. +FINE: Contents: + | {"name":"foo","uploaders":["nweiz@google.com"],"versions":[{"pubspec":{"name":"foo","version":"1.0.0"},"version":"1.0.0","archive_url":"http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz"}],"_fetchedAt": "$TIME"} +SLVR: selecting foo 1.0.0 +SLVR: Version solving took: $TIME + | Tried 1 solutions. +FINE: Resolving dependencies finished ($TIME) +MSG : + foo 1.0.0 +IO : Get package from http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz. +MSG : Downloading foo 1.0.0... +IO : Created temp directory $DIR +IO : HTTP GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz + | X-Pub-OS: $OS + | X-Pub-Command: get + | X-Pub-Session-ID: $ID + | X-Pub-Environment: test-environment + | X-Pub-Reason: direct + | user-agent: Dart pub 0.1.2+3 +IO : HTTP response 200 OK for GET http://localhost:$PORT/packages/foo/versions/1.0.0.tar.gz + | took: $TIME + | transfer-encoding: chunked + | date: $TIME + | x-frame-options: SAMEORIGIN + | content-type: text/plain; charset=utf-8 + | x-xss-protection: 1; mode=block + | x-content-type-options: nosniff + | server: dart:io with Shelf +IO : Creating $FILE from stream +FINE: Created $FILE from stream +IO : Created temp directory $DIR +IO : Reading binary file $FILE. +FINE: Extracting .tar.gz stream to $DIR +IO : Creating $FILE from stream +FINE: Created $FILE from stream +IO : Creating $FILE from stream +FINE: Created $FILE from stream +FINE: Extracted .tar.gz to $DIR +IO : Renaming directory $A to $B +IO : Deleting directory $DIR +IO : Writing $N characters to text file pubspec.lock. +FINE: Contents: + | # Generated by pub + | # See https://dart.dev/tools/pub/glossary#lockfile + | packages: + | foo: + | dependency: "direct main" + | description: + | name: foo + | url: "http://localhost:$PORT" + | source: hosted + | version: "1.0.0" + | sdks: + | dart: ">=0.1.2 <1.0.0" +MSG : Changed 1 dependency! +IO : Writing $N characters to text file .packages. +FINE: Contents: + | # This file is deprecated. Tools should instead consume + | # `.dart_tool/package_config.json`. + | # + | # For more info see: https://dart.dev/go/dot-packages-deprecation + | # + | # Generated by pub on $TIME + | foo:file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0/lib/ + | myapp:lib/ +IO : Writing $N characters to text file .dart_tool/package_config.json. +FINE: Contents: + | { + | "configVersion": 2, + | "packages": [ + | { + | "name": "foo", + | "rootUri": "file://$SANDBOX/cache/hosted/localhost%2558$PORT/foo-1.0.0", + | "packageUri": "lib/", + | "languageVersion": "2.7" + | }, + | { + | "name": "myapp", + | "rootUri": "../", + | "packageUri": "lib/", + | "languageVersion": "0.1" + | } + | ], + | "generated": "$TIME", + | "generator": "pub", + | "generatorVersion": "0.1.2+3" + | } +---- End log transcript ---- +-------------------------------- END OF OUTPUT --------------------------------- + +$ tool/test-bin/pub_command_runner.dart pub fail +[E] Bad state: Pub has crashed +[E] tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected +[E] package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn> +[E] package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn> +[E] dart:async new Future.sync +[E] package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback +[E] dart:async runZonedGuarded +[E] package:pub/src/utils.dart $LINE:$COL captureErrors +[E] package:pub/src/command.dart $LINE:$COL PubCommand.run +[E] package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand +[E] tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand +[E] tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.run +[E] tool/test-bin/pub_command_runner.dart $LINE:$COL main +[E] This is an unexpected error. The full log and other details are collected in: +[E] +[E] $SANDBOX/cache/log/pub_log.txt +[E] +[E] Consider creating an issue on https://github.com/dart-lang/pub/issues/new +[E] and attaching the relevant parts of that log file. + +-------------------------------- END OF OUTPUT --------------------------------- + +Information about the latest pub run. + +If you believe something is not working right, you can go to +https://github.com/dart-lang/pub/issues/new to post a new issue and attach this file. + +Before making this file public, make sure to remove any sensitive information! + +Pub version: 0.1.2+3 +Created: $TIME +FLUTTER_ROOT: <not set> +PUB_HOSTED_URL: http://localhost:$PORT +PUB_CACHE: "$SANDBOX/cache" +Command: dart pub fail +Platform: $OS + +---- Log transcript ---- +FINE: Pub 0.1.2+3 +ERR : Bad state: Pub has crashed +FINE: Exception type: StateError +ERR : tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected + | package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn> + | package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn> + | dart:async new Future.sync + | package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback + | dart:async runZonedGuarded + | package:pub/src/utils.dart $LINE:$COL captureErrors + | package:pub/src/command.dart $LINE:$COL PubCommand.run + | package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand + | tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand + | tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.run + | tool/test-bin/pub_command_runner.dart $LINE:$COL main +ERR : This is an unexpected error. The full log and other details are collected in: + | + | $SANDBOX/cache/log/pub_log.txt + | + | Consider creating an issue on https://github.com/dart-lang/pub/issues/new + | and attaching the relevant parts of that log file. +---- End log transcript ----
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt index 5d32e5e..2a37c21 100644 --- a/test/testdata/goldens/help_test/pub add --help.txt +++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -2,22 +2,23 @@ ## Section 0 $ pub add --help -Add a dependency to pubspec.yaml. +Add dependencies to pubspec.yaml. -Usage: pub add <package>[:<constraint>] [options] +Usage: pub add <package>[:<constraint>] [<package2>[:<constraint2>]...] [options] -h, --help Print this usage information. --d, --dev Adds package to the development dependencies instead. +-d, --dev Adds to the development dependencies instead. --git-url Git URL of the package --git-ref Git branch or commit to be retrieved --git-path Path of git package in repository --hosted-url URL of package host server - --path Local path - --sdk SDK source for package + --path Add package from local path + --sdk=<[flutter]> add package from SDK source + [flutter] --[no-]offline Use cached packages instead of accessing the network. -n, --dry-run Report what dependencies would change but don't change any. --[no-]precompile Build executables in immediate dependencies. --C, --directory=<dir> Run this in the directory<dir>. +-C, --directory=<dir> Run this in the directory <dir>. Run "pub help" to see global options. See https://dart.dev/tools/pub/cmd/pub-add for detailed documentation.
diff --git a/tool/test-bin/pub_command_runner.dart b/tool/test-bin/pub_command_runner.dart index 2be51e8..e438aae 100644 --- a/tool/test-bin/pub_command_runner.dart +++ b/tool/test-bin/pub_command_runner.dart
@@ -9,12 +9,30 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:pub/pub.dart'; +import 'package:pub/src/command.dart'; import 'package:pub/src/exit_codes.dart' as exit_codes; import 'package:pub/src/log.dart' as log; import 'package:usage/usage.dart'; final _LoggingAnalytics loggingAnalytics = _LoggingAnalytics(); +// A command for explicitly throwing an exception, to test the handling of +// unexpected eceptions. +class ThrowingCommand extends PubCommand { + @override + String get name => 'fail'; + + @override + String get description => 'Throws an exception'; + + bool get hide => true; + + @override + Future<int> runProtected() async { + throw StateError('Pub has crashed'); + } +} + class Runner extends CommandRunner<int> { late ArgResults _options; @@ -23,7 +41,10 @@ ? PubAnalytics(() => loggingAnalytics, dependencyKindCustomDimensionName: 'cd1') : null; - addCommand(pubCommand(analytics: analytics)); + addCommand( + pubCommand(analytics: analytics, isVerbose: () => _options['verbose']) + ..addSubcommand(ThrowingCommand())); + argParser.addFlag('verbose'); } @override
diff --git a/tool/test.dart b/tool/test.dart index e7877a2..7d98156 100755 --- a/tool/test.dart +++ b/tool/test.dart
@@ -29,7 +29,7 @@ await precompile( executablePath: path.join('bin', 'pub.dart'), outputPath: pubSnapshotFilename, - incrementalDillOutputPath: pubSnapshotIncrementalFilename, + incrementalDillPath: pubSnapshotIncrementalFilename, name: 'bin/pub.dart', packageConfigPath: path.join('.dart_tool', 'package_config.json')); testProcess = await Process.start(