Skip to content

Commit

Permalink
Implement command "unpack" (#4111)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigurdm authored Feb 15, 2024
1 parent f68b0e1 commit 235e942
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 0 deletions.
192 changes: 192 additions & 0 deletions lib/src/command/unpack.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// 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 '../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 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 running implicit `pub get`.
$topLevelProgram pub unpack foo --output=archives
Downloads and extracts latest stable version of package:foo in a directory
`archives/foo-<version>`.
$topLevelProgram pub unpack 'foo:{hosted:"https://my_repo.org"}'
Downloads and extracts latest stable version of package:foo from my_repo.org
in a directory `foo-<version>`.
''';

@override
String get argumentsDescription => 'package-name[:constraint]';

@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-unpack';

@override
bool get takesArguments => true;

UnpackCommand() {
argParser.addFlag(
'resolve',
help: 'Whether to do pub get in the downloaded folder',
defaultsTo: true,
hide: log.verbosity != log.Verbosity.all,
);
argParser.addFlag(
'force',
abbr: 'f',
help: 'overwrites an existing folder if it exists',
);
argParser.addOption(
'output',
abbr: 'o',
help: 'Download and extract the package in this dir',
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('Please provide only a single package name');
}
final arg = argResults.rest[0];
final match = _argRegExp.firstMatch(arg);
if (match == null) {
usageException('Use the form package:constraint 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['output'] as String;
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);
},
);
final e = Entrypoint(
destinationDir,
cache,
);
if (argResults['resolve'] as bool) {
try {
await e.acquireDependencies(SolveType.get);
} finally {
log.message('To explore type: cd $destinationDir');
if (e.example != null) {
log.message('To explore the example type: cd ${e.example!.rootDir}');
}
}
}
}

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')),
);
} on FormatException catch (e) {
usageException('Failed parsing package specification: ${e.message}');
}
return dummyPubspec.dependencies[packageName]!;
}
}
}
2 changes: 2 additions & 0 deletions lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'command/outdated.dart';
import 'command/remove.dart';
import 'command/run.dart';
import 'command/token.dart';
import 'command/unpack.dart';
import 'command/upgrade.dart';
import 'command/uploader.dart';
import 'command/version.dart';
Expand Down Expand Up @@ -148,6 +149,7 @@ class PubCommandRunner extends CommandRunner<int> implements PubTopLevel {
addCommand(RemoveCommand());
addCommand(RunCommand());
addCommand(UpgradeCommand());
addCommand(UnpackCommand());
addCommand(UploaderCommand());
addCommand(LoginCommand());
addCommand(LogoutCommand());
Expand Down
2 changes: 2 additions & 0 deletions lib/src/pub_embeddable_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'command/outdated.dart';
import 'command/remove.dart';
import 'command/run.dart';
import 'command/token.dart';
import 'command/unpack.dart';
import 'command/upgrade.dart';
import 'command/uploader.dart';
import 'log.dart' as log;
Expand Down Expand Up @@ -77,6 +78,7 @@ class PubEmbeddableCommand extends PubCommand implements PubTopLevel {
addSubcommand(OutdatedCommand());
addSubcommand(RemoveCommand());
addSubcommand(RunCommand(deprecated: true, alwaysUseSubprocess: true));
addSubcommand(UnpackCommand());
addSubcommand(UpgradeCommand());
addSubcommand(UploaderCommand());
addSubcommand(LoginCommand());
Expand Down
7 changes: 7 additions & 0 deletions lib/src/source/hosted.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,13 @@ class HostedSource extends CachedSource {
.toList();
}

Future<void> downloadInto(
PackageId id,
String destPath,
SystemCache cache,
) =>
_download(id, destPath, cache);

/// Downloads package [package] at [version] from the archive_url and unpacks
/// it into [destPath].
///
Expand Down
1 change: 1 addition & 0 deletions test/testdata/goldens/embedding/embedding_test/--help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Available subcommands:
publish Publish the current package to pub.dev.
remove Removes dependencies from `pubspec.yaml`.
token Manage authentication tokens for hosted pub repositories.
unpack Downloads a package and unpacks it in place.
upgrade Upgrade the current package's dependencies to latest versions.

Run "pub_command_runner help" to see global options.
Expand Down
38 changes: 38 additions & 0 deletions test/testdata/goldens/help_test/pub unpack --help.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# GENERATED BY: test/help_test.dart

## Section 0
$ pub unpack --help
Downloads a package and unpacks it in place.

For example:

dart pub unpack foo

Downloads and extracts the latest stable package:foo from pub.dev in a
directory `foo-<version>`.

dart 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 running implicit `pub get`.

dart pub unpack foo --output=archives

Downloads and extracts latest stable version of package:foo in a directory
`archives/foo-<version>`.

dart pub unpack 'foo:{hosted:"https://my_repo.org"}'

Downloads and extracts latest stable version of package:foo from my_repo.org
in a directory `foo-<version>`.


Usage: pub unpack package-name[:constraint]
-h, --help Print this usage information.
-f, --[no-]force overwrites an existing folder if it exists
-o, --output Download and extract the package in this dir
(defaults to ".")

Run "pub help" to see global options.
See https://dart.dev/tools/pub/cmd/pub-unpack for detailed documentation.

Loading

0 comments on commit 235e942

Please sign in to comment.