[dartdev] dart install
This CL adds three new commands to `dart`: ``` Global install Install a Dart CLI tool for global use. installed List globally installed Dart CLI tools. uninstall Remove a globally installed Dart CLI tool. ``` These commands are intended to replace `dart pub global` subcommands while adding support for `hook/build.dart` and building packages in AOT instead of running them with JIT. Internal design doc: http://go/dart-install-cli. Implementation details: * The source of truth is the state of the file system. These commands write and read directories, files, and symlinks. * App bundles and symlinks are placed in `DART_DATA_HOME` as per http://go/dart-data-home. * On Unix systems we use symlinks and on Windows batchfiles to place executables in the bin directory that point to an application bundle. (These OS differences have been encapsulated in a single class.) * On Windows, when an application is running, trying to re-install it will fail. Test coverage: * Installing from hosted, git, and local paths. * Installing a package with hooks. * Installing a package with hooks and user-defines. * Surfacing build hook failures during install. * Installing packages with conflicting executables names. This tests `--overwrite` flag behavior. * Installing a new or the same version, this should simply succeed. * A warning is shown if the bin directory is not on the `PATH`. * Running an installed app reports the correct exit code on exit. * Listing all installed versions, including the versions not on the`PATH`. * Uninstalling, which uninstalls all versions. * Re-installing while it is running. * Uninstalling while it is running. Out of scope for initial version: * Saving the SDK version (to display in `dart installed`). * Short-circuiting if re-installing an exactly installed version. Bug: https://github.com/dart-lang/sdk/issues/60889 Change-Id: I8f3a60d26e013957ce6fd7f52e564bcaaff30509 Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-linux-release-try,pkg-mac-release-arm64-try,pkg-mac-release-try,pkg-win-release-arm64-try,pkg-win-release-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441581 Reviewed-by: Sigurd Meldgaard <sigurdm@google.com> Commit-Queue: Daco Harkes <dacoharkes@google.com> Reviewed-by: Michael Goderbauer <goderbauer@google.com>
This commit is contained in:
committed by
Commit Queue
parent
c92417ba82
commit
94894b1796
@@ -27,10 +27,13 @@ import 'src/commands/devtools.dart';
|
||||
import 'src/commands/doc.dart';
|
||||
import 'src/commands/fix.dart';
|
||||
import 'src/commands/info.dart';
|
||||
import 'src/commands/install.dart';
|
||||
import 'src/commands/installed.dart';
|
||||
import 'src/commands/language_server.dart';
|
||||
import 'src/commands/run.dart';
|
||||
import 'src/commands/test.dart';
|
||||
import 'src/commands/tooling_daemon.dart';
|
||||
import 'src/commands/uninstall.dart';
|
||||
import 'src/core.dart';
|
||||
import 'src/experiments.dart';
|
||||
import 'src/unified_analytics.dart';
|
||||
@@ -135,6 +138,11 @@ class DartdevRunner extends CommandRunner<int> {
|
||||
nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
|
||||
));
|
||||
addCommand(ToolingDaemonCommand(verbose: verbose));
|
||||
if (nativeAssetsExperimentEnabled) {
|
||||
addCommand(InstallCommand(verbose: verbose));
|
||||
addCommand(InstalledCommand(verbose: verbose));
|
||||
addCommand(UninstallCommand(verbose: verbose));
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:dartdev/src/native_assets_macos.dart';
|
||||
import 'package:dartdev/src/sdk.dart';
|
||||
import 'package:front_end/src/api_prototype/compiler_options.dart'
|
||||
show Verbosity;
|
||||
import 'package:hooks_runner/hooks_runner.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../core.dart';
|
||||
@@ -124,15 +125,21 @@ then that is used instead.''',
|
||||
// AOT compilation isn't supported on ia32. Currently, generating an
|
||||
// executable only supports AOT runtimes, so these commands are disabled.
|
||||
if (Platform.version.contains('ia32')) {
|
||||
stderr.write("'dart build' is not supported on x86 architectures");
|
||||
stderr.write("'dart build' is not supported on x86 architectures.");
|
||||
return 64;
|
||||
}
|
||||
final args = argResults!;
|
||||
|
||||
var target = args.option('target');
|
||||
if (target == null) {
|
||||
stderr.write(
|
||||
'There are multiple possible targets in the `bin/` directory, '
|
||||
"and the 'target' argument wasn't specified.",
|
||||
);
|
||||
return 255;
|
||||
}
|
||||
final sourceUri =
|
||||
File.fromUri(Uri.file(args.option('target')!).normalizePath())
|
||||
.absolute
|
||||
.uri;
|
||||
File.fromUri(Uri.file(target).normalizePath()).absolute.uri;
|
||||
if (!checkFile(sourceUri.toFilePath())) {
|
||||
return genericErrorExitCode;
|
||||
}
|
||||
@@ -146,38 +153,84 @@ then that is used instead.''',
|
||||
stderr.writeln('Requested output directory: ${outputUri.toFilePath()}');
|
||||
return 128;
|
||||
}
|
||||
final outputDir = Directory.fromUri(outputUri);
|
||||
if (await outputDir.exists()) {
|
||||
stdout.writeln('Deleting output directory: ${outputUri.toFilePath()}.');
|
||||
await outputDir.delete(recursive: true);
|
||||
}
|
||||
final bundleDirectory = Directory.fromUri(outputUri.resolve('bundle/'));
|
||||
final binDirectory = Directory.fromUri(bundleDirectory.uri.resolve('bin/'));
|
||||
await binDirectory.create(recursive: true);
|
||||
|
||||
final outputExeUri = binDirectory.uri.resolve(
|
||||
targetOS.executableFileName(
|
||||
path.basenameWithoutExtension(sourceUri.path),
|
||||
),
|
||||
);
|
||||
final verbosity = args.option('verbosity')!;
|
||||
final enabledExperiments = args.enabledExperiments;
|
||||
|
||||
stdout.writeln('''The `dart build cli` command is in preview at the moment.
|
||||
See documentation on https://dart.dev/interop/c-interop#native-assets.
|
||||
''');
|
||||
|
||||
stdout.writeln('Building native assets.');
|
||||
final packageConfigUri = await DartNativeAssetsBuilder.ensurePackageConfig(
|
||||
sourceUri,
|
||||
);
|
||||
final pubspecUri =
|
||||
await DartNativeAssetsBuilder.findWorkspacePubspec(packageConfigUri);
|
||||
final executableName = path.basenameWithoutExtension(sourceUri.path);
|
||||
|
||||
return await doBuild(
|
||||
executables: [(name: executableName, sourceEntryPoint: sourceUri)],
|
||||
enabledExperiments: enabledExperiments,
|
||||
outputUri: outputUri,
|
||||
packageConfigUri: packageConfigUri!,
|
||||
pubspecUri: pubspecUri,
|
||||
recordUseEnabled: recordUseEnabled,
|
||||
verbose: verbose,
|
||||
verbosity: verbosity,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<int> doBuild({
|
||||
required DartBuildExecutables executables,
|
||||
required Uri outputUri,
|
||||
required Uri packageConfigUri,
|
||||
required Uri? pubspecUri,
|
||||
required bool recordUseEnabled,
|
||||
required List<String> enabledExperiments,
|
||||
required bool verbose,
|
||||
required String verbosity,
|
||||
}) async {
|
||||
if (executables.length >= 2 && recordUseEnabled) {
|
||||
// Multiple entry points can lead to multiple different tree-shakings.
|
||||
// We either need to generate a new entry point that combines all entry
|
||||
// points and combine that into a single executable and have wrappers
|
||||
// around that executable. Or, we need to merge the recorded uses for the
|
||||
// various entrypoints. The former will lead to smaller bundle-size
|
||||
// overall.
|
||||
stderr.writeln(
|
||||
'Multiple executables together with record use is not yet supported.',
|
||||
);
|
||||
return 255;
|
||||
}
|
||||
final outputDir = Directory.fromUri(outputUri);
|
||||
if (await outputDir.exists()) {
|
||||
stdout.writeln('Deleting output directory: ${outputUri.toFilePath()}.');
|
||||
try {
|
||||
await outputDir.delete(recursive: true);
|
||||
} on PathAccessException {
|
||||
stderr.writeln(
|
||||
'Failed to delete: ${outputUri.toFilePath()}. '
|
||||
'The application might be in use.',
|
||||
);
|
||||
return 255;
|
||||
}
|
||||
}
|
||||
|
||||
// Place the bundle in a subdir so that we can potentially put debug symbols
|
||||
// next to it.
|
||||
final bundleDirectory = Directory.fromUri(outputUri.resolve('bundle/'));
|
||||
final binDirectory = Directory.fromUri(bundleDirectory.uri.resolve('bin/'));
|
||||
await binDirectory.create(recursive: true);
|
||||
|
||||
stdout.writeln('Building native assets.');
|
||||
|
||||
final packageConfig =
|
||||
await DartNativeAssetsBuilder.loadPackageConfig(packageConfigUri!);
|
||||
await DartNativeAssetsBuilder.loadPackageConfig(packageConfigUri);
|
||||
if (packageConfig == null) {
|
||||
return compileErrorExitCode;
|
||||
}
|
||||
final runPackageName = await DartNativeAssetsBuilder.findRootPackageName(
|
||||
sourceUri,
|
||||
executables.first.sourceEntryPoint,
|
||||
);
|
||||
final pubspecUri =
|
||||
pubspecUri ??=
|
||||
await DartNativeAssetsBuilder.findWorkspacePubspec(packageConfigUri);
|
||||
final builder = DartNativeAssetsBuilder(
|
||||
pubspecUri: pubspecUri,
|
||||
@@ -195,75 +248,89 @@ See documentation on https://dart.dev/interop/c-interop#native-assets.
|
||||
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
try {
|
||||
String? recordedUsagesPath;
|
||||
if (recordUseEnabled) {
|
||||
recordedUsagesPath = path.join(tempDir.path, 'recorded_usages.json');
|
||||
}
|
||||
final generator = KernelGenerator(
|
||||
genSnapshot: sdk.genSnapshot,
|
||||
targetDartAotRuntime: sdk.dartAotRuntime,
|
||||
kind: Kind.exe,
|
||||
sourceFile: sourceUri.toFilePath(),
|
||||
outputFile: outputExeUri.toFilePath(),
|
||||
verbose: verbose,
|
||||
verbosity: args.option('verbosity')!,
|
||||
defines: [],
|
||||
packages: packageConfigUri.toFilePath(),
|
||||
targetOS: targetOS,
|
||||
enableExperiment: args.enabledExperiments.join(','),
|
||||
tempDir: tempDir,
|
||||
);
|
||||
|
||||
final snapshotGenerator = await generator.generate(
|
||||
recordedUsagesFile: recordedUsagesPath,
|
||||
);
|
||||
|
||||
final linkResult = await builder.linkNativeAssetsAOT(
|
||||
recordedUsagesPath: recordedUsagesPath,
|
||||
buildResult: buildResult,
|
||||
);
|
||||
if (linkResult == null) {
|
||||
stderr.writeln('Native assets link failed.');
|
||||
return 255;
|
||||
}
|
||||
|
||||
final allAssets = [
|
||||
...buildResult.encodedAssets,
|
||||
...linkResult.encodedAssets
|
||||
];
|
||||
|
||||
final staticAssets = allAssets
|
||||
.where((e) => e.isCodeAsset)
|
||||
.map(CodeAsset.fromEncoded)
|
||||
.where((e) => e.linkMode == StaticLinking());
|
||||
if (staticAssets.isNotEmpty) {
|
||||
stderr.write(
|
||||
"""'dart build' does not yet support CodeAssets with static linking.
|
||||
Use linkMode as dynamic library instead.""");
|
||||
return 255;
|
||||
}
|
||||
|
||||
var first = true;
|
||||
Uri? nativeAssetsYamlUri;
|
||||
if (allAssets.isNotEmpty) {
|
||||
final kernelAssets = await bundleNativeAssets(
|
||||
allAssets,
|
||||
builder.target,
|
||||
binDirectory.uri,
|
||||
relocatable: true,
|
||||
verbose: true,
|
||||
LinkResult? linkResult;
|
||||
for (final e in executables) {
|
||||
String? recordedUsagesPath;
|
||||
if (recordUseEnabled) {
|
||||
recordedUsagesPath = path.join(tempDir.path, 'recorded_usages.json');
|
||||
}
|
||||
final outputExeUri = binDirectory.uri.resolve(
|
||||
targetOS.executableFileName(e.name),
|
||||
);
|
||||
final generator = KernelGenerator(
|
||||
genSnapshot: sdk.genSnapshot,
|
||||
targetDartAotRuntime: sdk.dartAotRuntime,
|
||||
kind: Kind.exe,
|
||||
sourceFile: e.sourceEntryPoint.toFilePath(),
|
||||
outputFile: outputExeUri.toFilePath(),
|
||||
verbose: verbose,
|
||||
verbosity: verbosity,
|
||||
defines: [],
|
||||
packages: packageConfigUri.toFilePath(),
|
||||
targetOS: targetOS,
|
||||
enableExperiment: enabledExperiments.join(','),
|
||||
tempDir: tempDir,
|
||||
);
|
||||
nativeAssetsYamlUri =
|
||||
await writeNativeAssetsYaml(kernelAssets, tempDir.uri);
|
||||
}
|
||||
|
||||
await snapshotGenerator.generate(
|
||||
nativeAssets: nativeAssetsYamlUri?.toFilePath(),
|
||||
);
|
||||
final snapshotGenerator = await generator.generate(
|
||||
recordedUsagesFile: recordedUsagesPath,
|
||||
);
|
||||
|
||||
if (targetOS == OS.macOS) {
|
||||
// The dylibs are opened with a relative path to the executable.
|
||||
// MacOS prevents opening dylibs that are not on the include path.
|
||||
await rewriteInstallPath(outputExeUri);
|
||||
if (first) {
|
||||
// Multiple executables are only supported with recorded uses
|
||||
// disabled, so don't re-invoke link hooks.
|
||||
linkResult = await builder.linkNativeAssetsAOT(
|
||||
recordedUsagesPath: recordedUsagesPath,
|
||||
buildResult: buildResult,
|
||||
);
|
||||
}
|
||||
if (linkResult == null) {
|
||||
stderr.writeln('Native assets link failed.');
|
||||
return 255;
|
||||
}
|
||||
|
||||
final allAssets = [
|
||||
...buildResult.encodedAssets,
|
||||
...linkResult.encodedAssets
|
||||
];
|
||||
|
||||
final staticAssets = allAssets
|
||||
.where((e) => e.isCodeAsset)
|
||||
.map(CodeAsset.fromEncoded)
|
||||
.where((e) => e.linkMode == StaticLinking());
|
||||
if (staticAssets.isNotEmpty) {
|
||||
stderr.write(
|
||||
"""'dart build' does not yet support CodeAssets with static linking.
|
||||
Use linkMode as dynamic library instead.""");
|
||||
return 255;
|
||||
}
|
||||
|
||||
if (allAssets.isNotEmpty && first) {
|
||||
// Without tree-shaking, the assets after linking must be identical
|
||||
// for all entry points.
|
||||
final kernelAssets = await bundleNativeAssets(
|
||||
allAssets,
|
||||
builder.target,
|
||||
binDirectory.uri,
|
||||
relocatable: true,
|
||||
verbose: true,
|
||||
);
|
||||
nativeAssetsYamlUri =
|
||||
await writeNativeAssetsYaml(kernelAssets, tempDir.uri);
|
||||
}
|
||||
|
||||
await snapshotGenerator.generate(
|
||||
nativeAssets: nativeAssetsYamlUri?.toFilePath(),
|
||||
);
|
||||
|
||||
if (targetOS == OS.macOS) {
|
||||
// The dylibs are opened with a relative path to the executable.
|
||||
// MacOS prevents opening dylibs that are not on the include path.
|
||||
await rewriteInstallPath(outputExeUri);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
} finally {
|
||||
await tempDir.delete(recursive: true);
|
||||
@@ -277,3 +344,13 @@ extension on String {
|
||||
String makeFolder() => endsWith('\\') || endsWith('/') ? this : '$this/';
|
||||
String removeDotDart() => replaceFirst(RegExp(r'\.dart$'), '');
|
||||
}
|
||||
|
||||
/// The executables to build in a `dart build cli` app bundle.
|
||||
///
|
||||
/// All entry points must be in the same package.
|
||||
///
|
||||
/// The names are typically taken from the `executables` section of the
|
||||
/// `pubspec.yaml` file.
|
||||
///
|
||||
/// Recorded usages and multiple executables are not supported yet.
|
||||
typedef DartBuildExecutables = List<({String name, Uri sourceEntryPoint})>;
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
// Copyright (c) 2025, 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 'dart:io';
|
||||
|
||||
import 'package:dartdev/src/commands/build.dart';
|
||||
import 'package:dartdev/src/install/file_system.dart';
|
||||
import 'package:dartdev/src/install/pub_formats.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub/pub.dart';
|
||||
import 'package:pub_formats/pub_formats.dart';
|
||||
|
||||
import '../core.dart';
|
||||
|
||||
class InstallCommand extends DartdevCommand {
|
||||
static const cmdName = 'install';
|
||||
static const cmdDescription =
|
||||
'''Install or upgrade a Dart CLI tool for global use.
|
||||
|
||||
Install all executables specified in a package's pubspec.yaml executables
|
||||
section (https://dart.dev/tools/pub/pubspec#executables) on the PATH. If the
|
||||
executables section doesn't exist, installs all `bin/*.dart` entry points as
|
||||
executables.
|
||||
|
||||
If the same package has been previously installed, it will be overwritten.
|
||||
|
||||
You can specify three different values for the <package> 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)''';
|
||||
static const int genericErrorExitCode = 255;
|
||||
|
||||
@override
|
||||
String get invocation {
|
||||
final superNoArguments = super.invocation.replaceAll(' [arguments]', '');
|
||||
return '$superNoArguments <package> [version-constraint]';
|
||||
}
|
||||
|
||||
@override
|
||||
CommandCategory get commandCategory => CommandCategory.global;
|
||||
|
||||
InstallCommand({bool verbose = false})
|
||||
: super(cmdName, cmdDescription, verbose) {
|
||||
argParser.addOption(
|
||||
'git-path',
|
||||
help: 'Path of git package in repository. '
|
||||
'Only applies when using a git url for <package>.',
|
||||
);
|
||||
|
||||
argParser.addOption(
|
||||
'git-ref',
|
||||
help: 'Git branch or commit to be retrieved. '
|
||||
'Only applies when using a git url for <package>.',
|
||||
);
|
||||
|
||||
argParser.addFlag(
|
||||
'overwrite',
|
||||
negatable: false,
|
||||
help: 'Overwrite executables from other packages with the same name.',
|
||||
);
|
||||
|
||||
argParser.addOption(
|
||||
'hosted-url',
|
||||
abbr: 'u',
|
||||
help: 'A custom pub server URL for the package. '
|
||||
'Only applies when using a package name for <package>.',
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses the arguments.
|
||||
///
|
||||
/// Reports usage errors to user if the wrong number or arguments or the wrong
|
||||
/// flags are passed.
|
||||
_InstallCommandParsedArguments _parseArguments() {
|
||||
final argResults = this.argResults!;
|
||||
|
||||
final overwrite = argResults.flag('overwrite');
|
||||
|
||||
Iterable<String> args = argResults.rest;
|
||||
|
||||
String readArg([String error = '']) {
|
||||
if (args.isEmpty) usageException(error);
|
||||
final arg = args.first;
|
||||
args = args.skip(1);
|
||||
return arg;
|
||||
}
|
||||
|
||||
final argument = readArg('No package source given.');
|
||||
final sourceKind = _SourceKind.fromArgument(argument);
|
||||
|
||||
final gitPath = argResults.option('git-path');
|
||||
var gitRef = argResults.option('git-ref');
|
||||
if (sourceKind != _SourceKind.git && (gitPath != null || gitRef != null)) {
|
||||
usageException(
|
||||
'Options `--git-path` and `--git-ref` '
|
||||
'can only be used with a git source.',
|
||||
);
|
||||
}
|
||||
|
||||
final hostedUrl = argResults.option('hosted-url');
|
||||
if (sourceKind != _SourceKind.hosted && hostedUrl != null) {
|
||||
usageException(
|
||||
'Option `--hosted-url` can only be used with a hosted source.',
|
||||
);
|
||||
}
|
||||
|
||||
String? versionConstraint;
|
||||
switch (sourceKind) {
|
||||
case _SourceKind.git:
|
||||
case _SourceKind.path:
|
||||
break;
|
||||
case _SourceKind.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<String> _findPackageName(
|
||||
_InstallCommandParsedArguments parsedArgs,
|
||||
) async {
|
||||
switch (parsedArgs.sourceKind) {
|
||||
case _SourceKind.git:
|
||||
return await getPackageNameFromGitRepo(
|
||||
parsedArgs.source,
|
||||
ref: parsedArgs.gitRef,
|
||||
path: parsedArgs.gitPath,
|
||||
relativeTo: Directory.current.path,
|
||||
tagPattern: null,
|
||||
);
|
||||
case _SourceKind.hosted:
|
||||
return parsedArgs.source;
|
||||
case _SourceKind.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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a helper package to pull in the requested package as a dependency.
|
||||
///
|
||||
/// The user provides us either with (1) a package name plus optional version
|
||||
/// constraint, (2) a git repo, or (3) a local path. In order to avoid
|
||||
/// reimplementing pub's knowledge about how to pull in (1) and (2), we create
|
||||
/// a package with a dependency on the package that the user wants to install.
|
||||
/// Subsequently, we run `pub get` to let pub pull in dependencies, and we use
|
||||
/// the `package_graph.json` to find the root of the package that was pulled
|
||||
/// in by pub.
|
||||
void _createHelperPackagePubspec({
|
||||
required _InstallCommandParsedArguments parsedArgs,
|
||||
required String packageName,
|
||||
required Directory helperPackageDir,
|
||||
}) {
|
||||
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) {
|
||||
_SourceKind.git => GitDependencySourceSyntax(
|
||||
git: GitSyntax(
|
||||
url: parsedArgs.source,
|
||||
path$: parsedArgs.gitPath,
|
||||
ref: parsedArgs.gitRef,
|
||||
),
|
||||
),
|
||||
_SourceKind.hosted => HostedDependencySourceSyntax(
|
||||
hosted: parsedArgs.hostedUrl,
|
||||
version: parsedArgs.versionConstraint!,
|
||||
),
|
||||
_SourceKind.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,
|
||||
),
|
||||
}
|
||||
},
|
||||
);
|
||||
helperPackagePubspec.writeSync(tempPubspec);
|
||||
}
|
||||
|
||||
static const _helperPackageName = 'dart_install_helper_package';
|
||||
|
||||
Future<void> _resolveHelperPackage(Directory helperPackageDir) async {
|
||||
try {
|
||||
await ensurePubspecResolved(helperPackageDir.path);
|
||||
} on ResolutionFailedException catch (e) {
|
||||
_installException(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/// The executables that should be placed on the user's PATH when this
|
||||
/// package is installed.
|
||||
DartBuildExecutables _loadDeclaredExecutables(
|
||||
File sourcePackagePubspecFile,
|
||||
Directory sourcePackageRootDirectory,
|
||||
) {
|
||||
final pubspecSyntax = PubspecYamlFile.loadSync(sourcePackagePubspecFile);
|
||||
|
||||
final errors = pubspecSyntax.validateExecutables();
|
||||
if (errors.isNotEmpty) {
|
||||
_installException([
|
||||
'The pubspec.yaml contains the following errors:',
|
||||
...errors
|
||||
].join('\n'));
|
||||
}
|
||||
// This is a map of strings to string. Each key is the name of the command
|
||||
// that will be placed on the user's PATH. The value is the name of the
|
||||
// .dart script (without extension) in the package's `bin` directory that
|
||||
// should be run for that command. If the value is null, it defaults to the
|
||||
// key.
|
||||
final executablesSyntax = pubspecSyntax.executables;
|
||||
if (executablesSyntax == null) {
|
||||
_installException('The pubspec.yaml contained no executables section.');
|
||||
}
|
||||
if (executablesSyntax.isEmpty) {
|
||||
_installException(
|
||||
'The pubspec.yaml executables section contained no executables.');
|
||||
}
|
||||
|
||||
return [
|
||||
for (final executable in executablesSyntax.entries)
|
||||
(
|
||||
name: executable.key,
|
||||
sourceEntryPoint: sourcePackageRootDirectory.uri
|
||||
.resolve('bin/${executable.value ?? executable.key}.dart')
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _doBuild(
|
||||
DartBuildExecutables executables,
|
||||
Directory buildDirectory,
|
||||
File helperPackageConfigFile,
|
||||
File sourcePackagePubspecFile,
|
||||
) async {
|
||||
// TODO(https://github.com/dart-lang/native/issues/2465): Add a test for
|
||||
// user-defines in the source package pubspec.
|
||||
final buildResult = await BuildCliSubcommand.doBuild(
|
||||
executables: executables,
|
||||
enabledExperiments: [],
|
||||
outputUri: buildDirectory.uri,
|
||||
packageConfigUri: helperPackageConfigFile.uri,
|
||||
pubspecUri: sourcePackagePubspecFile.uri,
|
||||
recordUseEnabled: false,
|
||||
verbose: verbose,
|
||||
verbosity: 'all',
|
||||
);
|
||||
if (buildResult != 0) {
|
||||
_installException('Build failed.', exitCode: buildResult);
|
||||
}
|
||||
}
|
||||
|
||||
void _uniinstallAllPackageVersions(String packageName) {
|
||||
final bundles =
|
||||
DartInstallDirectory().allAppBundlesSync(packageName: packageName);
|
||||
|
||||
try {
|
||||
for (final bundle in bundles) {
|
||||
print('Uninstalling ${bundle.directory.path}.');
|
||||
final links = bundle.executablesOnPathSync;
|
||||
for (final link in links) {
|
||||
print('Deleting ${link.entity.path}');
|
||||
link.deleteSync();
|
||||
}
|
||||
print('Deleting ${bundle.directory.path}');
|
||||
bundle.directory.deleteSync(recursive: true);
|
||||
}
|
||||
} on PathAccessException {
|
||||
_installException('Deletion failed. The application might be in use.');
|
||||
}
|
||||
}
|
||||
|
||||
AppBundleDirectory _selectAppBundleDirectory(
|
||||
_InstallCommandParsedArguments parsedArgs,
|
||||
String packageName,
|
||||
Directory helperPackageDir,
|
||||
File helperPackageLockFile,
|
||||
) {
|
||||
final AppBundleDirectory outputDir;
|
||||
switch (parsedArgs.sourceKind) {
|
||||
case _SourceKind.git:
|
||||
final resolvedGitRef = parsedArgs.gitRef ??
|
||||
GitPackageDescriptionSyntax.fromJson(
|
||||
PubspecLockFile.loadSync(helperPackageLockFile)
|
||||
.packages![packageName]!
|
||||
.description
|
||||
.json,
|
||||
).resolvedRef;
|
||||
outputDir = DartInstallDirectory().gitAppBundle(
|
||||
packageName,
|
||||
resolvedGitRef,
|
||||
);
|
||||
case _SourceKind.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 _SourceKind.path:
|
||||
outputDir = DartInstallDirectory().localAppBundle(packageName);
|
||||
}
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
Future<void> _createAppBundleDirectory(
|
||||
AppBundleDirectory appBundleDirectory,
|
||||
Directory buildDirectory,
|
||||
File helperPackageLockFile,
|
||||
File sourcePackagePubspecFile) async {
|
||||
if (appBundleDirectory.directory.existsSync()) {
|
||||
try {
|
||||
appBundleDirectory.directory.deleteSync(recursive: true);
|
||||
} on PathAccessException {
|
||||
_installException(
|
||||
'Failed to delete: ${appBundleDirectory.directory.path}. '
|
||||
'The application might be in use.',
|
||||
);
|
||||
}
|
||||
}
|
||||
appBundleDirectory.directory.createSync(recursive: true);
|
||||
final bundleDirectory =
|
||||
Directory.fromUri(buildDirectory.uri.resolve('bundle/'));
|
||||
await bundleDirectory.rename(
|
||||
appBundleDirectory.directory.uri.resolve('bundle/').toFilePath());
|
||||
await helperPackageLockFile.copy(appBundleDirectory.pubspecLock.path);
|
||||
await sourcePackagePubspecFile.copy(appBundleDirectory.pubspec.path);
|
||||
}
|
||||
|
||||
void _installExecutablesOnPath(
|
||||
DartBuildExecutables executables,
|
||||
AppBundleDirectory appBundleDirectory,
|
||||
String packageName,
|
||||
_InstallCommandParsedArguments parsedArgs) {
|
||||
final errors = <String>[];
|
||||
for (final executable in executables) {
|
||||
final executableName = executable.name;
|
||||
final executableFile = appBundleDirectory.executable(executableName);
|
||||
final executableOnPath =
|
||||
DartInstallDirectory().bin.executable(executableName);
|
||||
var createLink = true;
|
||||
|
||||
if (executableOnPath.existsSync()) {
|
||||
final targetExecutable = executableOnPath.targetSync();
|
||||
final targetPackageName = targetExecutable.appBundle.tryPackageName;
|
||||
if (targetPackageName == null ||
|
||||
targetPackageName == packageName ||
|
||||
parsedArgs.overwrite) {
|
||||
try {
|
||||
executableOnPath.deleteSync();
|
||||
} on PathAccessException {
|
||||
_installException(
|
||||
'Failed to delete: ${executableOnPath.entity.path}. '
|
||||
'The application might be in use.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
errors.add(
|
||||
'Refusing to overwrite executable $executableName from package:$targetPackageName. '
|
||||
'Pass --overwrite to override.',
|
||||
);
|
||||
createLink = false;
|
||||
}
|
||||
}
|
||||
if (createLink) {
|
||||
executableOnPath.createSync(executableFile);
|
||||
print('Installed: ${executableOnPath.entity.path}');
|
||||
}
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
_installException(errors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks to see if the binstubs are on the user's PATH and, if not, suggests
|
||||
/// that the user add the directory to their PATH.
|
||||
///
|
||||
/// [installed] should be the name of an installed executable that can be used
|
||||
/// to test whether accessing it on the path works.
|
||||
static void _suggestIfNotOnPath(String installed) {
|
||||
final binDirPath = DartInstallDirectory().bin.directory.path;
|
||||
if (Platform.isWindows) {
|
||||
// See if the shell can find one of the binstubs.
|
||||
// "\q" means return exit code 0 if found or 1 if not.
|
||||
final result = Process.runSync('where', [r'\q', '$installed.bat']);
|
||||
if (result.exitCode == 0) return;
|
||||
|
||||
stdout.writeln(
|
||||
'Warning: Dart installs executables into '
|
||||
'$binDirPath, which is not on your path.\n'
|
||||
"You can fix that by adding that directory to your system's "
|
||||
'"Path" environment variable.\n'
|
||||
'A web search for "configure windows path" will show you how.',
|
||||
);
|
||||
} else {
|
||||
// See if the shell can find one of the binstubs.
|
||||
//
|
||||
// The "command" builtin is more reliable than the "which" executable. See
|
||||
// http://unix.stackexchange.com/questions/85249/why-not-use-which-what-to-use-then
|
||||
final result = Process.runSync(
|
||||
'command',
|
||||
[
|
||||
'-v',
|
||||
installed,
|
||||
],
|
||||
runInShell: true,
|
||||
);
|
||||
if (result.exitCode == 0) return;
|
||||
|
||||
var binDir = binDirPath;
|
||||
if (binDir.startsWith(Platform.environment['HOME']!)) {
|
||||
binDir = p.join(
|
||||
r'$HOME',
|
||||
p.relative(binDir, from: Platform.environment['HOME']),
|
||||
);
|
||||
}
|
||||
final shellConfigFiles = Platform.isMacOS
|
||||
// zsh is default on mac - mention that first.
|
||||
? '(.zshrc, .bashrc, .bash_profile, etc.)'
|
||||
: '(.bashrc, .bash_profile, .zshrc, etc.)';
|
||||
stdout.writeln(
|
||||
"'Warning: Dart installs executables into "
|
||||
'$binDir, which is not on your path.\n'
|
||||
"You can fix that by adding this to your shell's config file "
|
||||
'$shellConfigFiles:\n'
|
||||
'\n'
|
||||
' export PATH="\$PATH":"$binDir"\n'
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
final parsedArgs = _parseArguments();
|
||||
final packageName = await _findPackageName(parsedArgs);
|
||||
return await _inTempDir((tempDirectory) async {
|
||||
try {
|
||||
final helperPackageDirectory =
|
||||
Directory.fromUri(tempDirectory.uri.resolve('helperPackage/'));
|
||||
helperPackageDirectory.createSync();
|
||||
_createHelperPackagePubspec(
|
||||
helperPackageDir: helperPackageDirectory,
|
||||
packageName: packageName,
|
||||
parsedArgs: parsedArgs,
|
||||
);
|
||||
await _resolveHelperPackage(helperPackageDirectory);
|
||||
|
||||
final helperPackageLockFile =
|
||||
File.fromUri(helperPackageDirectory.uri.resolve('pubspec.lock'));
|
||||
final helperPackageConfigFile = File.fromUri(helperPackageDirectory.uri
|
||||
.resolve('.dart_tool/package_config.json'));
|
||||
|
||||
final sourcePackageRootDirectory = Directory(Uri.parse(
|
||||
PackageConfigFile.loadSync(helperPackageConfigFile)
|
||||
.packages
|
||||
.firstWhere((e) => e.name == packageName)
|
||||
.rootUri,
|
||||
).toFilePath())
|
||||
.ensureEndWithSeparator;
|
||||
|
||||
final sourcePackagePubspecFile = File.fromUri(
|
||||
sourcePackageRootDirectory.uri.resolve('pubspec.yaml'));
|
||||
|
||||
final executables = _loadDeclaredExecutables(
|
||||
sourcePackagePubspecFile,
|
||||
sourcePackageRootDirectory,
|
||||
);
|
||||
|
||||
final buildDirectory =
|
||||
Directory.fromUri(tempDirectory.uri.resolve('build/'));
|
||||
await _doBuild(
|
||||
executables,
|
||||
buildDirectory,
|
||||
helperPackageConfigFile,
|
||||
sourcePackagePubspecFile,
|
||||
);
|
||||
|
||||
_uniinstallAllPackageVersions(packageName);
|
||||
|
||||
AppBundleDirectory appBundleDirectory = _selectAppBundleDirectory(
|
||||
parsedArgs,
|
||||
packageName,
|
||||
helperPackageDirectory,
|
||||
helperPackageLockFile,
|
||||
);
|
||||
await _createAppBundleDirectory(
|
||||
appBundleDirectory,
|
||||
buildDirectory,
|
||||
helperPackageLockFile,
|
||||
sourcePackagePubspecFile,
|
||||
);
|
||||
|
||||
_installExecutablesOnPath(
|
||||
executables,
|
||||
appBundleDirectory,
|
||||
packageName,
|
||||
parsedArgs,
|
||||
);
|
||||
_suggestIfNotOnPath(executables.first.name);
|
||||
} on _InstallException catch (e) {
|
||||
stderr.writeln(e.message);
|
||||
return genericErrorExitCode;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// Throws a [_InstallException] with [message].
|
||||
///
|
||||
/// This enables similar coding style to using [usageException]s.
|
||||
Never _installException(String message, {int? exitCode}) =>
|
||||
throw _InstallException(message, exitCode: exitCode);
|
||||
|
||||
static Future<T> _inTempDir<T>(
|
||||
Future<T> Function(Directory tempDirectory) fun) async {
|
||||
final tempDir = await Directory.systemTemp.createTemp();
|
||||
// Deal with Windows temp folder aliases.
|
||||
final tempDirResolved = Directory.fromUri(
|
||||
Directory(await tempDir.resolveSymbolicLinks()).uri.normalizePath(),
|
||||
);
|
||||
try {
|
||||
return await fun(tempDirResolved);
|
||||
} finally {
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} on PathAccessException {
|
||||
if (Platform.isWindows) {
|
||||
// Don't fail on Windows having files in use.
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _InstallCommandParsedArguments {
|
||||
final String source;
|
||||
final _SourceKind sourceKind;
|
||||
final String? versionConstraint;
|
||||
final String? gitPath;
|
||||
final String? gitRef;
|
||||
final String? hostedUrl;
|
||||
final bool overwrite;
|
||||
|
||||
_InstallCommandParsedArguments._({
|
||||
required this.source,
|
||||
required this.sourceKind,
|
||||
required this.versionConstraint,
|
||||
required this.gitPath,
|
||||
required this.gitRef,
|
||||
required this.hostedUrl,
|
||||
required this.overwrite,
|
||||
});
|
||||
}
|
||||
|
||||
enum _SourceKind {
|
||||
git,
|
||||
hosted,
|
||||
path;
|
||||
|
||||
static _SourceKind fromArgument(String argument) {
|
||||
if (_packageNameRegExp.hasMatch(argument)) {
|
||||
return hosted;
|
||||
}
|
||||
final parsedUri = Uri.tryParse(argument);
|
||||
if (parsedUri != null) {
|
||||
switch (parsedUri.scheme.toLowerCase()) {
|
||||
case 'git':
|
||||
case 'http':
|
||||
case 'https':
|
||||
return git;
|
||||
}
|
||||
}
|
||||
final parsedGitSshUrl = _GitSshUrl.tryParse(argument);
|
||||
if (parsedGitSshUrl != null) {
|
||||
return git;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// A regular expression matching a Dart identifier.
|
||||
///
|
||||
/// This also matches a package name, since they must be Dart identifiers.
|
||||
static final _identifierRegExp = RegExp(r'[a-zA-Z_]\w*');
|
||||
|
||||
/// A regular expression matching allowed package names.
|
||||
///
|
||||
/// This allows dot-separated valid Dart identifiers. The dots are there for
|
||||
/// compatibility with Google's internal Dart packages, but they may not be used
|
||||
/// when publishing a package to pub.dev.
|
||||
static final _packageNameRegExp = RegExp(
|
||||
'^${_identifierRegExp.pattern}(\\.${_identifierRegExp.pattern})*\$',
|
||||
);
|
||||
}
|
||||
|
||||
// Expected format: git@host:owner/repository.git
|
||||
class _GitSshUrl {
|
||||
final String user;
|
||||
final String host;
|
||||
final String owner;
|
||||
final String repository;
|
||||
final String fullUrl;
|
||||
|
||||
_GitSshUrl({
|
||||
required this.user,
|
||||
required this.host,
|
||||
required this.owner,
|
||||
required this.repository,
|
||||
required this.fullUrl,
|
||||
});
|
||||
|
||||
static _GitSshUrl? tryParse(String url) {
|
||||
final regex = RegExp(r'^(\w+)@([^:]+):([^/]+)/(.+?)(?:\.git)?$');
|
||||
final match = regex.firstMatch(url);
|
||||
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _GitSshUrl(
|
||||
user: match.group(1)!,
|
||||
host: match.group(2)!,
|
||||
owner: match.group(3)!,
|
||||
repository: match.group(4)!,
|
||||
fullUrl: url,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GitSshUrl(user: $user, host: $host, owner: $owner, repository: $repository, fullUrl: $fullUrl)';
|
||||
}
|
||||
}
|
||||
|
||||
/// An exception during the installation process.
|
||||
class _InstallException implements Exception {
|
||||
final String message;
|
||||
final int? exitCode;
|
||||
|
||||
_InstallException(
|
||||
this.message, {
|
||||
this.exitCode,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2025, 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 'dart:io';
|
||||
|
||||
import 'package:dartdev/src/core.dart';
|
||||
import 'package:dartdev/src/install/file_system.dart';
|
||||
import 'package:dartdev/src/install/pub_formats.dart';
|
||||
import 'package:pub_formats/pub_formats.dart';
|
||||
|
||||
class InstalledCommand extends DartdevCommand {
|
||||
static const cmdName = 'installed';
|
||||
static const cmdDescription = 'List globally installed Dart CLI tools.';
|
||||
|
||||
@override
|
||||
CommandCategory get commandCategory => CommandCategory.global;
|
||||
|
||||
InstalledCommand({bool verbose = false})
|
||||
: super(cmdName, cmdDescription, verbose) {
|
||||
argParser.addFlag(
|
||||
'all',
|
||||
abbr: 'a',
|
||||
help: '''Also list packages which are currently not active.
|
||||
Active package have executables on `PATH`.
|
||||
App bundles of packages on disk which have no executables
|
||||
on `PATH` are non-active.''',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
final argResults = this.argResults!;
|
||||
final all = argResults.flag('all');
|
||||
|
||||
final installedPackages = getInstalledPackages();
|
||||
for (final package in installedPackages) {
|
||||
if (package.installed == Installed.not && !all) {
|
||||
continue;
|
||||
}
|
||||
print(package.toString());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static List<InstalledPackage> getInstalledPackages() {
|
||||
final allAppBundles = DartInstallDirectory().allAppBundlesSync();
|
||||
final result = <InstalledPackage>[];
|
||||
for (final appBundleDir in allAppBundles) {
|
||||
final packageName = appBundleDir.packageName;
|
||||
final lockFile = appBundleDir.pubspecLock;
|
||||
final pubspecLock = PubspecLockFile.loadSync(lockFile);
|
||||
final lockInfo = pubspecLock.packages!.entries
|
||||
.where((entry) =>
|
||||
entry.value.dependency == DependencyTypeSyntax.directMain)
|
||||
.single
|
||||
.value;
|
||||
final binaries = appBundleDir.executablesSync;
|
||||
var foundBinary = false;
|
||||
var missingBinary = false;
|
||||
for (final binary in binaries) {
|
||||
final link = binary.onPath;
|
||||
if (!link.existsSync()) {
|
||||
missingBinary = true;
|
||||
} else {
|
||||
if (link.targetSync().equals(binary)) {
|
||||
foundBinary = true;
|
||||
} else {
|
||||
missingBinary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
final lastModified = lockFile.lastModifiedSync();
|
||||
result.add(InstalledPackage(
|
||||
name: packageName,
|
||||
appBundle: appBundleDir.directory,
|
||||
installed: switch ((foundBinary, missingBinary)) {
|
||||
(_, false) => Installed.fully,
|
||||
(true, true) => Installed.partial,
|
||||
(false, true) => Installed.not,
|
||||
},
|
||||
lockInfo: lockInfo,
|
||||
lastModified: lastModified,
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledPackage {
|
||||
final String name;
|
||||
final Directory appBundle;
|
||||
final Installed installed;
|
||||
final PackageSyntax lockInfo;
|
||||
final DateTime lastModified;
|
||||
|
||||
InstalledPackage({
|
||||
required this.appBundle,
|
||||
required this.installed,
|
||||
required this.lastModified,
|
||||
required this.lockInfo,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var result = '$name ${lockInfo.version}';
|
||||
switch (lockInfo.source) {
|
||||
case PackageSourceSyntax.git:
|
||||
final description =
|
||||
GitPackageDescriptionSyntax.fromJson(lockInfo.description.json);
|
||||
final url = description.url;
|
||||
final resolvedRef = description.resolvedRef.substring(0, 8);
|
||||
result += ' from Git repository "$url" at "$resolvedRef"';
|
||||
case PackageSourceSyntax.hosted:
|
||||
break;
|
||||
case PackageSourceSyntax.path$:
|
||||
final description =
|
||||
PathPackageDescriptionSyntax.fromJson(lockInfo.description.json);
|
||||
final path = description.path$;
|
||||
result += ' from "$path" at $lastModified';
|
||||
default:
|
||||
result += ' from an unknown source "${lockInfo.source.name}"';
|
||||
}
|
||||
switch (installed) {
|
||||
case Installed.fully:
|
||||
break;
|
||||
case Installed.partial:
|
||||
result += ' (partially active)';
|
||||
case Installed.not:
|
||||
result += ' (not active)';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
enum Installed {
|
||||
fully,
|
||||
partial,
|
||||
not;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2025, 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 'dart:io';
|
||||
|
||||
import 'package:dartdev/src/core.dart';
|
||||
import 'package:dartdev/src/install/file_system.dart';
|
||||
|
||||
class UninstallCommand extends DartdevCommand {
|
||||
static const cmdName = 'uninstall';
|
||||
static const cmdDescription = '''Remove a globally installed Dart CLI tool.
|
||||
|
||||
Completely deletes all installed versions of <package> and all executables from
|
||||
<package> placed on PATH.''';
|
||||
|
||||
@override
|
||||
String get invocation {
|
||||
final superNoArguments = super.invocation.replaceAll(' [arguments]', '');
|
||||
return '$superNoArguments <package>';
|
||||
}
|
||||
|
||||
@override
|
||||
CommandCategory get commandCategory => CommandCategory.global;
|
||||
|
||||
UninstallCommand({bool verbose = false})
|
||||
: super(cmdName, cmdDescription, verbose);
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
final argResults = this.argResults!;
|
||||
Iterable<String> args = argResults.rest;
|
||||
if (args.length != 1) {
|
||||
final arguments = args.isEmpty ? 'none' : '"${args.join(' ')}"';
|
||||
usageException(
|
||||
'Wrong number of arguments, expected "<package>", got $arguments.',
|
||||
);
|
||||
}
|
||||
final package = args.single;
|
||||
|
||||
final bundles =
|
||||
DartInstallDirectory().allAppBundlesSync(packageName: package);
|
||||
if (bundles.isEmpty) {
|
||||
print('Did not find any packages named "$package".');
|
||||
return 255;
|
||||
}
|
||||
|
||||
try {
|
||||
for (final bundle in bundles) {
|
||||
final links = bundle.executablesOnPathSync;
|
||||
for (final link in links) {
|
||||
print('Deleting ${link.entity.path}');
|
||||
link.deleteSync();
|
||||
}
|
||||
print('Deleting ${bundle.directory.path}');
|
||||
bundle.directory.deleteSync(recursive: true);
|
||||
}
|
||||
} on PathAccessException {
|
||||
stderr.writeln('Deletion failed. The application might be in use.');
|
||||
return 255;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ abstract class DartdevCommand extends Command<int> {
|
||||
}
|
||||
|
||||
enum CommandCategory {
|
||||
global('Global'),
|
||||
project('Project'),
|
||||
sourceCode('Source code'),
|
||||
tools('Tools');
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) 2025, 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.
|
||||
|
||||
/// The directory and file structure for `dart install` on the host machine.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dart_data_home/dart_data_home.dart';
|
||||
import 'package:dartdev/src/utils.dart';
|
||||
|
||||
/// The root directory for Dart installations.
|
||||
///
|
||||
/// This directory contains various subdirectories for binaries and app bundles.
|
||||
///
|
||||
/// <pre>
|
||||
/// [DartInstallDirectory]
|
||||
/// ├── [bin]/
|
||||
/// │ └── (executables)
|
||||
/// └── app-bundles/
|
||||
/// └── (packageName)/
|
||||
/// ├── git/
|
||||
/// │ └── (gitHash)/
|
||||
/// │ └── [AppBundleDirectory] (e.g., 'my_package/git/abcdef123/')
|
||||
/// ├── hosted/
|
||||
/// │ └── (version)/
|
||||
/// │ └── [AppBundleDirectory] (e.g., 'my_package/hosted/1.0.0/')
|
||||
/// └── local/
|
||||
/// └── [AppBundleDirectory] (e.g., 'my_package/local/')
|
||||
/// </pre>
|
||||
extension type DartInstallDirectory._(Directory directory) {
|
||||
static final DartInstallDirectory _singleton =
|
||||
DartInstallDirectory._(Directory(getDartDataHome('install')));
|
||||
|
||||
factory DartInstallDirectory() {
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
BinOnPathDirectory get bin => BinOnPathDirectory._(
|
||||
Directory.fromUri(
|
||||
directory.uri.resolve('bin/'),
|
||||
),
|
||||
);
|
||||
|
||||
Directory get _appBundles =>
|
||||
Directory.fromUri(directory.uri.resolve('app-bundles/'));
|
||||
|
||||
AppBundleDirectory gitAppBundle(
|
||||
String packageName,
|
||||
String gitHash,
|
||||
) =>
|
||||
AppBundleDirectory._(
|
||||
Directory.fromUri(
|
||||
_appBundles.uri.resolve('$packageName/git/$gitHash/'),
|
||||
),
|
||||
);
|
||||
|
||||
AppBundleDirectory hostedAppBundle(
|
||||
String packageName,
|
||||
String version,
|
||||
) =>
|
||||
AppBundleDirectory._(
|
||||
Directory.fromUri(
|
||||
_appBundles.uri.resolve('$packageName/hosted/$version/'),
|
||||
),
|
||||
);
|
||||
|
||||
AppBundleDirectory localAppBundle(
|
||||
String packageName,
|
||||
) =>
|
||||
AppBundleDirectory._(
|
||||
Directory.fromUri(
|
||||
_appBundles.uri.resolve('$packageName/local/'),
|
||||
),
|
||||
);
|
||||
|
||||
List<AppBundleDirectory> allAppBundlesSync({String? packageName}) {
|
||||
final dartInstallAppbundlesDir = _appBundles;
|
||||
if (!dartInstallAppbundlesDir.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
final packageDirs =
|
||||
dartInstallAppbundlesDir.listSync().whereType<Directory>();
|
||||
final result = <AppBundleDirectory>[];
|
||||
for (final packageDir in packageDirs) {
|
||||
if (packageName != null && packageDir.name != packageName) {
|
||||
continue;
|
||||
}
|
||||
final gitDir = Directory.fromUri(packageDir.uri.resolve('git/'));
|
||||
final hostedDir = Directory.fromUri(packageDir.uri.resolve('hosted/'));
|
||||
final localDir = Directory.fromUri(packageDir.uri.resolve('local/'));
|
||||
if (gitDir.existsSync()) {
|
||||
result.addAll(
|
||||
gitDir
|
||||
.listSync()
|
||||
.whereType<Directory>()
|
||||
.map((d) => AppBundleDirectory._(d.ensureEndWithSeparator)),
|
||||
);
|
||||
}
|
||||
if (hostedDir.existsSync()) {
|
||||
result.addAll(
|
||||
hostedDir
|
||||
.listSync()
|
||||
.whereType<Directory>()
|
||||
.map((d) => AppBundleDirectory._(d.ensureEndWithSeparator)),
|
||||
);
|
||||
}
|
||||
if (localDir.existsSync()) {
|
||||
result.add(AppBundleDirectory._(localDir.ensureEndWithSeparator));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// The directory that contains all executables available on `PATH`.
|
||||
///
|
||||
/// <pre>
|
||||
/// [BinOnPathDirectory]
|
||||
/// └── [executable]s
|
||||
/// </pre>
|
||||
extension type BinOnPathDirectory._(Directory directory) {
|
||||
/// An executable with [name] in the bin directory.
|
||||
///
|
||||
/// The parameter [name] must not contain an extension.
|
||||
ExecutableOnPath executable(String name) {
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
return ExecutableOnPath._unix(Link.fromUri(directory.uri.resolve(name)));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return ExecutableOnPath._windows(
|
||||
File.fromUri(directory.uri.resolve('$name.bat')));
|
||||
}
|
||||
throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.');
|
||||
}
|
||||
}
|
||||
|
||||
/// An executable in [BinOnPathDirectory] available on `PATH`.
|
||||
///
|
||||
/// [entity] is a [Link] on Linux and MacOS, and a [File] on Windows.
|
||||
extension type ExecutableOnPath._(FileSystemEntity entity) {
|
||||
factory ExecutableOnPath._unix(Link link) => ExecutableOnPath._(link);
|
||||
|
||||
Link get unix {
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
return entity as Link;
|
||||
}
|
||||
throw UnsupportedError('Wrong OS: ${Platform.operatingSystem}.');
|
||||
}
|
||||
|
||||
factory ExecutableOnPath._windows(File file) => ExecutableOnPath._(file);
|
||||
|
||||
File get windows {
|
||||
if (Platform.isWindows) {
|
||||
return entity as File;
|
||||
}
|
||||
throw UnsupportedError('Wrong OS: ${Platform.operatingSystem}.');
|
||||
}
|
||||
|
||||
bool existsSync() => entity.existsSync();
|
||||
|
||||
void deleteSync() => entity.deleteSync();
|
||||
|
||||
static const _marker = 'target_file_path_marker';
|
||||
|
||||
void createSync(ExecutableInBundle target) {
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
return unix.createSync(target.file.path, recursive: true);
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
final wrapperScriptContents = '''
|
||||
@ECHO OFF
|
||||
REM $_marker
|
||||
"${target.file.path}" %*
|
||||
EXIT /B %ERRORLEVEL%
|
||||
''';
|
||||
if (!windows.existsSync()) {
|
||||
windows.createSync(recursive: true);
|
||||
}
|
||||
return windows.writeAsStringSync(wrapperScriptContents);
|
||||
}
|
||||
throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.');
|
||||
}
|
||||
|
||||
ExecutableInBundle targetSync() {
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
return ExecutableInBundle._(File(unix.targetSync()));
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
final wrapperScriptContents = windows.readAsStringSync();
|
||||
final iterator = wrapperScriptContents.split('\n').iterator..moveNext();
|
||||
while (!iterator.current.contains(_marker)) {
|
||||
iterator.moveNext();
|
||||
}
|
||||
iterator.moveNext();
|
||||
final line = iterator.current;
|
||||
final path = line.split('"')[1];
|
||||
return ExecutableInBundle._(File(path));
|
||||
}
|
||||
throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.');
|
||||
}
|
||||
|
||||
bool equals(ExecutableOnPath other) => entity.path == other.entity.path;
|
||||
}
|
||||
|
||||
/// A directory containing an app bundle and its installation data.
|
||||
///
|
||||
/// This directory is structured as follows:
|
||||
///
|
||||
/// <pre>
|
||||
/// [AppBundleDirectory]
|
||||
/// ├── bundle/ (Contains the application code and assets)
|
||||
/// │ ├── bin/ (Executables that can be run directly)
|
||||
/// │ │ └── [ExecutableInBundle] (Specific executable files for the app bundle)
|
||||
/// │ └── lib/ (Dynamic libraries required by the executables)
|
||||
/// │ └── (dynamic libraries) (Platform-specific shared libraries)
|
||||
/// ├── pubspec.lock (Generated by pub, locks package dependencies to specific versions)
|
||||
/// └── pubspec.yaml (Declares project dependencies and metadata)
|
||||
/// </pre>
|
||||
extension type AppBundleDirectory._(Directory directory) {
|
||||
String get packageName {
|
||||
final result = tryPackageName;
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
throw StateError('${directory.path} is not a valid app bundle directory.');
|
||||
}
|
||||
|
||||
String? get tryPackageName {
|
||||
if (!directory.path.startsWith(DartInstallDirectory()._appBundles.path)) {
|
||||
throw StateError(
|
||||
'${directory.path} does not start with ${DartInstallDirectory()._appBundles.path}.',
|
||||
);
|
||||
// return null;
|
||||
}
|
||||
final relativeSegments = directory.uri.pathSegments
|
||||
.skip(DartInstallDirectory()
|
||||
._appBundles
|
||||
.uri
|
||||
.pathSegments
|
||||
.where((e) => e.isNotEmpty)
|
||||
.length)
|
||||
.toList();
|
||||
if (relativeSegments.length < 2) {
|
||||
throw StateError(
|
||||
'$directory, $relativeSegments does not contain at least two path segments.',
|
||||
);
|
||||
// return null;
|
||||
}
|
||||
if (relativeSegments[1] != 'hosted' &&
|
||||
relativeSegments[1] != 'git' &&
|
||||
relativeSegments[1] != 'local') {
|
||||
throw StateError(
|
||||
'$directory, $relativeSegments, ${relativeSegments[1]} is not hosted, git or local.',
|
||||
);
|
||||
// return null;
|
||||
}
|
||||
return relativeSegments[0];
|
||||
}
|
||||
|
||||
Directory get _binDirectory =>
|
||||
Directory.fromUri(directory.uri.resolve('bundle/bin/'));
|
||||
|
||||
List<ExecutableInBundle> get executablesSync {
|
||||
final binaries = _binDirectory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.map((e) => ExecutableInBundle._(e))
|
||||
.toList();
|
||||
return binaries;
|
||||
}
|
||||
|
||||
/// The executables from this bundle which are available on `PATH`.
|
||||
List<ExecutableOnPath> get executablesOnPathSync {
|
||||
final result = <ExecutableOnPath>[];
|
||||
for (final executable in executablesSync) {
|
||||
final onPath = executable.onPath;
|
||||
if (onPath.existsSync() && onPath.targetSync().equals(executable)) {
|
||||
result.add(onPath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// An executable with [name] in the app bundle.
|
||||
///
|
||||
/// The parameter [name] most not contain an extension.
|
||||
ExecutableInBundle executable(String name) {
|
||||
return ExecutableInBundle._(File.fromUri(
|
||||
_binDirectory.uri.resolve(
|
||||
Platform.isWindows ? '$name.exe' : name,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
File get pubspec => File.fromUri(directory.uri.resolve('pubspec.yaml'));
|
||||
|
||||
File get pubspecLock => File.fromUri(directory.uri.resolve('pubspec.lock'));
|
||||
}
|
||||
|
||||
/// An executable inside an [AppBundleDirectory].
|
||||
extension type ExecutableInBundle._(File file) {
|
||||
AppBundleDirectory get appBundle {
|
||||
return AppBundleDirectory._(Directory.fromUri(
|
||||
file.uri.resolve('../../'),
|
||||
));
|
||||
}
|
||||
|
||||
ExecutableOnPath get onPath =>
|
||||
DartInstallDirectory().bin.executable(file.basenameWithoutExtension);
|
||||
|
||||
bool equals(ExecutableInBundle other) => file.path == other.file.path;
|
||||
}
|
||||
|
||||
extension DirectoryExtension on Directory {
|
||||
Directory get ensureEndWithSeparator => Directory.fromUri(uri);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2025, 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 'dart:convert' hide json;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:pub_formats/pub_formats.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
extension PubspecYamlFile on PubspecYamlFileSyntax {
|
||||
static PubspecYamlFileSyntax loadSync(File file) {
|
||||
return PubspecYamlFileSyntax.fromJson(
|
||||
_convertYamlMapToJsonMap(
|
||||
loadYamlDocument(file.readAsStringSync()).contents as YamlMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void writeSync(File file) {
|
||||
// JSON is valid YAML, and JSON encoding is much faster, write JSON.
|
||||
return file.writeAsStringSync(_jsonEncoder.convert(json));
|
||||
}
|
||||
}
|
||||
|
||||
extension PubspecLockFile on PubspecLockFileSyntax {
|
||||
static PubspecLockFileSyntax loadSync(File file) {
|
||||
return PubspecLockFileSyntax.fromJson(
|
||||
_convertYamlMapToJsonMap(
|
||||
loadYamlDocument(file.readAsStringSync()).contents as YamlMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PackageGraphFile on PackageGraphFileSyntax {
|
||||
static PackageGraphFileSyntax loadSync(File file) {
|
||||
return PackageGraphFileSyntax.fromJson(
|
||||
jsonDecode(file.readAsStringSync()) as Map<String, Object?>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PackageConfigFile on PackageConfigFileSyntax {
|
||||
static PackageConfigFileSyntax loadSync(File file) {
|
||||
return PackageConfigFileSyntax.fromJson(
|
||||
jsonDecode(file.readAsStringSync()) as Map<String, Object?>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _jsonEncoder = JsonEncoder.withIndent(' ');
|
||||
|
||||
Map<String, Object?> _convertYamlMapToJsonMap(YamlMap yamlMap) {
|
||||
final Map<String, Object?> jsonMap = {};
|
||||
yamlMap.forEach((key, value) {
|
||||
if (key is! String) {
|
||||
throw UnsupportedError(
|
||||
'YAML map keys must be strings for JSON conversion.');
|
||||
}
|
||||
jsonMap[key] = _convertYamlValue(value);
|
||||
});
|
||||
return jsonMap;
|
||||
}
|
||||
|
||||
Object? _convertYamlValue(dynamic yamlValue) {
|
||||
if (yamlValue is YamlMap) {
|
||||
return _convertYamlMapToJsonMap(yamlValue);
|
||||
} else if (yamlValue is YamlList) {
|
||||
return yamlValue.map((e) => _convertYamlValue(e)).toList();
|
||||
} else {
|
||||
// For primitive types: String, int, double, bool, null.
|
||||
return yamlValue;
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,8 @@ String trimEnd(String s, String? suffix) {
|
||||
extension FileSystemEntityExtension on FileSystemEntity {
|
||||
String get name => p.basename(path);
|
||||
|
||||
String get basenameWithoutExtension => p.basenameWithoutExtension(path);
|
||||
|
||||
bool get isDartFile => this is File && p.extension(path) == '.dart';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies:
|
||||
code_assets: any
|
||||
collection: any
|
||||
dart2native: any
|
||||
dart_data_home: any
|
||||
dart_mcp_server: any
|
||||
dart_style: any
|
||||
dartdoc: any
|
||||
@@ -37,6 +38,7 @@ dependencies:
|
||||
package_config: any
|
||||
path: any
|
||||
pub: any
|
||||
pub_formats: any
|
||||
unified_analytics: any
|
||||
vm: any
|
||||
vm_service: any
|
||||
|
||||
@@ -79,6 +79,11 @@ void help() {
|
||||
'''
|
||||
Available commands:
|
||||
|
||||
Global
|
||||
install Install or upgrade a Dart CLI tool for global use.
|
||||
installed List globally installed Dart CLI tools.
|
||||
uninstall Remove a globally installed Dart CLI tool.
|
||||
|
||||
Project
|
||||
build Build a Dart application including native assets.
|
||||
compile Compile Dart to various formats.
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:yaml_edit/yaml_edit.dart';
|
||||
|
||||
import '../utils.dart';
|
||||
|
||||
export 'package:hooks_runner/src/utils/run_process.dart' show RunProcessResult;
|
||||
|
||||
extension UriExtension on Uri {
|
||||
Uri get parent {
|
||||
return File(toFilePath()).parent.uri;
|
||||
@@ -34,7 +36,15 @@ Future<void> inTempDir(Future<void> Function(Uri tempUri) fun) async {
|
||||
} finally {
|
||||
if (!Platform.environment.containsKey(keepTempKey) ||
|
||||
Platform.environment[keepTempKey]!.isEmpty) {
|
||||
await tempDir.delete(recursive: true);
|
||||
try {
|
||||
await tempDir.delete(recursive: true);
|
||||
} on PathAccessException {
|
||||
if (Platform.isWindows) {
|
||||
// Don't fail on files being in use.
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,12 +320,14 @@ Future<run_process.RunProcessResult> runDart({
|
||||
Uri? workingDirectory,
|
||||
required Logger? logger,
|
||||
bool expectExitCodeZero = true,
|
||||
Map<String, String>? environment,
|
||||
}) async {
|
||||
final result = await runProcess(
|
||||
executable: dartExecutable,
|
||||
arguments: arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
logger: logger,
|
||||
environment: environment,
|
||||
);
|
||||
if (expectExitCodeZero) {
|
||||
if (result.exitCode != 0) {
|
||||
|
||||
@@ -0,0 +1,850 @@
|
||||
// Copyright (c) 2025, 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 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pub_formats/pub_formats.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../utils.dart';
|
||||
import 'helpers.dart';
|
||||
|
||||
/// A package in the Dart SDK that we use for testing.
|
||||
///
|
||||
/// This package is on pub.dev, and on a Git repo, and on disk. So, this can be
|
||||
/// used for testing hosted, git, and path installs.
|
||||
///
|
||||
/// Moreover it has an executables section in the pubspec.
|
||||
const _packageForTest = 'vm_snapshot_analysis';
|
||||
|
||||
/// A valid version for [_packageForTest].
|
||||
///
|
||||
/// Not the newest version.
|
||||
const _packageVersion = '0.7.5';
|
||||
|
||||
/// The name of an executable in [_packageForTest].
|
||||
const _cliToolForTest = 'snapshot_analysis';
|
||||
|
||||
final _pathEnvVarSeparator = Platform.isWindows ? ';' : ':';
|
||||
|
||||
/// A package not in the Dart SDK repo. The Dart SDK repo takes too long to
|
||||
/// clone.
|
||||
const _gitPackageForTest = 'dart_app';
|
||||
|
||||
final _gitHttpsUrl = Uri.parse('https://dart.googlesource.com/native');
|
||||
|
||||
const _gitPath = 'pkgs/hooks_runner/test_data/dart_app/';
|
||||
|
||||
const _gitRef = '8ce789991cddb8864b0f3fa210c83d3d10e78316';
|
||||
|
||||
const String _dartDirectoryEnvKey = 'DART_DATA_HOME';
|
||||
|
||||
final _dartDevEntryScriptUri = resolveDartDevUri('bin/dartdev.dart');
|
||||
|
||||
final _sdkUri = resolveDartDevUri('.').resolve('../../');
|
||||
|
||||
final _packageRelativePath = Uri.directory('pkg/vm_snapshot_analysis/');
|
||||
|
||||
final _packageDir = Directory.fromUri(
|
||||
_sdkUri.resolveUri(_packageRelativePath),
|
||||
);
|
||||
|
||||
void main([List<String> args = const []]) async {
|
||||
if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool fromDartdevSource = args.contains('--source');
|
||||
final errorExitCode = fromDartdevSource
|
||||
? /* Dartdev doesn't exit the process, it sends a message to the VM.*/ 0
|
||||
: 255;
|
||||
final argsFiltered = args.where((e) => e != '--source').toList();
|
||||
final testName = argsFiltered.isEmpty ? null : argsFiltered.join(' ');
|
||||
|
||||
@isTest
|
||||
void skippableTest(
|
||||
String description,
|
||||
dynamic Function() body, {
|
||||
Timeout? timeout,
|
||||
}) {
|
||||
test(
|
||||
description,
|
||||
skip: !(testName == null || description.contains(testName)),
|
||||
timeout: timeout,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
final commandsHelpmessages = [
|
||||
(
|
||||
'install',
|
||||
'''
|
||||
Install or upgrade a Dart CLI tool for global use.
|
||||
|
||||
Install all executables specified in a package's pubspec.yaml executables
|
||||
section (https://dart.dev/tools/pub/pubspec#executables) on the PATH. If the
|
||||
executables section doesn't exist, installs all `bin/*.dart` entry points as
|
||||
executables.
|
||||
|
||||
If the same package has been previously installed, it will be overwritten.
|
||||
|
||||
You can specify three different values for the <package> 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)
|
||||
|
||||
Usage: dart install <package> [version-constraint]
|
||||
-h, --help Print this usage information.
|
||||
--git-path Path of git package in repository. Only applies when using a git url for <package>.
|
||||
--git-ref Git branch or commit to be retrieved. Only applies when using a git url for <package>.
|
||||
--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 <package>.
|
||||
|
||||
Run "dart help" to see global options.
|
||||
'''
|
||||
),
|
||||
(
|
||||
'installed',
|
||||
'''
|
||||
List globally installed Dart CLI tools.
|
||||
|
||||
Usage: dart installed [arguments]
|
||||
-h, --help Print this usage information.
|
||||
-a, --[no-]all Also list packages which are currently not active.
|
||||
Active package have executables on `PATH`.
|
||||
App bundles of packages on disk which have no executables
|
||||
on `PATH` are non-active.
|
||||
|
||||
Run "dart help" to see global options.
|
||||
'''
|
||||
),
|
||||
(
|
||||
'uninstall',
|
||||
'''
|
||||
Remove a globally installed Dart CLI tool.
|
||||
|
||||
Completely deletes all installed versions of <package> and all executables from
|
||||
<package> placed on PATH.
|
||||
|
||||
Usage: dart uninstall <package>
|
||||
-h, --help Print this usage information.
|
||||
|
||||
Run "dart help" to see global options.
|
||||
'''
|
||||
),
|
||||
];
|
||||
for (final (command, helpMessage) in commandsHelpmessages) {
|
||||
skippableTest('dart $command --help', timeout: longTimeout, () async {
|
||||
final result = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
command,
|
||||
['--help'],
|
||||
null,
|
||||
{},
|
||||
);
|
||||
expect(result.stdout, contains(helpMessage));
|
||||
});
|
||||
}
|
||||
|
||||
final argumentss = [
|
||||
(
|
||||
null,
|
||||
[_packageForTest],
|
||||
),
|
||||
(
|
||||
null,
|
||||
[_packageForTest, _packageVersion],
|
||||
),
|
||||
(
|
||||
null,
|
||||
[_packageForTest, _packageVersion, '--hosted-url', 'https://pub.dev/'],
|
||||
),
|
||||
(
|
||||
null,
|
||||
[_packageDir.path],
|
||||
),
|
||||
(
|
||||
_sdkUri,
|
||||
[_packageRelativePath.path],
|
||||
),
|
||||
(
|
||||
_packageDir.uri,
|
||||
['.'],
|
||||
),
|
||||
];
|
||||
|
||||
for (final (workingDirectory, arguments) in argumentss) {
|
||||
var testName = arguments.join(' ');
|
||||
if (workingDirectory != null) {
|
||||
testName += ' in ${workingDirectory.toFilePath()}';
|
||||
}
|
||||
|
||||
skippableTest('dart install $testName', timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
arguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
);
|
||||
|
||||
await _runToolForTest(environment);
|
||||
|
||||
final installedResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'installed',
|
||||
[],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
final installedLines = installedResult.stdout.split('\n');
|
||||
expect(installedLines.where((e) => e.isNotEmpty).length, equals(1));
|
||||
final installedLine = installedLines.first;
|
||||
expect(
|
||||
installedLine,
|
||||
startsWith(_packageForTest),
|
||||
);
|
||||
if (arguments.contains(_packageVersion)) {
|
||||
expect(
|
||||
installedLine,
|
||||
equals('$_packageForTest $_packageVersion'),
|
||||
);
|
||||
}
|
||||
if (arguments.contains(_packageRelativePath.toString())) {
|
||||
expect(
|
||||
installedLine,
|
||||
stringContainsInOrder([_packageRelativePath.toString(), '" at 20']),
|
||||
);
|
||||
}
|
||||
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'uninstall',
|
||||
[_packageForTest],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final argumentssGit = [
|
||||
(
|
||||
null,
|
||||
[_gitHttpsUrl.toString(), '--git-path', _gitPath],
|
||||
),
|
||||
(
|
||||
null,
|
||||
[_gitHttpsUrl.toString(), '--git-path', _gitPath, '--git-ref', _gitRef],
|
||||
),
|
||||
];
|
||||
|
||||
for (final (workingDirectory, arguments) in argumentssGit) {
|
||||
var testName = arguments.join(' ');
|
||||
if (workingDirectory != null) {
|
||||
testName += ' in ${workingDirectory.toFilePath()}';
|
||||
}
|
||||
|
||||
skippableTest('dart install $testName', timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
arguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
);
|
||||
|
||||
final installedResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'installed',
|
||||
[],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
final installedLines = installedResult.stdout.split('\n');
|
||||
expect(installedLines.where((e) => e.isNotEmpty).length, equals(1));
|
||||
final installedLine = installedLines.first;
|
||||
expect(
|
||||
installedLine,
|
||||
startsWith(_gitPackageForTest),
|
||||
);
|
||||
expect(
|
||||
installedLine,
|
||||
contains(' from Git repository "${_gitHttpsUrl.toString()}"'),
|
||||
);
|
||||
if (arguments.contains('--git-ref')) {
|
||||
expect(
|
||||
installedLine,
|
||||
contains(' at "${_gitRef.substring(0, 8)}"'),
|
||||
);
|
||||
}
|
||||
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'uninstall',
|
||||
[_gitPackageForTest],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
skippableTest('dart install ~/.dart/install/bin/ not on PATH', () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
};
|
||||
|
||||
await inTempDir((tempUri) async {
|
||||
final installResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[_packageForTest],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
expect(
|
||||
installResult.stdout,
|
||||
stringContainsInOrder([
|
||||
'Warning: Dart installs executables into ',
|
||||
'which is not on your path.',
|
||||
"You can fix that by adding that directory to your system's ",
|
||||
'"Path" environment variable.',
|
||||
'A web search for "configure windows path" will show you how.',
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
installResult.stdout,
|
||||
stringContainsInOrder([
|
||||
'Warning: Dart installs executables into',
|
||||
'You can fix that by adding this to your shell\'s config file ',
|
||||
'export PATH="\$PATH":',
|
||||
]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart install dart_app (with build hooks and code assets)',
|
||||
timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
await nativeAssetsTest('dart_app', (dartAppUri) async {
|
||||
// Add a second executable.
|
||||
final entryPoint1 =
|
||||
File.fromUri(dartAppUri.resolve('bin/dart_app.dart'));
|
||||
final entryPoint2 =
|
||||
File.fromUri(dartAppUri.resolve('bin/dart_app_copy.dart'));
|
||||
final entryPoint1Contents = await entryPoint1.readAsString();
|
||||
final entryPoint2Contents = entryPoint1Contents.replaceAll('5', '42');
|
||||
await entryPoint2.writeAsString(entryPoint2Contents);
|
||||
final pubspecFile = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
|
||||
final pubspecOld =
|
||||
pubspecFile.readAsStringSync().replaceAll('\r\n', '\n');
|
||||
final pubspecNew = pubspecOld.replaceAll(
|
||||
'''executables:
|
||||
dart_app:'''
|
||||
.replaceAll('\r\n', '\n'),
|
||||
'''executables:
|
||||
dart_app:
|
||||
dart_app_copy:'''
|
||||
.replaceAll('\r\n', '\n'),
|
||||
);
|
||||
expect(pubspecNew, isNot(equals(pubspecOld)));
|
||||
pubspecFile.writeAsStringSync(pubspecNew);
|
||||
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
|
||||
for (final (tool, someInt) in [
|
||||
('dart_app', 5),
|
||||
('dart_app_copy', 42)
|
||||
]) {
|
||||
final toolResult = await runProcess(
|
||||
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
|
||||
// Windows so that invoking an executable without extension works.
|
||||
executable: Uri.file(tool),
|
||||
// Run in some unrelated directory ensuring PATH is picked up.
|
||||
workingDirectory: Directory.systemTemp.uri,
|
||||
logger: logger,
|
||||
environment: environment,
|
||||
);
|
||||
expect(
|
||||
toolResult.stdout,
|
||||
stringContainsInOrder([
|
||||
'add($someInt, 6) = ${someInt + 6}',
|
||||
'subtract($someInt, 6) = ${someInt - 6}',
|
||||
]),
|
||||
);
|
||||
expect(toolResult.exitCode, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart install --overwrite', timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
await nativeAssetsTest('dart_app', (dartAppUri) async {
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
|
||||
// Not overwriting, but the same package is fine.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
|
||||
final pubspecFile = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
|
||||
final pubspecContents = await pubspecFile.readAsString();
|
||||
final pubspecContentsNew =
|
||||
pubspecContents.replaceFirst('dart_app', 'a_different_name');
|
||||
await pubspecFile.writeAsString(pubspecContentsNew);
|
||||
|
||||
// Trying to install an executable with the same name from a different
|
||||
// package should fail.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
expectedExitCode: errorExitCode,
|
||||
);
|
||||
|
||||
// Overwriting is fine.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath(), '--overwrite'],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
|
||||
// Using --overwrite leads to inactive versions.
|
||||
// `dart installed --all` should also report the non-active versions.
|
||||
for (final all in [true, false]) {
|
||||
final installedResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'installed',
|
||||
[if (all) '--all'],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
final installedLines = installedResult.stdout
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
if (all) {
|
||||
expect(installedLines, hasLength(2));
|
||||
expect(
|
||||
installedLines,
|
||||
contains(startsWith('dart_app')),
|
||||
);
|
||||
} else {
|
||||
expect(installedLines, hasLength(1));
|
||||
expect(
|
||||
installedLines,
|
||||
isNot(contains(startsWith('dart_app'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart install check exit codes', timeout: longTimeout,
|
||||
() async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
const appName = 'test_app';
|
||||
final dartAppUri = tempUri.resolve('$appName/');
|
||||
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
|
||||
await pubspec.create(recursive: true);
|
||||
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
|
||||
name: appName,
|
||||
environment: EnvironmentSyntax(
|
||||
sdk: '^${Platform.version.split(' ').first}',
|
||||
),
|
||||
executables: {
|
||||
appName: appName,
|
||||
},
|
||||
).json));
|
||||
final mainFile = File.fromUri(dartAppUri.resolve('bin/$appName.dart'));
|
||||
await mainFile.create(recursive: true);
|
||||
mainFile.writeAsString('''
|
||||
import 'dart:io';
|
||||
|
||||
void main(List<String> args) {
|
||||
exit(int.parse(args.first));
|
||||
}
|
||||
''');
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
|
||||
const testExitCode = 55;
|
||||
final toolResult = await runProcess(
|
||||
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
|
||||
// Windows so that invoking an executable without extension works.
|
||||
executable: Uri.file(appName),
|
||||
// Run in some unrelated directory ensuring PATH is picked up.
|
||||
workingDirectory: Directory.systemTemp.uri,
|
||||
arguments: ['$testExitCode'],
|
||||
logger: logger,
|
||||
environment: environment,
|
||||
expectedExitCode: testExitCode,
|
||||
);
|
||||
expect(toolResult.exitCode, testExitCode);
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart install hooks user-defines and failures',
|
||||
timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
const packageName = 'test_app';
|
||||
final dartAppUri = tempUri.resolve('$packageName/');
|
||||
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
|
||||
await pubspec.create(recursive: true);
|
||||
final mainFile =
|
||||
File.fromUri(dartAppUri.resolve('bin/$packageName.dart'));
|
||||
await mainFile.create(recursive: true);
|
||||
mainFile.writeAsString('''
|
||||
void main(List<String> args) { }
|
||||
''');
|
||||
final buildHookFile = File.fromUri(dartAppUri.resolve('hook/build.dart'));
|
||||
await buildHookFile.create(recursive: true);
|
||||
buildHookFile.writeAsString('''
|
||||
import 'package:hooks/hooks.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
await build(args, (input, output) async {
|
||||
final myUserDefine = input.userDefines['my_user_define'];
|
||||
if (myUserDefine == null) {
|
||||
throw Exception('Expected a user define');
|
||||
}
|
||||
});
|
||||
}
|
||||
''');
|
||||
for (final addUserDefine in [true, false]) {
|
||||
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
|
||||
name: packageName,
|
||||
environment: EnvironmentSyntax(
|
||||
sdk: '^${Platform.version.split(' ').first}',
|
||||
),
|
||||
executables: {
|
||||
packageName: packageName,
|
||||
},
|
||||
dependencies: {
|
||||
'hooks': PathDependencySourceSyntax(
|
||||
path$: sdkRootUri
|
||||
.resolve('third_party/pkg/native/pkgs/hooks/')
|
||||
.toFilePath(),
|
||||
),
|
||||
},
|
||||
hooks: HooksSyntax(
|
||||
userDefines: {
|
||||
packageName: {
|
||||
if (addUserDefine) 'my_user_define': 'a_value,',
|
||||
},
|
||||
},
|
||||
),
|
||||
).json));
|
||||
final installResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[dartAppUri.toFilePath()],
|
||||
null,
|
||||
environment,
|
||||
expectedExitCode: addUserDefine ? 0 : errorExitCode,
|
||||
);
|
||||
if (addUserDefine) {
|
||||
expect(installResult.exitCode, equals(0));
|
||||
expect(installResult.stderr, isEmpty);
|
||||
} else {
|
||||
// Check that build hook failures are surfaced and that error messages
|
||||
// are visible.
|
||||
expect(installResult.exitCode, equals(errorExitCode));
|
||||
expect(installResult.stderr, contains('Expected a user define'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart install uninstalls old versions', timeout: longTimeout,
|
||||
() async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
// Install two versions.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
['.'],
|
||||
_packageDir.uri,
|
||||
environment,
|
||||
);
|
||||
final installResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'install',
|
||||
[_packageForTest, _packageVersion],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
expect(
|
||||
installResult.stdout,
|
||||
stringContainsInOrder(['Uninstalling ', _packageForTest]),
|
||||
);
|
||||
|
||||
// `--all` should also report the non-active versions.
|
||||
Future<List<String>> runInstalled() async {
|
||||
final installedResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'installed',
|
||||
['--all'],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
final installedLines = installedResult.stdout
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
return installedLines;
|
||||
}
|
||||
|
||||
expect(await runInstalled(), hasLength(1));
|
||||
|
||||
// `uninstall` uninstalls all versions.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'uninstall',
|
||||
[_packageForTest],
|
||||
null,
|
||||
environment,
|
||||
);
|
||||
expect(await runInstalled(), hasLength(0));
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart uninstall', timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
};
|
||||
|
||||
// `uninstall` should have a non-zero exit if nothing was uninstalled.
|
||||
await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'uninstall',
|
||||
[_packageForTest],
|
||||
null,
|
||||
environment,
|
||||
expectedExitCode: errorExitCode,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
skippableTest('dart uninstall while running', timeout: longTimeout, () async {
|
||||
await inTempDir((tempUri) async {
|
||||
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
|
||||
|
||||
final environment = {
|
||||
_dartDirectoryEnvKey: tempUri.toFilePath(),
|
||||
'PATH':
|
||||
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
|
||||
};
|
||||
|
||||
const packageName = 'test_app';
|
||||
final dartAppUri = tempUri.resolve('$packageName/');
|
||||
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
|
||||
await pubspec.create(recursive: true);
|
||||
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
|
||||
name: packageName,
|
||||
environment: EnvironmentSyntax(
|
||||
sdk: '^${Platform.version.split(' ').first}',
|
||||
),
|
||||
executables: {
|
||||
packageName: packageName,
|
||||
},
|
||||
).json));
|
||||
final mainFile =
|
||||
File.fromUri(dartAppUri.resolve('bin/$packageName.dart'));
|
||||
await mainFile.create(recursive: true);
|
||||
mainFile.writeAsString('''
|
||||
void main(List<String> args) async {
|
||||
await Future.delayed(Duration(days: 1000000));
|
||||
}
|
||||
''');
|
||||
Future<RunProcessResult> doInstall(int expectedExitCode) async {
|
||||
return await _runDartdev(fromDartdevSource, 'install',
|
||||
[dartAppUri.toFilePath()], null, environment,
|
||||
expectedExitCode: expectedExitCode);
|
||||
}
|
||||
|
||||
await doInstall(0);
|
||||
|
||||
final runningProcess = await Process.start(
|
||||
packageName,
|
||||
[],
|
||||
environment: environment,
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
final installWhileRunningResult = await doInstall(
|
||||
Platform.isWindows ? errorExitCode : 0,
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
expect(
|
||||
installWhileRunningResult.stderr,
|
||||
contains('The application might be in use.'),
|
||||
);
|
||||
} else {
|
||||
expect(installWhileRunningResult.stderr, isEmpty);
|
||||
}
|
||||
|
||||
final uninstallWhileRunningResult = await _runDartdev(
|
||||
fromDartdevSource,
|
||||
'uninstall',
|
||||
[packageName],
|
||||
null,
|
||||
environment,
|
||||
expectedExitCode: Platform.isWindows ? errorExitCode : 0,
|
||||
);
|
||||
if (Platform.isWindows) {
|
||||
expect(
|
||||
uninstallWhileRunningResult.stderr,
|
||||
contains('The application might be in use.'),
|
||||
);
|
||||
} else {
|
||||
expect(uninstallWhileRunningResult.stderr, isEmpty);
|
||||
}
|
||||
|
||||
runningProcess.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<RunProcessResult> _runDartdev(
|
||||
bool fromDartdevSource,
|
||||
String command,
|
||||
List<String> arguments,
|
||||
Uri? workingDirectory,
|
||||
Map<String, String> environment, {
|
||||
int expectedExitCode = 0,
|
||||
}) async {
|
||||
final installResult = await runDart(
|
||||
arguments: [
|
||||
if (fromDartdevSource) _dartDevEntryScriptUri.toFilePath(),
|
||||
command,
|
||||
...arguments,
|
||||
],
|
||||
workingDirectory: workingDirectory,
|
||||
logger: logger,
|
||||
environment: environment,
|
||||
expectExitCodeZero: false,
|
||||
);
|
||||
expect(installResult.exitCode, equals(expectedExitCode));
|
||||
return installResult;
|
||||
}
|
||||
|
||||
/// Runs [_cliToolForTest] and expects the help message.
|
||||
Future<RunProcessResult> _runToolForTest(
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final toolResult = await runProcess(
|
||||
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
|
||||
// Windows so that invoking an executable without extension works.
|
||||
executable: Uri.file(_cliToolForTest),
|
||||
arguments: ['--help'],
|
||||
// Run in some unrelated directory ensuring PATH is picked up.
|
||||
workingDirectory: Directory.systemTemp.uri,
|
||||
logger: logger,
|
||||
environment: environment,
|
||||
);
|
||||
expect(
|
||||
toolResult.stdout,
|
||||
stringContainsInOrder([
|
||||
'Tools for binary size analysis of Dart VM AOT snapshots.',
|
||||
]),
|
||||
);
|
||||
expect(toolResult.exitCode, 0);
|
||||
return toolResult;
|
||||
}
|
||||
@@ -224,6 +224,8 @@ dependency_overrides:
|
||||
path: third_party/pkg/protobuf/protoc_plugin
|
||||
pub:
|
||||
path: third_party/pkg/pub
|
||||
pub_formats:
|
||||
path: third_party/pkg/native/pkgs/pub_formats
|
||||
pub_semver:
|
||||
path: third_party/pkg/tools/pkgs/pub_semver
|
||||
regression_tests:
|
||||
|
||||
Reference in New Issue
Block a user