From 528d4faff65d11822595d5262c43dcd742333440 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Tue, 10 Feb 2026 05:50:12 -0800 Subject: [PATCH] dart install and dart remote run with descriptors Bug: https://github.com/dart-lang/sdk/issues/62123 Change-Id: I16e4fc0c20b9728e8357b6f67540aec73b2804ce Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/477560 Reviewed-by: Daco Harkes Reviewed-by: Jonas Jensen Commit-Queue: Sigurd Meldgaard --- pkg/dartdev/lib/src/commands/install.dart | 292 ++++++++++-------- pkg/dartdev/lib/src/commands/run.dart | 289 +++++------------ pkg/dartdev/test/commands/help_test.dart | 2 +- pkg/dartdev/test/commands/run_test.dart | 8 +- .../test/data/dart_app/bin/other_app.dart | 3 + pkg/dartdev/test/data/dart_app/pubspec.yaml | 1 + .../test/native_assets/install_test.dart | 61 ++-- .../test/native_assets/run_remote_test.dart | 182 +++++------ 8 files changed, 361 insertions(+), 477 deletions(-) create mode 100644 pkg/dartdev/test/data/dart_app/bin/other_app.dart diff --git a/pkg/dartdev/lib/src/commands/install.dart b/pkg/dartdev/lib/src/commands/install.dart index aedf3294e89..c857c05d8f2 100644 --- a/pkg/dartdev/lib/src/commands/install.dart +++ b/pkg/dartdev/lib/src/commands/install.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 'dart:convert'; import 'dart:io'; import 'package:dartdev/src/commands/build.dart'; @@ -12,6 +13,7 @@ import 'package:front_end/src/api_prototype/compiler_options.dart' import 'package:path/path.dart' as p; import 'package:pub/pub.dart'; import 'package:pub_formats/pub_formats.dart'; +import 'package:yaml/yaml.dart'; import '../core.dart'; @@ -27,11 +29,20 @@ executables. If the same package has been previously installed, it will be overwritten. -You can specify three different values for the argument: -1. A package name. This will install the package from pub.dev. (hosted) - The [version-constraint] argument can only be passed to 'hosted'. -2. A git url. This will install the package from a git repository. (git) -3. A path on your machine. This will install the package from that path. (path)'''; +You can specify a package to install from pub.dev, a git repository, or a +local path using the `[@]` syntax. + +The `@` can be a version constraint (for hosted packages) or a +pub descriptor (consistent with pubspec.yaml). + +Examples: + dart install + dart install @^3.0.0 + dart install '@{hosted: https://pub.dev, version: ^3.0.0}' + dart install '@{git: {url: https://github.com//, path: }}' + dart install '@{path: /path/to/}' + +See https://dart.dev/go/pub-descriptors for more details.'''; static const int genericErrorExitCode = 255; static const gitRefOption = 'git-ref'; @@ -40,7 +51,7 @@ You can specify three different values for the argument: @override String get invocation { final superNoArguments = super.invocation.replaceAll(' [arguments]', ''); - return '$superNoArguments [version-constraint]'; + return '$superNoArguments [@]'; } @override @@ -53,6 +64,7 @@ You can specify three different values for the argument: help: 'Path of git package in repository. ' 'Only applies when using a git url for .', + hide: true, ); argParser.addOption( @@ -60,6 +72,7 @@ You can specify three different values for the argument: help: 'Git branch or commit to be retrieved. ' 'Only applies when using a git url for .', + hide: true, ); argParser.addFlag( @@ -74,6 +87,7 @@ You can specify three different values for the argument: help: 'A custom pub server URL for the package. ' 'Only applies when using a package name for .', + hide: true, ); } @@ -95,71 +109,99 @@ You can specify three different values for the argument: return arg; } - final argument = readArg('No package source given.'); - final sourceKind = _soureKindFromArgument(argument); - + final firstArgument = readArg('Specify a package to install.'); final gitPath = argResults.option(gitPathOption); var gitRef = argResults.option(gitRefOption); - if (sourceKind != RemoteSourceKind.git && - (gitPath != null || gitRef != null)) { - usageException( - 'Options `--$gitPathOption` and `--$gitRefOption` ' - 'can only be used with a git source.', + + final atIndex = firstArgument.indexOf('@'); + if (firstArgument.startsWith('git@') || atIndex == -1) { + final sourceKind = _sourceKindFromArgument(firstArgument); + final versionConstraint = sourceKind == RemoteSourceKind.hosted + ? (args.isEmpty ? 'any' : readArg()) + : null; + + if (sourceKind != RemoteSourceKind.git && + (gitPath != null || gitRef != null)) { + usageException( + 'Options `--$gitPathOption` and `--$gitRefOption` ' + 'can only be used with a git source.', + ); + } + + final hostedUrl = argResults.option('hosted-url'); + if (sourceKind != RemoteSourceKind.hosted && hostedUrl != null) { + usageException( + 'Option `--hosted-url` can only be used with a hosted source.', + ); + } + + if (args.isNotEmpty) { + usageException( + 'Too many arguments, did not expect "${args.join(' ')}"', + ); + } + return NonDescriptorInstallCommandParsedArguments( + source: firstArgument, + sourceKind: sourceKind, + versionConstraint: versionConstraint, + gitPath: gitPath, + gitRef: gitRef, + hostedUrl: hostedUrl, + overwrite: overwrite, + ); + } else { + if (gitPath != null || gitRef != null) { + usageException( + 'Options `--$gitPathOption` and `--$gitRefOption` ' + 'cannot be used with the @ descriptor syntax.', + ); + } + final Object? descriptor; + final packageName = firstArgument.substring(0, atIndex); + final descriptorString = firstArgument.substring(atIndex + 1); + try { + descriptor = loadYaml(descriptorString); + } on FormatException catch (e) { + usageException( + 'Could not parse (what comes after @) "$descriptorString": $e', + ); + } + return DescriptorInstallCommandParsedArguments( + packageName: packageName, + descriptor: descriptor, + overwrite: overwrite, ); } - - final hostedUrl = argResults.option('hosted-url'); - if (sourceKind != RemoteSourceKind.hosted && hostedUrl != null) { - usageException( - 'Option `--hosted-url` can only be used with a hosted source.', - ); - } - - String? versionConstraint; - switch (sourceKind) { - case RemoteSourceKind.git: - case RemoteSourceKind.path: - break; - case RemoteSourceKind.hosted: - versionConstraint = args.isEmpty ? 'any' : readArg(); - } - if (args.isNotEmpty) { - usageException('Too many arguments, did not expect "${args.join(' ')}"'); - } - return InstallCommandParsedArguments( - source: argument, - sourceKind: sourceKind, - versionConstraint: versionConstraint, - gitPath: gitPath, - gitRef: gitRef, - hostedUrl: hostedUrl, - overwrite: overwrite, - ); } Future _findPackageName( InstallCommandParsedArguments parsedArgs, ) async { - switch (parsedArgs.sourceKind) { - case RemoteSourceKind.git: - return await getPackageNameFromGitRepo( - parsedArgs.source, - ref: parsedArgs.gitRef, - path: parsedArgs.gitPath, - relativeTo: Directory.current.path, - tagPattern: null, - ); - case RemoteSourceKind.hosted: - return parsedArgs.source; - case RemoteSourceKind.path: - final pubspecFile = File.fromUri( - Directory(parsedArgs.source).absolute.uri.resolve('pubspec.yaml'), - ); - if (!await pubspecFile.exists()) { - usageException('No pubspec found in ${pubspecFile.path}.'); + switch (parsedArgs) { + case DescriptorInstallCommandParsedArguments _: + return parsedArgs.packageName; + case NonDescriptorInstallCommandParsedArguments _: + switch (parsedArgs.sourceKind) { + case RemoteSourceKind.git: + return await getPackageNameFromGitRepo( + parsedArgs.source, + ref: parsedArgs.gitRef, + path: parsedArgs.gitPath, + relativeTo: Directory.current.path, + tagPattern: null, + ); + case RemoteSourceKind.hosted: + return parsedArgs.source; + case RemoteSourceKind.path: + final pubspecFile = File.fromUri( + Directory(parsedArgs.source).absolute.uri.resolve('pubspec.yaml'), + ); + if (!await pubspecFile.exists()) { + usageException('No pubspec found in ${pubspecFile.path}.'); + } + final pubspecYaml = PubspecYamlFile.loadSync(pubspecFile); + return pubspecYaml.name; } - final pubspecYaml = PubspecYamlFile.loadSync(pubspecFile); - return pubspecYaml.name; } } @@ -180,34 +222,35 @@ You can specify three different values for the argument: final tempPubspec = File.fromUri( helperPackageDir.uri.resolve('pubspec.yaml'), ); - final helperPackagePubspec = PubspecYamlFileSyntax( - name: _helperPackageName, - environment: EnvironmentSyntax( - sdk: '^${Platform.version.split(' ').first}', - ), - dependencies: { - packageName: switch (parsedArgs.sourceKind) { - RemoteSourceKind.git => GitDependencySourceSyntax( - git: GitSyntax( - url: parsedArgs.source, - path$: parsedArgs.gitPath, - ref: parsedArgs.gitRef, - ), - ), - RemoteSourceKind.hosted => HostedDependencySourceSyntax( - hosted: parsedArgs.hostedUrl, - version: parsedArgs.versionConstraint!, - ), - RemoteSourceKind.path => - // Re-resolve dependencies for path activate, behave like it would work - // for users of the package if the activate via hosted or git. - PathDependencySourceSyntax( - path$: Directory(parsedArgs.source).absolute.path, - ), + final descriptor = switch (parsedArgs) { + DescriptorInstallCommandParsedArguments _ => parsedArgs.descriptor, + NonDescriptorInstallCommandParsedArguments _ => + switch (parsedArgs.sourceKind) { + RemoteSourceKind.git => { + 'git': { + 'url': parsedArgs.source, + if (parsedArgs.gitPath != null) 'path': parsedArgs.gitPath!, + if (parsedArgs.gitRef != null) 'ref': parsedArgs.gitRef!, + }, + }, + RemoteSourceKind.hosted => { + 'hosted': ?parsedArgs.hostedUrl, + 'version': parsedArgs.versionConstraint!, + }, + RemoteSourceKind.path => { + 'path': Directory(parsedArgs.source).absolute.path, + }, }, - }, + }; + + final helperPackagePubspec = { + 'name': _helperPackageName, + 'environment': {'sdk': '^${Platform.version.split(' ').first}'}, + 'dependencies': {packageName: descriptor}, + }; + tempPubspec.writeAsStringSync( + JsonEncoder.withIndent(' ').convert(helperPackagePubspec), ); - helperPackagePubspec.writeSync(tempPubspec); } static const _helperPackageName = 'dart_install_helper_package'; @@ -317,42 +360,27 @@ You can specify three different values for the argument: } static AppBundleDirectory selectAppBundleDirectory( - InstallCommandParsedArguments parsedArgs, String packageName, Directory helperPackageDir, File helperPackageLockFile, ) { - final AppBundleDirectory outputDir; - switch (parsedArgs.sourceKind) { - case RemoteSourceKind.git: - final resolvedGitRef = - parsedArgs.gitRef ?? - GitPackageDescriptionSyntax.fromJson( - PubspecLockFile.loadSync( - helperPackageLockFile, - ).packages![packageName]!.description.json, - ).resolvedRef; - outputDir = DartInstallDirectory().gitAppBundle( - packageName, - resolvedGitRef, - ); - case RemoteSourceKind.hosted: - final packageGraphJson = PackageGraphFile.loadSync( - File.fromUri( - helperPackageDir.uri.resolve('.dart_tool/package_graph.json'), - ), - ); - final resolvedVersion = packageGraphJson.packages - .firstWhere((e) => e.name == packageName) - .version; - outputDir = DartInstallDirectory().hostedAppBundle( - packageName, - resolvedVersion, - ); - case RemoteSourceKind.path: - outputDir = DartInstallDirectory().localAppBundle(packageName); + final lockFile = PubspecLockFile.loadSync(helperPackageLockFile); + final resolvedPackage = lockFile.packages![packageName]!; + final source = resolvedPackage.source; + + if (source == PackageSourceSyntax.git) { + final resolvedGitRef = GitPackageDescriptionSyntax.fromJson( + resolvedPackage.description.json, + ).resolvedRef; + return DartInstallDirectory().gitAppBundle(packageName, resolvedGitRef); + } else if (source == PackageSourceSyntax.path$) { + return DartInstallDirectory().localAppBundle(packageName); + } else { + return DartInstallDirectory().hostedAppBundle( + packageName, + resolvedPackage.version, + ); } - return outputDir; } static Future createAppBundleDirectory( @@ -572,7 +600,6 @@ You can specify three different values for the argument: _uniinstallAllPackageVersions(packageName); AppBundleDirectory appBundleDirectory = selectAppBundleDirectory( - parsedArgs, packageName, helperPackageDirectory, helperPackageLockFile, @@ -630,7 +657,25 @@ You can specify three different values for the argument: } } -final class InstallCommandParsedArguments { +sealed class InstallCommandParsedArguments { + final bool overwrite; + InstallCommandParsedArguments({required this.overwrite}); +} + +class DescriptorInstallCommandParsedArguments + extends InstallCommandParsedArguments { + final String packageName; + final Object? descriptor; + + DescriptorInstallCommandParsedArguments({ + required this.packageName, + required this.descriptor, + required super.overwrite, + }); +} + +class NonDescriptorInstallCommandParsedArguments + extends InstallCommandParsedArguments { /// Package name, git url, or file path, depending on [sourceKind]. final String source; final RemoteSourceKind sourceKind; @@ -638,22 +683,21 @@ final class InstallCommandParsedArguments { final String? gitPath; final String? gitRef; final String? hostedUrl; - final bool overwrite; - InstallCommandParsedArguments({ + NonDescriptorInstallCommandParsedArguments({ required this.source, required this.sourceKind, required this.versionConstraint, required this.gitPath, required this.gitRef, required this.hostedUrl, - required this.overwrite, + required super.overwrite, }); } enum RemoteSourceKind { git, hosted, path } -RemoteSourceKind _soureKindFromArgument(String argument) { +RemoteSourceKind _sourceKindFromArgument(String argument) { if (_packageNameRegExp.hasMatch(argument)) { return RemoteSourceKind.hosted; } diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart index 791789bc98e..dca2973bb82 100644 --- a/pkg/dartdev/lib/src/commands/run.dart +++ b/pkg/dartdev/lib/src/commands/run.dart @@ -17,6 +17,7 @@ import 'package:frontend_server/resident_frontend_server_utils.dart' show invokeReplaceCachedDill; import 'package:path/path.dart'; import 'package:pub/pub.dart'; +import 'package:yaml/yaml.dart'; import '../core.dart'; import '../experiments.dart'; @@ -56,17 +57,42 @@ class RunCommand extends DartdevCommand { bool verbose = false, this.nativeAssetsExperimentEnabled = false, this.dataAssetsExperimentEnabled = false, - }) : super(cmdName, '''Run a Dart program from a file or a local package. + }) : super(cmdName, ''' +Run a Dart program from a file or a local or remote package. -Usage: dart [vm-options] run [arguments] | [args] +Usage: + +Running a local script or package executable: + dart run [vm-options] |[:] args +Running a remote package executable: + dart run [:]> [args] A path to a Dart script (e.g., `bin/main.dart`). - An executable from a local package dependency, in the format [:]. - For example, `test:test` runs the `test` executable from the `test` package. - If the executable is not specified, the package name is used.''', verbose) { + The name of a package in the local package resolution. + + + The name of an executable in the package to execute. + + For example, `dart run test:test` runs the `test` executable from the `test` package. + If the executable is not specified, the package name is used. + + + A YAML formatted string that describes how to locate the + remote package, the same you could use in a pubspec. + + For example, to run the latest stable `pubviz` package from pub.dev: + dart run pubviz@ + To specify a version constraint: + dart run pubviz@^4.0.0 + To specify a custom package host: + dart run 'pubviz@{hosted: https://my_repository.com, version: ^1.0.0}' + To run from a git package: + dart run 'pubviz@{git: https://github.com/kevmoo/pubviz}' + +See https://dart.dev/go/pub-descriptors for more details.''', verbose) { argParser ..addFlag( residentOption, @@ -327,58 +353,12 @@ Usage: dart [vm-options] run [arguments] | [args] 'extension development environment.', ) ..addFlag('debug-dds', hide: true) - ..addExperimentalFlags(verbose: verbose) - ..addFlag( - 'enable-experiment-remote-run', - negatable: false, - hide: !verbose, - help: ''' -Enables running executables from remote packages. - - When running a remote executable, all other command-line flags are disabled, - except for the options for remote executables. `dart run ` - uses `dart install` under the hood and compiles the app into a standalone - executable, preventing passing VM options. - - (Syntax is expected to change in the future.) - - From a hosted package server: - /[@][:] - - Downloads the package from a hosted package server and runs the specified - executable. - If a version is provided, the specified version is downloaded. - If an executable is not specified, the package name is used. - For example, `https://pub.dev/dcli@1.0.0:dcli_complete` runs the - `dcli_complete` executable from version 1.0.0 of the `dcli` package. - - From a git repository: - [:] - - Clones the git repository and runs the specified executable from it. - If an executable is not specified, the package name from the cloned - repository's pubspec.yaml is used. - The git url can be any valid git url.''', - ) - ..addOption( - hide: !verbose, - gitPathOption, - help: - 'Path of git package in repository. ' - 'Only applies when using a git url for .', - ) - ..addOption( - hide: !verbose, - gitRefOption, - help: - 'Git branch or commit to be retrieved. ' - 'Only applies when using a git url for .', - ); + ..addExperimentalFlags(verbose: verbose); } @override String get invocation => - '${super.invocation} [ [args]]'; + 'dart run [vm-options] ||@ '; @override CommandCategory get commandCategory => CommandCategory.project; @@ -468,8 +448,8 @@ Enables running executables from remote packages. runArgs = args.rest.skip(1).toList(); } - if (args.flag('enable-experiment-remote-run') && - _isRemoteRun(mainCommand)) { + final atIndex = mainCommand.indexOf('@'); + if (atIndex != -1) { return _runRemote(args, mainCommand, runArgs); } return _runLocal(args, mainCommand, runArgs); @@ -498,12 +478,6 @@ Enables running executables from remote packages. ); return errorExitCode; } - if (args.wasParsed(gitPathOption) || args.wasParsed(gitRefOption)) { - usageException( - 'Options `--$gitPathOption` and `--$gitRefOption` ' - 'can only be used with a remote executable.', - ); - } String? nativeAssets; final packageConfigUri = await DartNativeAssetsBuilder.ensurePackageConfig( @@ -639,140 +613,6 @@ Enables running executables from remote packages. return 0; } - static RemoteSourceKind? _remoteSourceKindFromArgument(String argument) { - if (argument.startsWith('git@')) { - return RemoteSourceKind.git; - } - final potentialUri = argument - .split(_colonButNoSlashes) - .first - .split('@') - .first; - final endsWithDotGitRegex = RegExp(r'\.git[/\\]?$'); - if (endsWithDotGitRegex.hasMatch(potentialUri)) { - return RemoteSourceKind.git; - } - final parsedUri = Uri.tryParse(potentialUri); - if (parsedUri != null) { - switch (parsedUri.scheme.toLowerCase()) { - case 'git': - return RemoteSourceKind.git; - case 'http': - case 'https': - return RemoteSourceKind.hosted; - } - } - final parsedGitSshUrl = GitSshUrl.tryParse(potentialUri); - if (parsedGitSshUrl != null) { - return RemoteSourceKind.git; - } - - // Local execution. - return null; - } - - static bool _isRemoteRun(String mainCommand) { - return _remoteSourceKindFromArgument(mainCommand) != null; - } - - /// Parse the arguments for remote run. - /// - /// Constructs a [InstallCommandParsedArguments] to be able to reuse the - /// [InstallCommand] implementation. - InstallCommandParsedArguments _parseRemoteArguments(String mainCommand) { - final argResults = this.argResults!; - - final sourceKind = _remoteSourceKindFromArgument(mainCommand)!; - - final gitPath = argResults.option(gitPathOption); - var gitRef = argResults.option(gitRefOption); - if (sourceKind != RemoteSourceKind.git && - (gitPath != null || gitRef != null)) { - usageException( - 'Options `--$gitPathOption` and `--$gitRefOption` ' - 'can only be used with a git source.', - ); - } - - for (final option in argResults.options) { - if (argResults.wasParsed(option) && - option != gitPathOption && - option != gitRefOption && - option != verbosityOption && - option != 'enable-experiment-remote-run') { - usageException( - 'Option $option cannot be used in remote runs. ' - '`dart run ` uses `dart install` under the hood ' - 'and compiles the app into a standalone executable.', - ); - } - } - - String? hostedUrl; - String? versionConstraint; - final String source; - switch (sourceKind) { - case RemoteSourceKind.git: - if (mainCommand.startsWith('git@') && mainCommand.contains('.git')) { - // Valid values might contain a colon for the command or not: - // - git@github.com:org/repo.git - // - git@github.com:org/repo.git:executable - // Drop everything after the 2nd colon for the git repository. - source = mainCommand.split(':').sublist(0, 2).join(':'); - } else { - source = mainCommand.split(_colonButNoSlashes).first; - } - case RemoteSourceKind.hosted: - final parsedUri = Uri.parse( - mainCommand.split('@').first.split(_colonButNoSlashes).first, - ); - hostedUrl = '${parsedUri.scheme}://${parsedUri.host}'; - source = parsedUri.path.replaceFirst('/', ''); - versionConstraint = - mainCommand - .split('@') - .lastButNotFirstOrNull - ?.split(_colonButNoSlashes) - .first ?? - 'any'; - if (versionConstraint.isEmpty) { - versionConstraint = 'any'; - } - case RemoteSourceKind.path: - throw StateError('Unreachable'); - } - - return InstallCommandParsedArguments( - source: source, - sourceKind: sourceKind, - versionConstraint: versionConstraint, - gitPath: gitPath, - gitRef: gitRef, - hostedUrl: hostedUrl, - overwrite: false, - ); - } - - Future _findPackageName( - InstallCommandParsedArguments parsedArgs, - ) async { - switch (parsedArgs.sourceKind) { - case RemoteSourceKind.git: - return await getPackageNameFromGitRepo( - parsedArgs.source, - ref: parsedArgs.gitRef, - path: parsedArgs.gitPath, - relativeTo: Directory.current.path, - tagPattern: null, - ); - case RemoteSourceKind.hosted: - return parsedArgs.source; - - case RemoteSourceKind.path: - throw StateError('Unreachable'); - } - } - /// Installs (if needed) and runs the remote executable. /// /// Installs the app bundle at the same location as `dart install` but does @@ -782,9 +622,38 @@ Enables running executables from remote packages. String mainCommand, List runArgs, ) async { - final parsedArgs = _parseRemoteArguments(mainCommand); - final packageName = await _findPackageName(parsedArgs); + for (final option in args.options) { + if (args.wasParsed(option) && option != verbosityOption) { + usageException( + 'Option --$option cannot be used in remote runs. ' + '`dart run ` uses `dart install` under the hood ' + 'and compiles the app into a standalone executable.', + ); + } + } + final atIndex = mainCommand.indexOf('@'); + assert(atIndex != -1); + final command = mainCommand.substring(0, atIndex); + final descriptorString = mainCommand.substring(atIndex + 1); + final Object? descriptor; + try { + descriptor = loadYaml(descriptorString); + } on FormatException catch (e) { + usageException( + 'Failed to parse remote executable descriptor "$descriptorString": $e', + ); + } + final colonIndex = command.indexOf(':'); + final String packageName; + final String executable; + if (colonIndex == -1) { + packageName = command; + executable = command; + } else { + packageName = command.substring(0, colonIndex); + executable = command.substring(colonIndex + 1); + } return await InstallCommand.inTempDir((tempDirectory) async { try { // Create a helper package for running a pub-resolve and pulling in the @@ -796,7 +665,11 @@ Enables running executables from remote packages. InstallCommand.createHelperPackagePubspec( helperPackageDir: helperPackageDirectory, packageName: packageName, - parsedArgs: parsedArgs, + parsedArgs: DescriptorInstallCommandParsedArguments( + packageName: packageName, + descriptor: descriptor, + overwrite: false, + ), ); await InstallCommand.resolveHelperPackage(helperPackageDirectory); final helperPackageLockFile = File.fromUri( @@ -804,7 +677,6 @@ Enables running executables from remote packages. ); final appBundleDirectory = InstallCommand.selectAppBundleDirectory( - parsedArgs, packageName, helperPackageDirectory, helperPackageLockFile, @@ -858,14 +730,6 @@ Enables running executables from remote packages. ); } - final mainCommandRemainder = mainCommand.substring( - parsedArgs.source.length, - ); - final executable = - mainCommandRemainder - .split(_colonButNoSlashes) - .lastButNotFirstOrNull ?? - packageName; final executableUri = appBundleDirectory.directory.uri.resolve( 'bundle/bin/$executable', ); @@ -888,17 +752,6 @@ Enables running executables from remote packages. } } -extension on List { - /// Return the last element, but only if there are at least two elements. - T? get lastButNotFirstOrNull { - if (length < 2) return null; - return last; - } -} - -/// Does not match the :// in an url scheme or the :\ in a Windows path. -final _colonButNoSlashes = RegExp(r':(?!(//|\\))'); - /// Keep in sync with [getExecutableForCommand]. /// /// Returns `null` if root package should be used. diff --git a/pkg/dartdev/test/commands/help_test.dart b/pkg/dartdev/test/commands/help_test.dart index b1c6fac7ea2..c858a1c4786 100644 --- a/pkg/dartdev/test/commands/help_test.dart +++ b/pkg/dartdev/test/commands/help_test.dart @@ -90,7 +90,7 @@ Project compile Compile Dart to various formats. create Create a new Dart project. pub Work with packages. - run Run a Dart program from a file or a local package. + run Run a Dart program from a file or a local or remote package. test Run tests for a project. Source code diff --git a/pkg/dartdev/test/commands/run_test.dart b/pkg/dartdev/test/commands/run_test.dart index 8335becd1f5..01b21942cf5 100644 --- a/pkg/dartdev/test/commands/run_test.dart +++ b/pkg/dartdev/test/commands/run_test.dart @@ -107,13 +107,13 @@ void run() { expect( result.stdout, - contains('Run a Dart program from a file or a local package.'), + contains('Run a Dart program from a file or a local or remote package.'), ); expect(result.stdout, contains('Debugging options:')); expect( result.stdout, contains( - 'Usage: dart [vm-options] run [arguments] | [args]', + 'Usage: dart run [vm-options] ||@ ', ), ); expect(result.stderr, isEmpty); @@ -126,13 +126,13 @@ void run() { expect( result.stdout, - contains('Run a Dart program from a file or a local package.'), + contains('Run a Dart program from a file or a local or remote package.'), ); expect(result.stdout, contains('Debugging options:')); expect( result.stdout, contains( - 'Usage: dart [vm-options] run [arguments] | [args]', + 'dart run [vm-options] ||@ ', ), ); expect(result.stderr, isEmpty); diff --git a/pkg/dartdev/test/data/dart_app/bin/other_app.dart b/pkg/dartdev/test/data/dart_app/bin/other_app.dart new file mode 100644 index 00000000000..82c660e91af --- /dev/null +++ b/pkg/dartdev/test/data/dart_app/bin/other_app.dart @@ -0,0 +1,3 @@ +void main(List args) { + print('Hello from other app ${args.join(' ')}'); +} diff --git a/pkg/dartdev/test/data/dart_app/pubspec.yaml b/pkg/dartdev/test/data/dart_app/pubspec.yaml index 18e4d0fd36b..2a1fc6205a7 100644 --- a/pkg/dartdev/test/data/dart_app/pubspec.yaml +++ b/pkg/dartdev/test/data/dart_app/pubspec.yaml @@ -5,3 +5,4 @@ environment: executables: dart_app: + other_app: diff --git a/pkg/dartdev/test/native_assets/install_test.dart b/pkg/dartdev/test/native_assets/install_test.dart index 709784cd77c..c4a320e32bf 100644 --- a/pkg/dartdev/test/native_assets/install_test.dart +++ b/pkg/dartdev/test/native_assets/install_test.dart @@ -90,21 +90,26 @@ executables. If the same package has been previously installed, it will be overwritten. -You can specify three different values for the argument: -1. A package name. This will install the package from pub.dev. (hosted) - The [version-constraint] argument can only be passed to 'hosted'. -2. A git url. This will install the package from a git repository. (git) -3. A path on your machine. This will install the package from that path. (path) +You can specify a package to install from pub.dev, a git repository, or a +local path using the `[@]` syntax. -Usage: dart install [version-constraint] --h, --help Print this usage information. - --git-path Path of git package in repository. Only applies when using a git url for . - --git-ref Git branch or commit to be retrieved. Only applies when using a git url for . - --overwrite Overwrite executables from other packages with the same name. --u, --hosted-url A custom pub server URL for the package. Only applies when using a package name for . +The `@` can be a version constraint (for hosted packages) or a +pub descriptor (consistent with pubspec.yaml). -Run "dart help" to see global options. -''', +Examples: + dart install + dart install @^3.0.0 + dart install '@{hosted: https://pub.dev, version: ^3.0.0}' + dart install '@{git: {url: https://github.com//, path: }}' + dart install '@{path: /path/to/}' + +See https://dart.dev/go/pub-descriptors for more details. + +Usage: dart install [@] +-h, --help Print this usage information. + --overwrite Overwrite executables from other packages with the same name. + +Run "dart help" to see global options.''', ), ( 'installed', @@ -152,6 +157,9 @@ Run "dart help" to see global options. final argumentss = [ (null, [_packageForTest]), (null, [_packageForTest, _packageVersion]), + (null, ['$_packageForTest@$_packageVersion']), + (null, ['$_packageForTest@{"version": "$_packageVersion"}']), + (null, ['$_packageForTest@{version: $_packageVersion}']), ( null, [_packageForTest, _packageVersion, '--hosted-url', 'https://pub.dev/'], @@ -225,6 +233,7 @@ Run "dart help" to see global options. final argumentssGit = [ ['git'], ['git', '--git-path', '--git-ref'], + ['git-descriptor'], ]; for (final testArguments in argumentssGit) { @@ -234,7 +243,11 @@ Run "dart help" to see global options. await inTempDir((tempUri) async { final gitUri = tempUri.resolve('app.git/'); await Directory.fromUri(gitUri.resolve('bin/')).create(recursive: true); - for (final file in ['pubspec.yaml', 'bin/dart_app.dart']) { + for (final file in [ + 'pubspec.yaml', + 'bin/dart_app.dart', + 'bin/other_app.dart', + ]) { await File.fromUri( _package2Dir.uri.resolve(file), ).copy(gitUri.resolve(file).toFilePath()); @@ -261,11 +274,21 @@ Run "dart help" to see global options. as String) .trim(); final gitPath = './'; - final arguments = [ - gitUri.toFilePath(), - if (testArguments.contains('--git-path')) ...['--git-path', gitPath], - if (testArguments.contains('--git-ref')) ...['--git-ref', gitRef], - ]; + final List arguments; + if (testArguments.contains('git-descriptor')) { + arguments = [ + 'dart_app@{git: {url: ${gitUri.toFilePath()}, ref: $gitRef}}', + ]; + } else { + arguments = [ + gitUri.toFilePath(), + if (testArguments.contains('--git-path')) ...[ + '--git-path', + gitPath, + ], + if (testArguments.contains('--git-ref')) ...['--git-ref', gitRef], + ]; + } final dartDataHome = tempUri.resolve('dart_home/'); await Directory.fromUri(dartDataHome).create(); diff --git a/pkg/dartdev/test/native_assets/run_remote_test.dart b/pkg/dartdev/test/native_assets/run_remote_test.dart index 0fa05f3f8f9..8d05de9549a 100644 --- a/pkg/dartdev/test/native_assets/run_remote_test.dart +++ b/pkg/dartdev/test/native_assets/run_remote_test.dart @@ -11,10 +11,6 @@ import 'package:test/test.dart'; import '../utils.dart'; import 'helpers.dart'; -const _packageForTest = 'vm_snapshot_analysis'; -const _packageVersion = '0.7.5'; -const _cliToolForTest = 'snapshot_analysis'; - final _sdkUri = resolveDartDevUri('.').resolve('../../'); final _package2RelativePath = Uri.directory('pkg/dartdev/test/data/dart_app/'); @@ -23,8 +19,6 @@ final _package2Dir = Directory.fromUri( _sdkUri.resolveUri(_package2RelativePath), ); -const _gitPackageForTest = 'dart_app'; - final _pathEnvVarSeparator = Platform.isWindows ? ';' : ':'; const String _dartDirectoryEnvKey = 'DART_DATA_HOME'; @@ -48,52 +42,59 @@ void main() async { final result = await _runDartdev( fromDartdevSource, 'run', - ['--help', '-v'], + ['--help'], null, {}, ); - print(result.stdout); + printOnFailure('stdout:\n${result.stdout}'); expect( result.stdout, - contains( - ''' - --enable-experiment-remote-run Enables running executables from remote packages. - - When running a remote executable, all other command-line flags are disabled, - except for the options for remote executables. `dart run ` - uses `dart install` under the hood and compiles the app into a standalone - executable, preventing passing VM options. - - (Syntax is expected to change in the future.) - - From a hosted package server: - /[@][:] - - Downloads the package from a hosted package server and runs the specified - executable. - If a version is provided, the specified version is downloaded. - If an executable is not specified, the package name is used. - For example, `https://pub.dev/dcli@1.0.0:dcli_complete` runs the - `dcli_complete` executable from version 1.0.0 of the `dcli` package. - - From a git repository: - [:] - - Clones the git repository and runs the specified executable from it. - If an executable is not specified, the package name from the cloned - repository's pubspec.yaml is used. - The git url can be any valid git url. - --git-path Path of git package in repository. Only applies when using a git url for . - --git-ref Git branch or commit to be retrieved. Only applies when using a git url for .''', - ), + contains(''' +Run a Dart program from a file or a local or remote package. + +Usage: + +Running a local script or package executable: + dart run [vm-options] |[:] args +Running a remote package executable: + dart run [:]> [args] + + + A path to a Dart script (e.g., `bin/main.dart`). + + + The name of a package in the local package resolution. + + + The name of an executable in the package to execute. + + For example, `dart run test:test` runs the `test` executable from the `test` package. + If the executable is not specified, the package name is used. + + + A YAML formatted string that describes how to locate the + remote package, the same you could use in a pubspec. + + For example, to run the latest stable `pubviz` package from pub.dev: + dart run pubviz@ + To specify a version constraint: + dart run pubviz@^4.0.0 + To specify a custom package host: + dart run 'pubviz@{hosted: https://my_repository.com, version: ^1.0.0}' + To run from a git package: + dart run 'pubviz@{git: https://github.com/kevmoo/pubviz}' + +See https://dart.dev/go/pub-descriptors for more details.'''), ); }); - for (final version in ['', '@$_packageVersion', '@']) { - final testName = version == '' - ? 'no version' - : (version == '@' ? 'empty version' : 'with version'); - test('dart run hosted package $testName', timeout: longTimeout, () async { + for (final argument in [ + 'vm_snapshot_analysis:snapshot_analysis@0.7.5', + 'vm_snapshot_analysis:snapshot_analysis@^0.7.5', + 'vm_snapshot_analysis:snapshot_analysis@', // Resolves to latest stable version. + 'vm_snapshot_analysis:snapshot_analysis@{hosted: https://pub.dev}', + ]) { + test('dart run $argument', timeout: longTimeout, () async { await inTempDir((tempUri) async { final dartDataHome = tempUri.resolve('dart_home/'); await Directory.fromUri(dartDataHome).create(); @@ -109,8 +110,7 @@ void main() async { fromDartdevSource, 'run', [ - '--enable-experiment-remote-run', - 'https://pub.dev/$_packageForTest$version:$_cliToolForTest', + argument, // Make sure to pass arguments that influence stdout. 'compare', '--help', @@ -129,23 +129,18 @@ void main() async { }); } - final argumentssGit = [ - ['git'], - ['git', '--git-path', '--git-ref'], - ]; - - for (final testArguments in argumentssGit) { - var testName = testArguments.join(' '); - - test('dart run from $testName', timeout: longTimeout, () async { - await inTempDir((tempUri) async { - final (gitUri, gitRef) = await _setupSimpleGitRepo(tempUri); - final gitPath = './'; + test('dart run remote from git', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + final (gitUri, gitRef) = await _setupSimpleGitRepo(tempUri); + for (final (argument, expectedResult) in [ + ('dart_app@{git: ${gitUri.toString()}}', 'Hello Alice and Bob'), + ( + 'dart_app:other_app@{git: ${gitUri.toString()}}', + 'Hello from other app Alice and Bob', + ), + ]) { final arguments = [ - '--enable-experiment-remote-run', - if (testArguments.contains('--git-path')) ...['--git-path', gitPath], - if (testArguments.contains('--git-ref')) ...['--git-ref', gitRef], - '${gitUri.toFilePath()}:$_gitPackageForTest', + argument, // Make sure to pass arguments that influence stdout. 'Alice', 'and', @@ -170,48 +165,24 @@ void main() async { environment, ); - expect( - runResult.stdout, - stringContainsInOrder(['Hello Alice and Bob']), - ); + expect(runResult.stdout, contains(expectedResult)); expect(runResult.exitCode, 0); - }); + } }); - } + }); final errorArgumentss = [ ( - [ - '--enable-experiment-remote-run', - 'https://pub.dev/this_package_does_not_exist_12345', - ], + ['this_package_does_not_exist_12345@'], 'could not find package this_package_does_not_exist_12345 at', errorExitCode, ), ( - [ - '--enable-experiment-remote-run', - '--git-path', - 'foo/', - 'https://pub.dev/vm_snapshot_analysis', - ], - 'git-path', - usageExitCode, - ), - ( - [ - '--enable-experiment-remote-run', - '--enable-asserts', - 'https://pub.dev/vm_snapshot_analysis', - ], - 'enable-asserts', - usageExitCode, - ), - ( - ['--enable-experiment-remote-run', '--git-path', 'foo/'], - 'git-path', + ['--enable-asserts', 'vm_snapshot_analysis@'], + '--enable-asserts cannot be used in remote runs', usageExitCode, ), + (['my_package@{,bad,descriptor,}'], '{,bad,descriptor,}', usageExitCode), ]; for (final (errorArguments, error, exitCode) in errorArgumentss) { test('dart run ${errorArguments.join(' ')}', timeout: longTimeout, () async { @@ -284,10 +255,7 @@ void main(List args) async { final runResult = await _runDartdev( fromDartdevSource, 'run', - [ - '--enable-experiment-remote-run', - '${gitUri.toFilePath()}:$packageName', - ], + ['test_app_with_failing_hook@{git: ${gitUri.toFilePath()}}'], null, environment, expectedExitCode: errorExitCode, @@ -309,13 +277,7 @@ void main(List args) async { await inTempDir((tempUri) async { final (gitUri, gitRef) = await _setupSimpleGitRepo(tempUri); - final arguments = [ - '--enable-experiment-remote-run', - '--git-ref', - gitRef, - '${gitUri.toFilePath()}:$_gitPackageForTest', - 'World', - ]; + final arguments = ['dart_app@{git: ${gitUri.toFilePath()}}', 'World']; // 2. Setup environment final dartDataHome = tempUri.resolve('dart_home/'); @@ -372,11 +334,8 @@ void main(List args) async { ); final arguments = [ - '--enable-experiment-remote-run', if (verbosityError) '--verbosity=error', - '--git-ref', - gitRef, - '${gitUri.toFilePath()}:$packageName', + 'test_app_with_hook@{git: {url: ${gitUri.toFilePath()}, ref: $gitRef}}', 'ignored', 'arguments', ]; @@ -391,8 +350,6 @@ void main(List args) async { '${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}', }; - print(environment); - print('dart run ${arguments.join(' ')}'); final runResult = await _runDartdev( fromDartdevSource, 'run', @@ -407,7 +364,7 @@ void main(List args) async { expect(runResult.stdout, isNot(contains('Running build hooks'))); expect(runResult.stdout, isNot(contains('Running link hooks'))); expect(runResult.stdout, isNot(contains('Generated: '))); - // Should have no other output then the program. + // Should have no other output than the program. expect(runResult.stdout.trim(), equals('Hello World')); } else { expect(runResult.stdout, contains('Running build hooks')); @@ -513,6 +470,9 @@ Future<(Uri gitUri, String gitRef)> _setupSimpleGitRepo(Uri tempUri) async { 'bin/dart_app.dart': await File.fromUri( _package2Dir.uri.resolve('bin/dart_app.dart'), ).readAsString(), + 'bin/other_app.dart': await File.fromUri( + _package2Dir.uri.resolve('bin/other_app.dart'), + ).readAsString(), }, ); }