Skip to content
Snippets Groups Projects
global_packages.dart 32.3 KiB
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.

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 'dart.dart' as dart;
import 'entrypoint.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 'solver/version_solver.dart';
import 'source/cached.dart';
import 'source/git.dart';
import 'source/hosted.dart';
import 'source/path.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).
Jacob MacDonald's avatar
Jacob MacDonald committed
        cache.git.source
            .refFor(name, repo)
            .withConstraint(VersionConstraint.any)
            .withFeatures(features ?? const {}),
Jacob MacDonald's avatar
Jacob MacDonald committed
        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.
Jacob MacDonald's avatar
Jacob MacDonald committed
  Future activateHosted(
      String name, VersionConstraint constraint, List<String> executables,
      {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async {
    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.
Jacob MacDonald's avatar
Jacob MacDonald committed
    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
  ///
  /// Returns a map from executable name to path for the snapshots that were
  /// successfully precompiled.
Jacob MacDonald's avatar
Jacob MacDonald committed
  Future<Map<String, String>> _precompileExecutables(
      Entrypoint entrypoint, String packageName) {
    return log.progress("Precompiling executables", () {
      var binDir = p.join(_directory, packageName, 'bin');
      // 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];
Jacob MacDonald's avatar
Jacob MacDonald committed
      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');
Jacob MacDonald's avatar
Jacob MacDonald committed
      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.
      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".');
        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)}.');
  /// 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');
        // 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);
    if (source is CachedSource) {
      // For cached sources, the package itself is in the cache and the
      // lockfile is the one we just loaded.
Jacob MacDonald's avatar
Jacob MacDonald committed
      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,
    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.
Jacob MacDonald's avatar
Jacob MacDonald committed
  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');
Jacob MacDonald's avatar
Jacob MacDonald committed
    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()
Jacob MacDonald's avatar
Jacob MacDonald 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;
  }

  /// 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.
Jacob MacDonald's avatar
Jacob MacDonald committed
  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 "
Jacob MacDonald's avatar
Jacob MacDonald committed
              "\"${p.basenameWithoutExtension(entry)}\"",
              error,
              stackTrace);
    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(
Jacob MacDonald's avatar
Jacob MacDonald committed
              entrypoint.packageGraph.packages[id.name], packageExecutables,
              overwriteBinStubs: true,
              snapshots: snapshots,
        } 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.
Jacob MacDonald's avatar
Jacob MacDonald committed
        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,
Jacob MacDonald's avatar
Jacob MacDonald committed
      {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) {
Jacob MacDonald's avatar
Jacob MacDonald committed
      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.
Jacob MacDonald's avatar
Jacob MacDonald committed
    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 %*
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.
  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}
# 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"]);
Jacob MacDonald's avatar
Jacob MacDonald committed
      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 "
Jacob MacDonald's avatar
Jacob MacDonald committed
          '"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
Jacob MacDonald's avatar
Jacob MacDonald committed
      var result =
          runProcessSync("command", ["-v", installed], runInShell: true);
      if (result.exitCode == 0) return;

      var binDir = _binStubDir;
      if (binDir.startsWith(Platform.environment['HOME'])) {
Jacob MacDonald's avatar
Jacob MacDonald committed
        binDir =
            p.join("~", p.relative(binDir, from: Platform.environment['HOME']));
Jacob MacDonald's avatar
Jacob MacDonald committed
      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 "
Jacob MacDonald's avatar
Jacob MacDonald committed
          "(.bashrc, .bash_profile, etc.):\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];
  }