// 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:barback/barback.dart'; import 'package:pub_semver/pub_semver.dart'; import 'barback/asset_environment.dart'; import 'compiler.dart'; import 'dart.dart' as dart; import 'entrypoint.dart'; import 'exceptions.dart'; import 'executable.dart' as exe; import 'http.dart' as http; import 'io.dart'; import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; import 'package_name.dart'; import 'pubspec.dart'; import 'sdk.dart' as sdk; import 'solver/version_solver.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"); /// 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 activateGit(String repo, List<String> executables, {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async { var name = await cache.git.getPackageNameFromRepo(repo); // Call this just to log what the current active package is, if any. _describeActive(name); // 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). await _installInCache( cache.git.source .refFor(name, repo) .withConstraint(VersionConstraint.any) .withFeatures(features ?? const {}), executables, overwriteBinStubs: overwriteBinStubs); } /// Finds the latest version of the hosted package with [name] that matches /// [constraint] and makes it the active global version. /// /// The [features] map controls which features of the package to activate. /// /// [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 activateHosted( String name, VersionConstraint constraint, List<String> executables, {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async { _describeActive(name); await _installInCache( cache.hosted.source .refFor(name) .withConstraint(constraint) .withFeatures(features ?? const {}), 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 activatePath(String path, List<String> executables, {bool overwriteBinStubs}) async { var entrypoint = new Entrypoint(path, cache, isGlobal: true); // Get the package's dependencies. await entrypoint.acquireDependencies(SolveType.GET); var name = entrypoint.root.name; // Call this just to log what the current active package is, if any. _describeActive(name); // Write a lockfile that points to the local package. var fullPath = canonicalize(entrypoint.root.dir); var id = cache.path.source.idFor(name, entrypoint.root.version, fullPath); // TODO(rnystrom): Look in "bin" and display list of binaries that // user can run. _writeLockFile(name, new LockFile([id])); var binDir = p.join(_directory, name, 'bin'); if (dirExists(binDir)) deleteEntry(binDir); _updateBinStubs(entrypoint.root, executables, overwriteBinStubs: overwriteBinStubs); log.message('Activated ${_formatPackage(id)}.'); } /// Installs the package [dep] and its dependencies into the system cache. Future _installInCache(PackageRange dep, List<String> executables, {bool overwriteBinStubs}) async { // Create a dummy package with just [dep] so we can do resolution on it. var root = new Package.inMemory(new Pubspec("pub global activate", dependencies: [dep], sources: cache.sources)); // Resolve it and download its dependencies. var result = await resolveVersions(SolveType.GET, cache, root); if (!result.succeeded) { // If the package specified by the user doesn't exist, we want to // surface that as a [DataError] with the associated exit code. if (result.error.package != dep.name) throw result.error; if (result.error is NoVersionException) dataError(result.error.message); throw result.error; } result.showReport(SolveType.GET); // Make sure all of the dependencies are locally installed. await Future.wait(result.packages.map((id) { return http.withDependencyType(root.dependencyType(id.name), () async { if (id.isRoot) return; var source = cache.source(id.source); if (source is CachedSource) await source.downloadToSystemCache(id); }); })); var lockFile = result.lockFile; _writeLockFile(dep.name, lockFile); writeTextFile(_getPackagesFilePath(dep.name), lockFile.packagesFile(cache)); // Load the package graph from [result] so we don't need to re-parse all // the pubspecs. var entrypoint = new Entrypoint.fromSolveResult(root, cache, result, isGlobal: true); var snapshots = await _precompileExecutables(entrypoint, dep.name); _updateBinStubs(entrypoint.packageGraph.packages[dep.name], executables, overwriteBinStubs: overwriteBinStubs, snapshots: snapshots); var id = lockFile.packages[dep.name]; log.message('Activated ${_formatPackage(id)}.'); } /// Precompiles the executables for [packageName] and saves them in the global /// cache. /// /// Returns a map from executable name to path for the snapshots that were /// successfully precompiled. Future<Map<String, String>> _precompileExecutables( Entrypoint entrypoint, String packageName) { return log.progress("Precompiling executables", () { var binDir = p.join(_directory, packageName, 'bin'); cleanDir(binDir); // Try to avoid starting up an asset server to precompile packages if // possible. This is faster and produces better error messages. var package = entrypoint.packageGraph.packages[packageName]; if (entrypoint.packageGraph .transitiveDependencies(packageName) .every((package) => package.pubspec.transformers.isEmpty)) { return _precompileExecutablesWithoutBarback(package, binDir); } else { return _precompileExecutablesWithBarback(entrypoint, package, binDir); } }); } //// Precompiles all executables in [package] to snapshots from the //// filesystem. //// //// The snapshots are placed in [dir]. //// //// Returns a map from executable basenames without extensions to the paths //// to those executables. Future<Map<String, String>> _precompileExecutablesWithoutBarback( Package package, String dir) async { var precompiled = {}; await waitAndPrintErrors(package.executableIds.map((id) async { var url = p.toUri(package.dir); url = url.replace(path: p.url.join(url.path, id.path)); var basename = p.url.basename(id.path); var snapshotPath = p.join(dir, '$basename.snapshot'); await dart.snapshot(url, snapshotPath, packagesFile: p.toUri(_getPackagesFilePath(package.name)), id: id); precompiled[p.withoutExtension(basename)] = snapshotPath; })); return precompiled; } //// Precompiles all executables in [package] to snapshots from a barback //// asset environment. //// //// The snapshots are placed in [dir]. //// //// Returns a map from executable basenames without extensions to the paths //// to those executables. Future<Map<String, String>> _precompileExecutablesWithBarback( Entrypoint entrypoint, Package package, String dir) async { var environment = await AssetEnvironment.create( entrypoint, BarbackMode.RELEASE, entrypoints: package.executableIds, compiler: Compiler.none); environment.barback.errors.listen((error) { log.error(log.red("Build error:\n$error")); }); return environment.precompileExecutables(package.name, dir); } /// Finishes activating package [package] by saving [lockFile] in the cache. void _writeLockFile(String package, LockFile lockFile) { ensureDir(p.join(_directory, package)); // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it // when Dart 1.6 is old enough that we don't think anyone will have these // lockfiles anymore (issue 20703). var oldPath = p.join(_directory, "$package.lock"); if (fileExists(oldPath)) deleteEntry(oldPath); writeTextFile(_getLockFilePath(package), lockFile.serialize(cache.rootDir)); } /// Shows the user the currently active package with [name], if any. void _describeActive(String name) { try { var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); var id = lockFile.packages[name]; var source = id.source; if (source is GitSource) { var url = source.urlFromDescription(id.description); log.message('Package ${log.bold(name)} is currently active from Git ' 'repository "${url}".'); } else if (source is PathSource) { var path = source.pathFromDescription(id.description); log.message('Package ${log.bold(name)} is currently active at path ' '"$path".'); } else { log.message('Package ${log.bold(name)} is currently active at version ' '${log.bold(id.version)}.'); } } on IOException { // If we couldn't read the lock file, it's not activated. return null; } } /// 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(dir)) return false; _deleteBinStubs(name); var lockFile = new 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. Entrypoint find(String name) { var lockFilePath = _getLockFilePath(name); var lockFile; try { lockFile = new LockFile.load(lockFilePath, cache.sources); } on IOException { var oldLockFilePath = p.join(_directory, '$name.lock'); try { // TODO(nweiz): This looks for Dart 1.6's old lockfile location. // Remove it when Dart 1.6 is old enough that we don't think anyone // will have these lockfiles anymore (issue 20703). lockFile = new LockFile.load(oldLockFilePath, cache.sources); } on IOException { // If we couldn't read the lock file, it's not activated. dataError("No active package ${log.bold(name)}."); } // Move the old lockfile to its new location. ensureDir(p.dirname(lockFilePath)); new File(oldLockFilePath).renameSync(lockFilePath); } // 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); var source = cache.source(id.source); var entrypoint; if (source is CachedSource) { // For cached sources, the package itself is in the cache and the // lockfile is the one we just loaded. entrypoint = new Entrypoint.inMemory(cache.load(id), lockFile, cache, isGlobal: true); } else { // For uncached sources (i.e. path), the ID just points to the real // directory for the package. entrypoint = new Entrypoint( (id.source as PathSource).pathFromDescription(id.description), cache, isGlobal: true); } if (entrypoint.root.pubspec.flutterSdkConstraint != null) { dataError("${log.bold(name)} ${entrypoint.root.version} requires the " "Flutter SDK, which is unsupported for global executables."); } if (!entrypoint.root.pubspec.dartSdkConstraint.allows(sdk.version)) { dataError("${log.bold(name)} ${entrypoint.root.version} doesn't support " "Dart ${sdk.version}."); } return entrypoint; } /// Runs [package]'s [executable] with [args]. /// /// If [executable] is available in its precompiled 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 [checked] is true, the program is run in checked mode. If [mode] is /// passed, it's used as the barback mode; it defaults to /// [BarbackMode.RELEASE]. /// /// Returns the exit code from the executable. Future<int> runExecutable( String package, String executable, Iterable<String> args, {bool checked: false, BarbackMode mode}) { if (mode == null) mode = BarbackMode.RELEASE; var binDir = p.join(_directory, package, 'bin'); if (mode != BarbackMode.RELEASE || !fileExists(p.join(binDir, '$executable.dart.snapshot'))) { return exe.runExecutable(find(package), package, executable, args, isGlobal: true, checked: checked, mode: mode); } // Unless the user overrides the verbosity, we want to filter out the // normal pub output shown while loading the environment. if (log.verbosity == log.Verbosity.NORMAL) { log.verbosity = log.Verbosity.WARNING; } var snapshotPath = p.join(binDir, '$executable.dart.snapshot'); return exe.runSnapshot(snapshotPath, args, checked: checked, recompile: () async { log.fine("$package:$executable is out of date and needs to be " "recompiled."); await _precompileExecutables( find(package).packageGraph.entrypoint, package); }); } /// Gets the path to the lock file for an activated cached package with /// [name]. String _getLockFilePath(String name) => p.join(_directory, name, "pubspec.lock"); /// Gets the path to the .packages file for an activated cached package with /// [name]. String _getPackagesFilePath(String name) => p.join(_directory, name, ".packages"); /// 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 = new LockFile.load(p.join(_directory, path), cache.sources) .packages[name]; if (id == null) { throw new 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) { var source = id.source; if (source is GitSource) { var url = source.urlFromDescription(id.description); return '${log.bold(id.name)} ${id.version} from Git repository "$url"'; } else if (source is PathSource) { var path = source.pathFromDescription(id.description); 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 new ApplicationException("No 'Package' property."); } var executable = _binStubProperty(binstub, "Executable"); if (executable == null) { throw new 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)) { var id; try { id = _loadPackageId(entry); log.message("Reactivating ${log.bold(id.name)} ${id.version}..."); var entrypoint = find(id.name); var snapshots = await _precompileExecutables(entrypoint, id.name); var packageExecutables = executables.remove(id.name); if (packageExecutables == null) packageExecutables = []; _updateBinStubs( entrypoint.packageGraph.packages[id.name], packageExecutables, overwriteBinStubs: true, snapshots: snapshots, suggestIfNotOnPath: false); 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 = new StringBuffer("Binstubs exist for non-activated " "packages:\n"); executables.forEach((package, executableNames) { // TODO(nweiz): Use a normal for loop here when // https://github.com/dart-lang/async_await/issues/68 is fixed. executableNames.forEach( (executable) => deleteEntry(p.join(_binStubDir, executable))); message.writeln(" From ${log.bold(package)}: " "${toSentence(executableNames)}"); }); log.error(message); } return new Pair(successes, failures); } /// 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 [snapshots] is given, it is a map of the names of executables whose /// snapshots were precompiled to the paths of those snapshots. Binstubs for /// those will run the snapshot directly and skip pub entirely. /// /// If [suggestIfNotOnPath] is `true` (the default), this will warn the user if /// the bin directory isn't on their path. void _updateBinStubs(Package package, List<String> executables, {bool overwriteBinStubs, Map<String, String> snapshots, bool suggestIfNotOnPath: true}) { if (snapshots == null) snapshots = const {}; // 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: snapshots[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 .listFiles(beneath: "bin", recursive: false) .map((path) => package.relative(path)) .toList(); 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. /// /// If [snapshot] is non-null, it is a path to a snapshot file. 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, {bool overwrite, String snapshot}) { var binStubPath = p.join(_binStubDir, executable); if (Platform.operatingSystem == "windows") 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. var 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; } } // If the script was precompiled to a snapshot, just invoke that directly // and skip pub global run entirely. var invocation; if (snapshot != null) { // We expect absolute paths from the precompiler since relative ones // won't be relative to the right directory when the user runs this. assert(p.isAbsolute(snapshot)); invocation = 'dart "$snapshot"'; } else { invocation = "pub global run ${package.name}:$script"; } if (Platform.operatingSystem == "windows") { var batch = """ @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} $invocation %* """; if (snapshot != null) { batch += """ 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 ( exit /b %errorlevel% ) pub global run ${package.name}:$script %* """; } writeTextFile(binStubPath, batch); } else { var bash = """ #!/usr/bin/env sh # This file was created by pub v${sdk.version}. # Package: ${package.name} # Version: ${package.version} # Executable: ${executable} # Script: ${script} $invocation "\$@" """; if (snapshot != null) { bash += """ # 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 pub global run ${package.name}:$script "\$@" """; } // Write this as the system encoding since the system is going to execute // it and it might contain non-ASCII caharacters in the pathnames. writeTextFile(binStubPath, bash, encoding: const SystemEncoding()); // Make it executable. var result = Process.runSync('chmod', ['+x', binStubPath]); if (result.exitCode != 0) { // Couldn't make it executable so don't leave it laying around. try { deleteEntry(binStubPath); } on IOException catch (err) { // Do nothing. We're going to fail below anyway. log.fine("Could not delete binstub:\n$err"); } fail('Could not make "$binStubPath" executable (exit code ' '${result.exitCode}):\n${result.stderr}'); } } 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.operatingSystem == "windows") { // 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("~", 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 = new RegExp(quoteRegExp(name) + r": ([a-zA-Z0-9_-]+)"); var match = pattern.firstMatch(source); return match == null ? null : match[1]; } }