| // 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:path/path.dart' as p; |
| 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 '../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]!; |
| } |
| } |
| } |