| // Copyright (c) 2024, 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_semver/pub_semver.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import '../command.dart'; | |
| import '../command_runner.dart'; | |
| import '../entrypoint.dart'; | |
| import '../io.dart'; | |
| import '../log.dart' as log; | |
| import '../package_name.dart'; | |
| import '../path.dart'; | |
| import '../pubspec.dart'; | |
| import '../sdk.dart'; | |
| import '../solver/type.dart'; | |
| import '../source/hosted.dart'; | |
| import '../source/root.dart'; | |
| import '../utils.dart'; | |
| class UnpackCommand extends PubCommand { | |
| @override | |
| String get name => 'unpack'; | |
| @override | |
| String get description => ''' | |
| Downloads a package and unpacks it in place. | |
| For example: | |
| $topLevelProgram pub unpack foo | |
| Downloads and extracts the latest stable version of package:foo from pub.dev | |
| in a directory `foo-<version>`. | |
| $topLevelProgram pub unpack foo@1.2.3-pre --no-resolve | |
| Downloads and extracts package:foo version 1.2.3-pre in a directory | |
| `foo-1.2.3-pre` without running implicit `pub get`. | |
| $topLevelProgram pub unpack foo --output=archives | |
| Downloads and extracts the latest stable version of package:foo in a directory | |
| `archives/foo-<version>`. | |
| $topLevelProgram pub unpack 'foo@{hosted:"https://my_repo.org"}' | |
| Downloads and extracts the latest stable version of package:foo from my_repo.org | |
| in a directory `foo-<version>`. | |
| '''; | |
| @override | |
| String get argumentsDescription => 'package-name[@<descriptor>]'; | |
| @override | |
| String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-unpack'; | |
| @override | |
| bool get takesArguments => true; | |
| UnpackCommand() { | |
| argParser.addFlag( | |
| 'resolve', | |
| help: 'Whether to run pub get in the downloaded folder.', | |
| defaultsTo: true, | |
| hide: log.verbosity != log.Verbosity.all, | |
| ); | |
| argParser.addFlag( | |
| 'force', | |
| abbr: 'f', | |
| help: 'Overwrite the target directory if it already exists.', | |
| ); | |
| argParser.addOption( | |
| 'output', | |
| abbr: 'o', | |
| help: 'Download and extract the package in the specified directory.', | |
| defaultsTo: '.', | |
| ); | |
| } | |
| static final _argRegExp = RegExp( | |
| r'^(?<name>[a-zA-Z0-9_.]+)' | |
| r'(?:[:@](?<descriptor>.*))?$', | |
| ); | |
| @override | |
| Future<void> runProtected() async { | |
| if (argResults.rest.isEmpty) { | |
| usageException('Provide a package name.'); | |
| } | |
| if (argResults.rest.length > 1) { | |
| usageException('Provide only a single package name.'); | |
| } | |
| final arg = argResults.rest[0]; | |
| final match = _argRegExp.firstMatch(arg); | |
| if (match == null) { | |
| usageException('Use the form package@descriptor to specify the package.'); | |
| } | |
| final parseResult = _parseDescriptor( | |
| match.namedGroup('name')!, | |
| match.namedGroup('descriptor'), | |
| ); | |
| if (parseResult.description is! HostedDescription) { | |
| fail('Can only fetch hosted packages.'); | |
| } | |
| final versions = await parseResult.source.doGetVersions( | |
| parseResult.toRef(), | |
| null, | |
| cache, | |
| ); | |
| final constraint = parseResult.constraint; | |
| versions.removeWhere((id) => !constraint.allows(id.version)); | |
| if (versions.isEmpty) { | |
| fail('No matching versions of ${parseResult.name}.'); | |
| } | |
| versions.sort((id1, id2) => id1.version.compareTo(id2.version)); | |
| final id = versions.last; | |
| final name = id.name; | |
| final outputArg = argResults.optionWithDefault('output'); | |
| final destinationDir = p.join(outputArg, '$name-${id.version}'); | |
| if (entryExists(destinationDir)) { | |
| if (argResults.flag('force')) { | |
| deleteEntry(destinationDir); | |
| } else { | |
| fail( | |
| 'Target directory `$destinationDir` already exists. ' | |
| 'Use --force to overwrite.', | |
| ); | |
| } | |
| } | |
| await log.progress( | |
| 'Downloading $name ${id.version} to `$destinationDir`', | |
| () async { | |
| await cache.hosted.downloadInto(id, destinationDir, cache); | |
| }, | |
| ); | |
| if (argResults.flag('resolve')) { | |
| try { | |
| final pubspec = Pubspec.load( | |
| destinationDir, | |
| cache.sources, | |
| containingDescription: ResolvedRootDescription.fromDir( | |
| destinationDir, | |
| ), | |
| ); | |
| final buffer = StringBuffer(); | |
| if (pubspec.resolution != Resolution.none) { | |
| log.message(''' | |
| This package was developed as part of a workspace. | |
| Creating `pubspec_overrides.yaml` to resolve it alone.'''); | |
| buffer.writeln('resolution:'); | |
| } | |
| if (pubspec.dependencyOverrides.isNotEmpty) { | |
| log.message(''' | |
| This package was developed with dependency_overrides. | |
| Creating `pubspec_overrides.yaml` to resolve it without those overrides.'''); | |
| buffer.writeln('dependency_overrides:'); | |
| } | |
| if (buffer.isNotEmpty) { | |
| writeTextFile( | |
| p.join(destinationDir, 'pubspec_overrides.yaml'), | |
| buffer.toString(), | |
| ); | |
| } | |
| final e = Entrypoint(destinationDir, cache); | |
| await e.acquireDependencies(SolveType.get); | |
| } finally { | |
| log.message('To explore type: cd $destinationDir'); | |
| final exampleDir = p.join(destinationDir, 'example'); | |
| if (dirExists(exampleDir)) { | |
| log.message('To explore the example type: cd $exampleDir'); | |
| } | |
| } | |
| } | |
| } | |
| PackageRange _parseDescriptor(String packageName, String? descriptor) { | |
| late final defaultDescription = HostedDescription( | |
| packageName, | |
| cache.hosted.defaultUrl, | |
| ); | |
| if (descriptor == null) { | |
| return PackageRange( | |
| PackageRef(packageName, defaultDescription), | |
| VersionConstraint.any, | |
| ); | |
| } | |
| try { | |
| // An unquoted version constraint is not always valid yaml. | |
| // But we want to allow it here anyways. | |
| final constraint = VersionConstraint.parse(descriptor); | |
| return PackageRange( | |
| PackageRef(packageName, defaultDescription), | |
| constraint, | |
| ); | |
| } on FormatException { | |
| final parsedDescriptor = loadYaml(descriptor); | |
| // Use the pubspec parsing mechanism for parsing the descriptor. | |
| final Pubspec dummyPubspec; | |
| try { | |
| dummyPubspec = Pubspec.fromMap( | |
| { | |
| 'dependencies': {packageName: parsedDescriptor}, | |
| 'environment': {'sdk': sdk.version.toString()}, | |
| }, | |
| cache.sources, | |
| // Resolve relative paths relative to current, not where the | |
| // pubspec.yaml is. | |
| location: p.toUri(p.join(p.current, 'descriptor')), | |
| containingDescription: ResolvedRootDescription.fromDir('.'), | |
| ); | |
| } on FormatException catch (e) { | |
| usageException('Failed parsing package specification: ${e.message}'); | |
| } | |
| return dummyPubspec.dependencies[packageName]!; | |
| } | |
| } | |
| } |