|  | // Copyright (c) 2014, 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:async'; | 
|  | import 'dart:io'; | 
|  |  | 
|  | import 'package:path/path.dart' as p; | 
|  | import 'package:pub_semver/pub_semver.dart'; | 
|  |  | 
|  | import 'command_runner.dart'; | 
|  | import 'entrypoint.dart'; | 
|  | import 'exceptions.dart'; | 
|  | import 'executable.dart' as exec; | 
|  | import 'io.dart'; | 
|  | import 'lock_file.dart'; | 
|  | import 'log.dart' as log; | 
|  | import 'package.dart'; | 
|  | import 'package_name.dart'; | 
|  | import 'pub_embeddable_command.dart'; | 
|  | import 'pubspec.dart'; | 
|  | import 'sdk.dart'; | 
|  | import 'sdk/dart.dart'; | 
|  | import 'solver.dart'; | 
|  | import 'solver/incompatibility_cause.dart'; | 
|  | import 'solver/report.dart'; | 
|  | import 'source/cached.dart'; | 
|  | import 'source/git.dart'; | 
|  | import 'source/hosted.dart'; | 
|  | import 'source/path.dart'; | 
|  | import 'system_cache.dart'; | 
|  | import 'utils.dart'; | 
|  |  | 
|  | /// Maintains the set of packages that have been globally activated. | 
|  | /// | 
|  | /// These have been hand-chosen by the user to make their executables in bin/ | 
|  | /// available to the entire system. This lets them access them even when the | 
|  | /// current working directory is not inside another entrypoint package. | 
|  | /// | 
|  | /// Only one version of a given package name can be globally activated at a | 
|  | /// time. Activating a different version of a package will deactivate the | 
|  | /// previous one. | 
|  | /// | 
|  | /// This handles packages from uncached and cached sources a little differently. | 
|  | /// For a cached source, the package is physically in the user's pub cache and | 
|  | /// we don't want to mess with it by putting a lockfile in there. Instead, when | 
|  | /// we activate the package, we create a full lockfile and put it in the | 
|  | /// "global_packages" directory. It's named "<package>.lock". Unlike a normal | 
|  | /// lockfile, it also contains an entry for the root package itself, so that we | 
|  | /// know the version and description that was activated. | 
|  | /// | 
|  | /// Uncached packages (i.e. "path" packages) are somewhere else on the user's | 
|  | /// local file system and can have a lockfile directly in place. (And, in fact, | 
|  | /// we want to ensure we honor the user's lockfile there.) To activate it, we | 
|  | /// just need to know where that package directory is. For that, we create a | 
|  | /// lockfile that *only* contains the root package's [PackageId] -- basically | 
|  | /// just the path to the directory where the real lockfile lives. | 
|  | class GlobalPackages { | 
|  | /// The [SystemCache] containing the global packages. | 
|  | final SystemCache cache; | 
|  |  | 
|  | /// The directory where the lockfiles for activated packages are stored. | 
|  | String get _directory => p.join(cache.rootDir, 'global_packages'); | 
|  |  | 
|  | String _packageDir(String packageName) => p.join(_directory, packageName); | 
|  |  | 
|  | /// The directory where binstubs for global package executables are stored. | 
|  | String get _binStubDir => p.join(cache.rootDir, 'bin'); | 
|  |  | 
|  | /// Creates a new global package registry backed by the given directory on | 
|  | /// the user's file system. | 
|  | /// | 
|  | /// The directory may not physically exist yet. If not, this will create it | 
|  | /// when needed. | 
|  | GlobalPackages(this.cache); | 
|  |  | 
|  | /// Caches the package located in the Git repository [repo] and makes it the | 
|  | /// active global version. | 
|  | /// | 
|  | /// [executables] is the names of the executables that should have binstubs. | 
|  | /// If `null`, all executables in the package will get binstubs. If empty, no | 
|  | /// binstubs will be created. | 
|  | /// | 
|  | /// The [features] map controls which features of the package to activate. | 
|  | /// | 
|  | /// If [overwriteBinStubs] is `true`, any binstubs that collide with | 
|  | /// existing binstubs in other packages will be overwritten by this one's. | 
|  | /// Otherwise, the previous ones will be preserved. | 
|  | Future<void> activateGit( | 
|  | String repo, | 
|  | List<String>? executables, { | 
|  | required bool overwriteBinStubs, | 
|  | String? path, | 
|  | String? ref, | 
|  | }) async { | 
|  | var name = await cache.git.getPackageNameFromRepo( | 
|  | repo, | 
|  | ref, | 
|  | path, | 
|  | cache, | 
|  | relativeTo: p.current, | 
|  | ); | 
|  |  | 
|  | // TODO(nweiz): Add some special handling for git repos that contain path | 
|  | // dependencies. Their executables shouldn't be cached, and there should | 
|  | // be a mechanism for redoing dependency resolution if a path pubspec has | 
|  | // changed (see also issue 20499). | 
|  | PackageRef packageRef; | 
|  | try { | 
|  | packageRef = cache.git.parseRef( | 
|  | name, | 
|  | { | 
|  | 'url': repo, | 
|  | if (path != null) 'path': path, | 
|  | if (ref != null) 'ref': ref, | 
|  | }, | 
|  | containingDir: '.', | 
|  | ); | 
|  | } on FormatException catch (e) { | 
|  | throw ApplicationException(e.message); | 
|  | } | 
|  | await _installInCache( | 
|  | packageRef.withConstraint(VersionConstraint.any), | 
|  | executables, | 
|  | overwriteBinStubs: overwriteBinStubs, | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Finds the latest version of the hosted package with [name] that matches | 
|  | /// [constraint] and makes it the active global version. | 
|  | /// | 
|  | /// [executables] is the names of the executables that should have binstubs. | 
|  | /// If `null`, all executables in the package will get binstubs. If empty, no | 
|  | /// binstubs will be created. | 
|  | /// | 
|  | /// if [overwriteBinStubs] is `true`, any binstubs that collide with | 
|  | /// existing binstubs in other packages will be overwritten by this one's. | 
|  | /// Otherwise, the previous ones will be preserved. | 
|  | /// | 
|  | /// [url] is an optional custom pub server URL. If not null, the package to be | 
|  | /// activated will be fetched from this URL instead of the default pub URL. | 
|  | Future<void> activateHosted( | 
|  | PackageRange range, | 
|  | List<String>? executables, { | 
|  | required bool overwriteBinStubs, | 
|  | String? url, | 
|  | }) async { | 
|  | await _installInCache( | 
|  | range, | 
|  | executables, | 
|  | overwriteBinStubs: overwriteBinStubs, | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Makes the local package at [path] globally active. | 
|  | /// | 
|  | /// [executables] is the names of the executables that should have binstubs. | 
|  | /// If `null`, all executables in the package will get binstubs. If empty, no | 
|  | /// binstubs will be created. | 
|  | /// | 
|  | /// if [overwriteBinStubs] is `true`, any binstubs that collide with | 
|  | /// existing binstubs in other packages will be overwritten by this one's. | 
|  | /// Otherwise, the previous ones will be preserved. | 
|  | Future<void> activatePath( | 
|  | String path, | 
|  | List<String>? executables, { | 
|  | required bool overwriteBinStubs, | 
|  | required PubAnalytics? analytics, | 
|  | }) async { | 
|  | var entrypoint = Entrypoint(path, cache); | 
|  |  | 
|  | // Get the package's dependencies. | 
|  | await entrypoint.acquireDependencies( | 
|  | SolveType.get, | 
|  | analytics: analytics, | 
|  | ); | 
|  | var name = entrypoint.root.name; | 
|  | _describeActive(name, cache); | 
|  |  | 
|  | // Write a lockfile that points to the local package. | 
|  | var fullPath = canonicalize(entrypoint.rootDir); | 
|  | var id = cache.path.idFor( | 
|  | name, | 
|  | entrypoint.root.version, | 
|  | fullPath, | 
|  | p.current, | 
|  | ); | 
|  |  | 
|  | final tempDir = cache.createTempDir(); | 
|  | // TODO(rnystrom): Look in "bin" and display list of binaries that | 
|  | // user can run. | 
|  | LockFile([id]).writeToFile(p.join(tempDir, 'pubspec.lock'), cache); | 
|  |  | 
|  | tryDeleteEntry(_packageDir(name)); | 
|  | tryRenameDir(tempDir, _packageDir(name)); | 
|  |  | 
|  | _updateBinStubs( | 
|  | entrypoint, | 
|  | entrypoint.root, | 
|  | executables, | 
|  | overwriteBinStubs: overwriteBinStubs, | 
|  | ); | 
|  | log.message('Activated ${_formatPackage(id)}.'); | 
|  | } | 
|  |  | 
|  | /// Installs the package [dep] and its dependencies into the system cache. | 
|  | /// | 
|  | /// If [silent] less logging will be printed. | 
|  | Future<void> _installInCache( | 
|  | PackageRange dep, | 
|  | List<String>? executables, { | 
|  | required bool overwriteBinStubs, | 
|  | bool silent = false, | 
|  | }) async { | 
|  | final name = dep.name; | 
|  | LockFile? originalLockFile = _describeActive(name, cache); | 
|  |  | 
|  | // Create a dummy package with just [dep] so we can do resolution on it. | 
|  | var root = Package.inMemory( | 
|  | Pubspec( | 
|  | 'pub global activate', | 
|  | dependencies: [dep], | 
|  | sources: cache.sources, | 
|  | ), | 
|  | ); | 
|  |  | 
|  | // Resolve it and download its dependencies. | 
|  | SolveResult result; | 
|  | try { | 
|  | result = await log.spinner( | 
|  | 'Resolving dependencies', | 
|  | () => resolveVersions(SolveType.get, cache, root), | 
|  | condition: !silent, | 
|  | ); | 
|  | } on SolveFailure catch (error) { | 
|  | for (var incompatibility | 
|  | in error.incompatibility.externalIncompatibilities) { | 
|  | if (incompatibility.cause != IncompatibilityCause.noVersions) continue; | 
|  | if (incompatibility.terms.single.package.name != name) continue; | 
|  | // If the SolveFailure is caused by [dep] not | 
|  | // being available, report that as a [dataError]. | 
|  | dataError(error.toString()); | 
|  | } | 
|  | rethrow; | 
|  | } | 
|  | // We want the entrypoint to be rooted at 'dep' not the dummy-package. | 
|  | result.packages.removeWhere((id) => id.name == 'pub global activate'); | 
|  |  | 
|  | final lockFile = await result.downloadCachedPackages(cache); | 
|  | final sameVersions = | 
|  | originalLockFile != null && originalLockFile.samePackageIds(lockFile); | 
|  |  | 
|  | final PackageId id = lockFile.packages[name]!; | 
|  | if (sameVersions) { | 
|  | log.message(''' | 
|  | The package $name is already activated at newest available version. | 
|  | To recompile executables, first run `$topLevelProgram pub global deactivate $name`. | 
|  | '''); | 
|  | } else { | 
|  | // Only precompile binaries if we have a new resolution. | 
|  | if (!silent) { | 
|  | await SolveReport( | 
|  | SolveType.get, | 
|  | null, | 
|  | root.pubspec, | 
|  | originalLockFile ?? LockFile.empty(), | 
|  | lockFile, | 
|  | result.availableVersions, | 
|  | cache, | 
|  | dryRun: false, | 
|  | quiet: false, | 
|  | enforceLockfile: false, | 
|  | ).show(); | 
|  | } | 
|  |  | 
|  | final tempDir = cache.createTempDir(); | 
|  | lockFile.writeToFile(p.join(tempDir, 'pubspec.lock'), cache); | 
|  |  | 
|  | // Load the package graph from [result] so we don't need to re-parse all | 
|  | // the pubspecs. | 
|  | final entrypoint = Entrypoint.global( | 
|  | tempDir, | 
|  | cache.loadCached(id), | 
|  | lockFile, | 
|  | cache, | 
|  | solveResult: result, | 
|  | ); | 
|  |  | 
|  | await entrypoint.writePackageConfigFile(); | 
|  |  | 
|  | await entrypoint.precompileExecutables(); | 
|  |  | 
|  | tryDeleteEntry(_packageDir(name)); | 
|  | tryRenameDir(tempDir, _packageDir(name)); | 
|  | } | 
|  |  | 
|  | final entrypoint = Entrypoint.global( | 
|  | _packageDir(id.name), | 
|  | cache.loadCached(id), | 
|  | lockFile, | 
|  | cache, | 
|  | solveResult: result, | 
|  | ); | 
|  | _updateBinStubs( | 
|  | entrypoint, | 
|  | cache.load(entrypoint.lockFile.packages[dep.name]!), | 
|  | executables, | 
|  | overwriteBinStubs: overwriteBinStubs, | 
|  | ); | 
|  | if (!silent) log.message('Activated ${_formatPackage(id)}.'); | 
|  | } | 
|  |  | 
|  | /// Shows the user the currently active package with [name], if any. | 
|  | LockFile? _describeActive(String name, SystemCache cache) { | 
|  | late final LockFile lockFile; | 
|  | try { | 
|  | lockFile = LockFile.load(_getLockFilePath(name), cache.sources); | 
|  | } on IOException { | 
|  | // Couldn't read the lock file. It probably doesn't exist. | 
|  | return null; | 
|  | } | 
|  | var id = lockFile.packages[name]!; | 
|  | final description = id.description.description; | 
|  |  | 
|  | if (description is GitDescription) { | 
|  | log.message('Package ${log.bold(name)} is currently active from Git ' | 
|  | 'repository "${GitDescription.prettyUri(description.url)}".'); | 
|  | } else if (description is PathDescription) { | 
|  | log.message('Package ${log.bold(name)} is currently active at path ' | 
|  | '"${description.path}".'); | 
|  | } else { | 
|  | log.message('Package ${log.bold(name)} is currently active at version ' | 
|  | '${log.bold(id.version)}.'); | 
|  | } | 
|  | return lockFile; | 
|  | } | 
|  |  | 
|  | /// Deactivates a previously-activated package named [name]. | 
|  | /// | 
|  | /// Returns `false` if no package with [name] was currently active. | 
|  | bool deactivate(String name) { | 
|  | var dir = p.join(_directory, name); | 
|  | if (!dirExists(_directory)) { | 
|  | return false; | 
|  | } | 
|  | // By listing all files instead of using only `dirExists` this check will | 
|  | // work on case-preserving file-systems. | 
|  | final files = listDir(_directory); | 
|  | if (!files.contains(dir)) { | 
|  | return false; | 
|  | } | 
|  | if (!dirExists(dir)) { | 
|  | // This can happen if `dir` was really a file. | 
|  | return false; | 
|  | } | 
|  |  | 
|  | _deleteBinStubs(name); | 
|  |  | 
|  | var lockFile = LockFile.load(_getLockFilePath(name), cache.sources); | 
|  | var id = lockFile.packages[name]!; | 
|  | log.message('Deactivated package ${_formatPackage(id)}.'); | 
|  |  | 
|  | deleteEntry(dir); | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /// Finds the active package with [name]. | 
|  | /// | 
|  | /// Returns an [Entrypoint] loaded with the active package if found. | 
|  | Future<Entrypoint> find(String name) async { | 
|  | var lockFilePath = _getLockFilePath(name); | 
|  | late LockFile lockFile; | 
|  | try { | 
|  | lockFile = LockFile.load(lockFilePath, cache.sources); | 
|  | } on IOException { | 
|  | // If we couldn't read the lock file, it's not activated. | 
|  | dataError('No active package ${log.bold(name)}.'); | 
|  | } | 
|  |  | 
|  | // Remove the package itself from the lockfile. We put it in there so we | 
|  | // could find and load the [Package] object, but normally an entrypoint | 
|  | // doesn't expect to be in its own lockfile. | 
|  | var id = lockFile.packages[name]!; | 
|  | lockFile = lockFile.removePackage(name); | 
|  |  | 
|  | Entrypoint entrypoint; | 
|  | if (id.source is CachedSource) { | 
|  | // For cached sources, the package itself is in the cache and the | 
|  | // lockfile is the one we just loaded. | 
|  | entrypoint = Entrypoint.global( | 
|  | _packageDir(id.name), | 
|  | cache.loadCached(id), | 
|  | lockFile, | 
|  | cache, | 
|  | ); | 
|  | } else { | 
|  | // For uncached sources (i.e. path), the ID just points to the real | 
|  | // directory for the package. | 
|  | entrypoint = Entrypoint( | 
|  | (id.description.description as PathDescription).path, | 
|  | cache, | 
|  | ); | 
|  | } | 
|  |  | 
|  | entrypoint.root.pubspec.sdkConstraints.forEach((sdkName, constraint) { | 
|  | var sdk = sdks[sdkName]; | 
|  | if (sdk == null) { | 
|  | dataError('${log.bold(name)} ${entrypoint.root.version} requires ' | 
|  | 'unknown SDK "$name".'); | 
|  | } else if (sdkName == 'dart') { | 
|  | if (constraint.effectiveConstraint.allows((sdk as DartSdk).version)) { | 
|  | return; | 
|  | } | 
|  | dataError("${log.bold(name)} ${entrypoint.root.version} doesn't " | 
|  | 'support Dart ${sdk.version}.'); | 
|  | } else { | 
|  | dataError('${log.bold(name)} ${entrypoint.root.version} requires the ' | 
|  | '${sdk.name} SDK, which is unsupported for global executables.'); | 
|  | } | 
|  | }); | 
|  |  | 
|  | // Check that the SDK constraints the lockFile says we have are honored. | 
|  | lockFile.sdkConstraints.forEach((sdkName, constraint) { | 
|  | var sdk = sdks[sdkName]; | 
|  | if (sdk == null) { | 
|  | dataError('${log.bold(name)} as globally activated requires ' | 
|  | 'unknown SDK "$name".'); | 
|  | } else if (sdkName == 'dart') { | 
|  | if (constraint.allows((sdk as DartSdk).version)) return; | 
|  | dataError("${log.bold(name)} as globally activated doesn't " | 
|  | 'support Dart ${sdk.version}, try: $topLevelProgram pub global activate $name'); | 
|  | } else { | 
|  | dataError('${log.bold(name)} as globally activated requires the ' | 
|  | '${sdk.name} SDK, which is unsupported for global executables.'); | 
|  | } | 
|  | }); | 
|  |  | 
|  | return entrypoint; | 
|  | } | 
|  |  | 
|  | /// Runs [package]'s [executable] with [args]. | 
|  | /// | 
|  | /// If [executable] is available in its built form, that will be | 
|  | /// recompiled if the SDK has been upgraded since it was first compiled and | 
|  | /// then run. Otherwise, it will be run from source. | 
|  | /// | 
|  | /// If [enableAsserts] is true, the program is run with assertions enabled. | 
|  | /// | 
|  | /// Returns the exit code from the executable. | 
|  | Future<int> runExecutable( | 
|  | Entrypoint entrypoint, | 
|  | exec.Executable executable, | 
|  | List<String> args, { | 
|  | bool enableAsserts = false, | 
|  | required Future<void> Function(exec.Executable) recompile, | 
|  | List<String> vmArgs = const [], | 
|  | required bool alwaysUseSubprocess, | 
|  | }) async { | 
|  | return await exec.runExecutable( | 
|  | entrypoint, | 
|  | executable, | 
|  | args, | 
|  | enableAsserts: enableAsserts, | 
|  | recompile: (exectuable) async { | 
|  | await recompile(exectuable); | 
|  | _refreshBinStubs(entrypoint, executable); | 
|  | }, | 
|  | vmArgs: vmArgs, | 
|  | alwaysUseSubprocess: alwaysUseSubprocess, | 
|  | ); | 
|  | } | 
|  |  | 
|  | /// Gets the path to the lock file for an activated cached package with | 
|  | /// [name]. | 
|  | String _getLockFilePath(String name) => | 
|  | p.join(_directory, name, 'pubspec.lock'); | 
|  |  | 
|  | /// Shows the user a formatted list of globally activated packages. | 
|  | void listActivePackages() { | 
|  | if (!dirExists(_directory)) return; | 
|  |  | 
|  | listDir(_directory).map(_loadPackageId).toList() | 
|  | ..sort((id1, id2) => id1.name.compareTo(id2.name)) | 
|  | ..forEach((id) => log.message(_formatPackage(id))); | 
|  | } | 
|  |  | 
|  | /// Returns the [PackageId] for the globally-activated package at [path]. | 
|  | /// | 
|  | /// [path] should be a path within [_directory]. It can either be an old-style | 
|  | /// path to a single lockfile or a new-style path to a directory containing a | 
|  | /// lockfile. | 
|  | PackageId _loadPackageId(String path) { | 
|  | var name = p.basenameWithoutExtension(path); | 
|  | if (!fileExists(path)) path = p.join(path, 'pubspec.lock'); | 
|  |  | 
|  | var id = | 
|  | LockFile.load(p.join(_directory, path), cache.sources).packages[name]; | 
|  |  | 
|  | if (id == null) { | 
|  | throw FormatException("Pubspec for activated package $name didn't " | 
|  | 'contain an entry for itself.'); | 
|  | } | 
|  |  | 
|  | return id; | 
|  | } | 
|  |  | 
|  | /// Returns formatted string representing the package [id]. | 
|  | String _formatPackage(PackageId id) { | 
|  | final description = id.description.description; | 
|  | if (description is GitDescription) { | 
|  | var url = GitDescription.prettyUri(description.url); | 
|  | return '${log.bold(id.name)} ${id.version} from Git repository "$url"'; | 
|  | } else if (description is PathDescription) { | 
|  | var path = description.path; | 
|  | return '${log.bold(id.name)} ${id.version} at path "$path"'; | 
|  | } else { | 
|  | return '${log.bold(id.name)} ${id.version}'; | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Repairs any corrupted globally-activated packages and their binstubs. | 
|  | /// | 
|  | /// Returns a pair of two lists of strings. The first indicates which packages | 
|  | /// were successfully re-activated; the second indicates which failed. | 
|  | Future<Pair<List<String>, List<String>>> repairActivatedPackages() async { | 
|  | var executables = <String, List<String>>{}; | 
|  | if (dirExists(_binStubDir)) { | 
|  | for (var entry in listDir(_binStubDir)) { | 
|  | try { | 
|  | var binstub = readTextFile(entry); | 
|  | var package = _binStubProperty(binstub, 'Package'); | 
|  | if (package == null) { | 
|  | throw ApplicationException("No 'Package' property."); | 
|  | } | 
|  |  | 
|  | var executable = _binStubProperty(binstub, 'Executable'); | 
|  | if (executable == null) { | 
|  | throw ApplicationException("No 'Executable' property."); | 
|  | } | 
|  |  | 
|  | executables.putIfAbsent(package, () => []).add(executable); | 
|  | } catch (error, stackTrace) { | 
|  | log.error( | 
|  | 'Error reading binstub for ' | 
|  | '"${p.basenameWithoutExtension(entry)}"', | 
|  | error, | 
|  | stackTrace, | 
|  | ); | 
|  |  | 
|  | tryDeleteEntry(entry); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | var successes = <String>[]; | 
|  | var failures = <String>[]; | 
|  | if (dirExists(_directory)) { | 
|  | for (var entry in listDir(_directory)) { | 
|  | PackageId? id; | 
|  | try { | 
|  | id = _loadPackageId(entry); | 
|  | log.message('Reactivating ${log.bold(id.name)} ${id.version}...'); | 
|  |  | 
|  | var entrypoint = await find(id.name); | 
|  | final packageExecutables = executables.remove(id.name) ?? []; | 
|  |  | 
|  | if (entrypoint.isCached) { | 
|  | deleteEntry(entrypoint.globalDir!); | 
|  | await _installInCache( | 
|  | id.toRange(), | 
|  | packageExecutables, | 
|  | overwriteBinStubs: true, | 
|  | silent: true, | 
|  | ); | 
|  | } else { | 
|  | await activatePath( | 
|  | entrypoint.rootDir, | 
|  | packageExecutables, | 
|  | overwriteBinStubs: true, | 
|  | analytics: null, | 
|  | ); | 
|  | } | 
|  | successes.add(id.name); | 
|  | } catch (error, stackTrace) { | 
|  | var message = 'Failed to reactivate ' | 
|  | '${log.bold(p.basenameWithoutExtension(entry))}'; | 
|  | if (id != null) { | 
|  | message += ' ${id.version}'; | 
|  | if (id.source is! HostedSource) message += ' from ${id.source}'; | 
|  | } | 
|  |  | 
|  | log.error(message, error, stackTrace); | 
|  | failures.add(p.basenameWithoutExtension(entry)); | 
|  |  | 
|  | tryDeleteEntry(entry); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (executables.isNotEmpty) { | 
|  | var message = StringBuffer('Binstubs exist for non-activated ' | 
|  | 'packages:\n'); | 
|  | executables.forEach((package, executableNames) { | 
|  | for (var executable in executableNames) { | 
|  | deleteEntry(p.join(_binStubDir, executable)); | 
|  | } | 
|  |  | 
|  | message.writeln('  From ${log.bold(package)}: ' | 
|  | '${toSentence(executableNames)}'); | 
|  | }); | 
|  | log.error(message); | 
|  | } | 
|  |  | 
|  | return Pair(successes, failures); | 
|  | } | 
|  |  | 
|  | /// Rewrites all binstubs that refer to [executable] of [entrypoint]. | 
|  | /// | 
|  | /// This is meant to be called after a recompile due to eg. outdated | 
|  | /// snapshots. | 
|  | void _refreshBinStubs(Entrypoint entrypoint, exec.Executable executable) { | 
|  | if (!dirExists(_binStubDir)) return; | 
|  | for (var file in listDir(_binStubDir, includeDirs: false)) { | 
|  | var contents = readTextFile(file); | 
|  | var binStubPackage = _binStubProperty(contents, 'Package'); | 
|  | var binStubScript = _binStubProperty(contents, 'Script'); | 
|  | if (binStubPackage == null || binStubScript == null) { | 
|  | log.fine('Could not parse binstub $file:\n$contents'); | 
|  | continue; | 
|  | } | 
|  | if (binStubPackage == entrypoint.root.name && | 
|  | binStubScript == | 
|  | p.basenameWithoutExtension(executable.relativePath)) { | 
|  | log.fine('Replacing old binstub $file'); | 
|  | deleteEntry(file); | 
|  | _createBinStub( | 
|  | entrypoint.root, | 
|  | p.basenameWithoutExtension(file), | 
|  | binStubScript, | 
|  | overwrite: true, | 
|  | snapshot: entrypoint.pathOfExecutable(executable), | 
|  | ); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Updates the binstubs for [package]. | 
|  | /// | 
|  | /// A binstub is a little shell script in `PUB_CACHE/bin` that runs an | 
|  | /// executable from a globally activated package. This removes any old | 
|  | /// binstubs from the previously activated version of the package and | 
|  | /// (optionally) creates new ones for the executables listed in the package's | 
|  | /// pubspec. | 
|  | /// | 
|  | /// [executables] is the names of the executables that should have binstubs. | 
|  | /// If `null`, all executables in the package will get binstubs. If empty, no | 
|  | /// binstubs will be created. | 
|  | /// | 
|  | /// If [overwriteBinStubs] is `true`, any binstubs that collide with | 
|  | /// existing binstubs in other packages will be overwritten by this one's. | 
|  | /// Otherwise, the previous ones will be preserved. | 
|  | /// | 
|  | /// If [suggestIfNotOnPath] is `true` (the default), this will warn the user if | 
|  | /// the bin directory isn't on their path. | 
|  | void _updateBinStubs( | 
|  | Entrypoint entrypoint, | 
|  | Package package, | 
|  | List<String>? executables, { | 
|  | required bool overwriteBinStubs, | 
|  | bool suggestIfNotOnPath = true, | 
|  | }) { | 
|  | // Remove any previously activated binstubs for this package, in case the | 
|  | // list of executables has changed. | 
|  | _deleteBinStubs(package.name); | 
|  |  | 
|  | if ((executables != null && executables.isEmpty) || | 
|  | package.pubspec.executables.isEmpty) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | ensureDir(_binStubDir); | 
|  |  | 
|  | var installed = <String>[]; | 
|  | var collided = <String, String>{}; | 
|  | var allExecutables = ordered(package.pubspec.executables.keys); | 
|  | for (var executable in allExecutables) { | 
|  | if (executables != null && !executables.contains(executable)) continue; | 
|  |  | 
|  | var script = package.pubspec.executables[executable]!; | 
|  |  | 
|  | var previousPackage = _createBinStub( | 
|  | package, | 
|  | executable, | 
|  | script, | 
|  | overwrite: overwriteBinStubs, | 
|  | snapshot: entrypoint.pathOfExecutable( | 
|  | exec.Executable.adaptProgramName(package.name, script), | 
|  | ), | 
|  | ); | 
|  | if (previousPackage != null) { | 
|  | collided[executable] = previousPackage; | 
|  |  | 
|  | if (!overwriteBinStubs) continue; | 
|  | } | 
|  |  | 
|  | installed.add(executable); | 
|  | } | 
|  |  | 
|  | if (installed.isNotEmpty) { | 
|  | var names = namedSequence('executable', installed.map(log.bold)); | 
|  | log.message('Installed $names.'); | 
|  | } | 
|  |  | 
|  | // Show errors for any collisions. | 
|  | if (collided.isNotEmpty) { | 
|  | for (var command in ordered(collided.keys)) { | 
|  | if (overwriteBinStubs) { | 
|  | log.warning('Replaced ${log.bold(command)} previously installed from ' | 
|  | '${log.bold(collided[command])}.'); | 
|  | } else { | 
|  | log.warning('Executable ${log.bold(command)} was already installed ' | 
|  | 'from ${log.bold(collided[command])}.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!overwriteBinStubs) { | 
|  | log.warning('Deactivate the other package(s) or activate ' | 
|  | '${log.bold(package.name)} using --overwrite.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Show errors for any unknown executables. | 
|  | if (executables != null) { | 
|  | var unknown = ordered( | 
|  | executables | 
|  | .where((exe) => !package.pubspec.executables.keys.contains(exe)), | 
|  | ); | 
|  | if (unknown.isNotEmpty) { | 
|  | dataError("Unknown ${namedSequence('executable', unknown)}."); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Show errors for any missing scripts. | 
|  | // TODO(rnystrom): This can print false positives since a script may be | 
|  | // produced by a transformer. Do something better. | 
|  | var binFiles = package.executablePaths; | 
|  | for (var executable in installed) { | 
|  | var script = package.pubspec.executables[executable]; | 
|  | var scriptPath = p.join('bin', '$script.dart'); | 
|  | if (!binFiles.contains(scriptPath)) { | 
|  | log.warning('Warning: Executable "$executable" runs "$scriptPath", ' | 
|  | 'which was not found in ${log.bold(package.name)}.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (suggestIfNotOnPath && installed.isNotEmpty) { | 
|  | _suggestIfNotOnPath(installed.first); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Creates a binstub named [executable] that runs [script] from [package]. | 
|  | /// | 
|  | /// If [overwrite] is `true`, this will replace an existing binstub with that | 
|  | /// name for another package. | 
|  | /// | 
|  | /// [snapshot] is a path to a snapshot file. If that snapshot exists the | 
|  | /// binstub will invoke that directly. Otherwise, it will run | 
|  | /// `pub global run`. | 
|  | /// | 
|  | /// If a collision occurs, returns the name of the package that owns the | 
|  | /// existing binstub. Otherwise returns `null`. | 
|  | String? _createBinStub( | 
|  | Package package, | 
|  | String executable, | 
|  | String script, { | 
|  | required bool overwrite, | 
|  | required String snapshot, | 
|  | }) { | 
|  | var binStubPath = p.join(_binStubDir, executable); | 
|  | if (Platform.isWindows) binStubPath += '.bat'; | 
|  |  | 
|  | // See if the binstub already exists. If so, it's for another package | 
|  | // since we already deleted all of this package's binstubs. | 
|  | String? previousPackage; | 
|  | if (fileExists(binStubPath)) { | 
|  | var contents = readTextFile(binStubPath); | 
|  | previousPackage = _binStubProperty(contents, 'Package'); | 
|  | if (previousPackage == null) { | 
|  | log.fine('Could not parse binstub $binStubPath:\n$contents'); | 
|  | } else if (!overwrite) { | 
|  | return previousPackage; | 
|  | } | 
|  | } | 
|  |  | 
|  | late String binstub; | 
|  | // Batch files behave in funky ways if they are modified while updating. | 
|  | // To ensure that the byte-offsets of everything stays the same even if the | 
|  | // snapshot filename changes we insert some padding in lines containing the | 
|  | // snapshot. | 
|  | // 260 is the maximal short path length on Windows. Hopefully that is | 
|  | // enough. | 
|  | final padding = ' ' * (260 - snapshot.length); | 
|  | // We need an absolute path since relative ones won't be relative to the | 
|  | // right directory when the user runs this. | 
|  | snapshot = p.absolute(snapshot); | 
|  | if (Platform.isWindows) { | 
|  | binstub = ''' | 
|  | @echo off | 
|  | rem This file was created by pub v${sdk.version}. | 
|  | rem Package: ${package.name} | 
|  | rem Version: ${package.version} | 
|  | rem Executable: $executable | 
|  | rem Script: $script | 
|  | if exist "$snapshot" $padding( | 
|  | call dart "$snapshot" $padding%* | 
|  | rem The VM exits with code 253 if the snapshot version is out-of-date. | 
|  | rem If it is, we need to delete it and run "pub global" manually. | 
|  | if not errorlevel 253 ( | 
|  | goto error | 
|  | ) | 
|  | call dart pub global run ${package.name}:$script %* | 
|  | ) else ( | 
|  | call dart pub global run ${package.name}:$script %* | 
|  | ) | 
|  | goto eof | 
|  | :error | 
|  | exit /b %errorlevel% | 
|  | :eof | 
|  | '''; | 
|  | } else { | 
|  | binstub = ''' | 
|  | #!/usr/bin/env sh | 
|  | # This file was created by pub v${sdk.version}. | 
|  | # Package: ${package.name} | 
|  | # Version: ${package.version} | 
|  | # Executable: $executable | 
|  | # Script: $script | 
|  | if [ -f $snapshot ]; then | 
|  | dart "$snapshot" "\$@" | 
|  | # The VM exits with code 253 if the snapshot version is out-of-date. | 
|  | # If it is, we need to delete it and run "pub global" manually. | 
|  | exit_code=\$? | 
|  | if [ \$exit_code != 253 ]; then | 
|  | exit \$exit_code | 
|  | fi | 
|  | dart pub global run ${package.name}:$script "\$@" | 
|  | else | 
|  | dart pub global run ${package.name}:$script "\$@" | 
|  | fi | 
|  | '''; | 
|  | } | 
|  |  | 
|  | // Write the binstub to a temporary location, make it executable and move | 
|  | // it into place afterwards to avoid races. | 
|  | final tempDir = cache.createTempDir(); | 
|  | try { | 
|  | final tmpPath = p.join(tempDir, binStubPath); | 
|  |  | 
|  | // Write this as the system encoding since the system is going to | 
|  | // execute it and it might contain non-ASCII characters in the | 
|  | // pathnames. | 
|  | writeTextFile(tmpPath, binstub, encoding: const SystemEncoding()); | 
|  |  | 
|  | if (Platform.isLinux || Platform.isMacOS) { | 
|  | // Make it executable. | 
|  | var result = Process.runSync('chmod', ['+x', tmpPath]); | 
|  | if (result.exitCode != 0) { | 
|  | // Couldn't make it executable so don't leave it laying around. | 
|  | fail('Could not make "$tmpPath" executable (exit code ' | 
|  | '${result.exitCode}):\n${result.stderr}'); | 
|  | } | 
|  | } | 
|  | File(tmpPath).renameSync(binStubPath); | 
|  | } finally { | 
|  | deleteEntry(tempDir); | 
|  | } | 
|  |  | 
|  | return previousPackage; | 
|  | } | 
|  |  | 
|  | /// Deletes all existing binstubs for [package]. | 
|  | void _deleteBinStubs(String package) { | 
|  | if (!dirExists(_binStubDir)) return; | 
|  |  | 
|  | for (var file in listDir(_binStubDir, includeDirs: false)) { | 
|  | var contents = readTextFile(file); | 
|  | var binStubPackage = _binStubProperty(contents, 'Package'); | 
|  | if (binStubPackage == null) { | 
|  | log.fine('Could not parse binstub $file:\n$contents'); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (binStubPackage == package) { | 
|  | log.fine('Deleting old binstub $file'); | 
|  | deleteEntry(file); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /// 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. | 
|  | void _suggestIfNotOnPath(String installed) { | 
|  | 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. | 
|  | var result = runProcessSync('where', [r'\q', '$installed.bat']); | 
|  | if (result.exitCode == 0) return; | 
|  |  | 
|  | log.warning("${log.yellow('Warning:')} Pub installs executables into " | 
|  | '${log.bold(_binStubDir)}, 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 | 
|  | var result = | 
|  | runProcessSync('command', ['-v', installed], runInShell: true); | 
|  | if (result.exitCode == 0) return; | 
|  |  | 
|  | var binDir = _binStubDir; | 
|  | if (binDir.startsWith(Platform.environment['HOME']!)) { | 
|  | binDir = p.join( | 
|  | r'$HOME', | 
|  | p.relative(binDir, from: Platform.environment['HOME']), | 
|  | ); | 
|  | } | 
|  |  | 
|  | log.warning("${log.yellow('Warning:')} Pub installs executables into " | 
|  | '${log.bold(binDir)}, which is not on your path.\n' | 
|  | "You can fix that by adding this to your shell's config file " | 
|  | '(.bashrc, .bash_profile, etc.):\n' | 
|  | '\n' | 
|  | "  ${log.bold('export PATH="\$PATH":"$binDir"')}\n" | 
|  | '\n'); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Returns the value of the property named [name] in the bin stub script | 
|  | /// [source]. | 
|  | String? _binStubProperty(String source, String name) { | 
|  | var pattern = RegExp(RegExp.escape(name) + r': ([a-zA-Z0-9_-]+)'); | 
|  | var match = pattern.firstMatch(source); | 
|  | return match == null ? null : match[1]; | 
|  | } | 
|  | } |