Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement command "unpack" #4111

Merged
merged 10 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1353,6 +1353,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
Loading