Newer
Older
// 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.
library pub.global_packages;
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 'exceptions.dart';
import 'executable.dart' as exe;
import 'io.dart';
import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
import 'pubspec.dart';
import 'sdk.dart' as sdk;
import 'solver/version_solver.dart';
import 'source/cached.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.
///
/// 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,
{bool overwriteBinStubs}) async {
var source = cache.sources["git"] as GitSource;
var name = await source.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(
new PackageDep(name, "git", VersionConstraint.any, repo),
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.
Future activateHosted(String name, VersionConstraint constraint,
List<String> executables, {bool overwriteBinStubs}) async {
_describeActive(name);
await _installInCache(new PackageDep(name, "hosted", constraint, name),
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);
// Get the package's dependencies.
await entrypoint.ensureLockFileIsUpToDate();
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 = new PackageId(name, "path", entrypoint.root.version,
PathSource.describePath(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);
/// Installs the package [dep] and its dependencies into the system cache.
Future _installInCache(PackageDep 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.sources, 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.
var ids = await Future.wait(result.packages.map(_cacheDependency));
var lockFile = new LockFile(ids);
// Load the package graph from [result] so we don't need to re-parse all
// the pubspecs.
var graph = await new Entrypoint.inMemory(root, lockFile, cache)
.loadPackageGraph(result);
var snapshots = await _precompileExecutables(graph.entrypoint, dep.name);
_writeLockFile(dep.name, lockFile);
_updateBinStubs(graph.packages[dep.name], executables,
overwriteBinStubs: overwriteBinStubs, snapshots: snapshots);
}
/// Precompiles the executables for [package] 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 package) {
return log.progress("Precompiling executables", () async {
var binDir = p.join(_directory, package, 'bin');
cleanDir(binDir);
var graph = await entrypoint.loadPackageGraph();
var environment = await AssetEnvironment.create(
entrypoint, BarbackMode.RELEASE,
entrypoints: graph.packages[package].executableIds,
useDart2JS: false);
environment.barback.errors.listen((error) {
log.error(log.red("Build error:\n$error"));
return environment.precompileExecutables(package, binDir);
/// Downloads [id] into the system cache if it's a cached package.
///
/// Returns the resolved [PackageId] for [id].
Future<PackageId> _cacheDependency(PackageId id) async {
var source = cache.sources[id.source];
if (!id.isRoot && source is CachedSource) {
await source.downloadToSystemCache(id);
}
return source.resolveId(id);
/// 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, cache.sources));
var id = lockFile.packages[package];
rnystrom@google.com
committed
log.message('Activated ${_formatPackage(id)}.');
/// Shows the user the currently active package with [name], if any.
rnystrom@google.com
committed
void _describeActive(String name) {
rnystrom@google.com
committed
var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources);
var id = lockFile.packages[name];
rnystrom@google.com
committed
if (id.source == 'git') {
var url = GitSource.urlFromDescription(id.description);
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active from Git '
'repository "${url}".');
} else if (id.source == 'path') {
var path = PathSource.pathFromDescription(id.description);
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active at path '
'"$path".');
rnystrom@google.com
committed
log.message('Package ${log.bold(name)} is currently active at version '
'${log.bold(id.version)}.');
// 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.
Future<Entrypoint> find(String name) async {
var lockFilePath = _getLockFilePath(name);
var lockFile;
try {
lockFile = new LockFile.load(lockFilePath, cache.sources);
var oldLockFilePath = p.join(_directory, '$name.lock');
// 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);
// 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);
}
// Load the package from the cache.
var id = lockFile.packages[name];
lockFile.packages.remove(name);
var source = cache.sources[id.source];
if (source is CachedSource) {
// For cached sources, the package itself is in the cache and the
// lockfile is the one we just loaded.
var dir = await cache.sources[id.source].getDirectory(id);
var package = new Package.load(name, dir, cache.sources);
return new Entrypoint.inMemory(package, lockFile, cache);
}
// For uncached sources (i.e. path), the ID just points to the real
// directory for the package.
assert(id.source == "path");
return new Entrypoint(PathSource.pathFromDescription(id.description),
cache);
/// 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 [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, {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 find(package).then((entrypoint) {
return exe.runExecutable(entrypoint, package, executable, args,
mode: mode, isGlobal: true);
});
}
// 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, recompile: () {
log.fine("$package:$executable is out of date and needs to be "
"recompiled.");
return find(package)
.then((entrypoint) => entrypoint.loadPackageGraph())
.then((graph) => _precompileExecutables(graph.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");
rnystrom@google.com
committed
/// Shows the user a formatted list of globally activated packages.
rnystrom@google.com
committed
void listActivePackages() {
if (!dirExists(_directory)) return;
listDir(_directory).map(_loadPackageId).toList()
rnystrom@google.com
committed
..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;
}
rnystrom@google.com
committed
/// Returns formatted string representing the package [id].
String _formatPackage(PackageId id) {
if (id.source == 'git') {
var url = GitSource.urlFromDescription(id.description);
return '${log.bold(id.name)} ${id.version} from Git repository "$url"';
} else if (id.source == 'path') {
var path = PathSource.pathFromDescription(id.description);
return '${log.bold(id.name)} ${id.version} at path "$path"';
} else {
return '${log.bold(id.name)} ${id.version}';
}
}
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/// Repairs any corrupted globally-activated packages and their binstubs.
///
/// Returns a pair of two [int]s. The first indicates how many packages were
/// successfully re-activated; the second indicates how many failed.
Future<Pair<int, int>> repairActivatedPackages() async {
var executables = {};
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 = 0;
var failures = 0;
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 = await find(id.name);
var graph = await entrypoint.loadPackageGraph();
var snapshots = await _precompileExecutables(entrypoint, id.name);
var packageExecutables = executables.remove(id.name);
if (packageExecutables == null) packageExecutables = [];
_updateBinStubs(graph.packages[id.name], packageExecutables,
overwriteBinStubs: true, snapshots: snapshots,
suggestIfNotOnPath: false);
successes++;
} catch (error, stackTrace) {
var message = "Failed to reactivate "
"${log.bold(p.basenameWithoutExtension(entry))}";
if (id != null) {
message += " ${id.version}";
if (id.source != "hosted") message += " from ${id.source}";
}
log.error(message, error, stackTrace);
failures++;
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 = [];
var collided = {};
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]);
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
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}
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 = """
# 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 "\$@"
""";
}
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
writeTextFile(binStubPath, bash);
// 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.
var result = runProcessSync("which", [installed]);
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];
}