text: var assert = require('assert');
  var compiler = require('./compiler.js');
  var archinfo = require('../utils/archinfo.js');
  var _ = require('underscore');
  var linker = require('./linker.js');
  var buildmessage = require('../utils/buildmessage.js');
  import Builder from './builder.js';
  var bundler = require('./bundler.js');
  var watch = require('../fs/watch.js');
  var files = require('../fs/files.js');
  var isopackets = require('../tool-env/isopackets.js');
  var colonConverter = require('../utils/colon-converter.js');
  var utils = require('../utils/utils.js');
  var buildPluginModule = require('./build-plugin.js');
  var Console = require('../console/console.js').Console;
  var Profile = require('../tool-env/profile.js').Profile;
  
  var rejectBadPath = function (p) {
    if (p.match(/\.\./)) {
      throw new Error("bad path: " + p);
    }
  };
  
  ///////////////////////////////////////////////////////////////////////////////
  // Unibuild
  ///////////////////////////////////////////////////////////////////////////////
  
  // Options:
  // - kind [required] (main/plugin/app)
  // - arch [required]
  // - uses
  // - implies
  // - watchSet
  // - nodeModulesDirectories
  // - declaredExports
  // - resources
  
  var nextBuildId = 1;
  var Unibuild = function (isopack, options) {
    var self = this;
    options = options || {};
    self.pkg = isopack;
  
    self.kind = options.kind;
    self.arch = options.arch;
  
    self.uses = options.uses;
    self.implies = options.implies || [];
  
    // This WatchSet will end up having the watch items from the
    // SourceArch (such as package.js or .meteor/packages), plus all of
    // the actual source files for the unibuild (including items that we
    // looked at to find the source files, such as directories we
    // scanned).
    self.watchSet = options.watchSet || new watch.WatchSet();
  
    // Each Unibuild is given a unique id when it's loaded (it is
    // not saved to disk). This is just a convenience to make it easier
    // to keep track of Unibuilds in a map; it's used by bundler
    // and compiler. We put some human readable info in here too to make
    // debugging easier.
    self.id = self.pkg.name + "." + self.kind + "@" + self.arch + "#" +
      (nextBuildId ++);
  
    // 'declaredExports' are the variables which are exported from this package.
    // A list of objects with keys 'name' (required) and 'testOnly' (boolean,
    // defaults to false).
    self.declaredExports = options.declaredExports;
  
    // All of the data provided for eventual inclusion in the bundle,
    // other than JavaScript that still needs to be fed through the
    // final link stage. A list of objects with these keys:
    //
    // type: "source", "head", "body", "asset". (resources produced by
    // legacy source handlers can also be "js" or "css".
    //
    // data: The contents of this resource, as a Buffer. For example,
    // for "head", the data to insert in <head>; for "js", the
    // JavaScript source code (which may be subject to further
    // processing such as minification); for "asset", the contents of a
    // static resource such as an image.
    //
    // servePath: The (absolute) path at which the resource would prefer
    // to be served. Interpretation varies by type. For example, always
    // honored for "asset", ignored for "head" and "body", sometimes
    // honored for CSS but ignored if we are concatenating.
    //
    // sourceMap: Allowed only for "js". If present, a string.
    //
    // fileOptions: for "source", the options passed to `api.addFiles`.
    // plugin-specific.
    //
    // extension: for "source", the file extension that this matched
    // against at build time. null if matched against a specific filename.
    self.resources = options.resources;
  
    // Map from absolute paths of node_modules directories to
    // NodeModulesDirectory objects.
    self.nodeModulesDirectories = options.nodeModulesDirectories;
  
    // Provided for backwards compatibility; please use
    // unibuild.nodeModulesDirectories instead!
    _.some(self.nodeModulesDirectories, (nmd, nodeModulesPath) => {
      if (! nmd.local) {
        self.nodeModulesPath = nodeModulesPath;
        return true;
      }
    });
  };
  
  ///////////////////////////////////////////////////////////////////////////////
  // Isopack
  ///////////////////////////////////////////////////////////////////////////////
  
  // Meteor has a packaging system called "Isobuild". Isobuild knows how to
  // compile the same JavaScript code-base to different architectures: browser,
  // node.js-like server environment (could be Rhino or other) or a webview in a
  // Cordova mobile app.
  //
  // Each package used by Isobuild forms an Isopack. Isopack is a package format
  // containing source code for each architecture it can be ran on.
  // Each separate part built for a separate architecture is called "Unibuild".
  //
  // There are multiple reasons why we can't call it just "build" and historically
  // the name "Unibuild" has been associated with parts of Isopacks. We also can't
  // call it "Isobuild" because this is the brand-name of the whole
  // build/packaging system.
  var Isopack = function () {
    var self = this;
  
    // These have the same meaning as in PackageSource.
    self.name = null;
    self.metadata = {};
    self.version = null;
    self.isTest = false;
    self.debugOnly = false;
    self.prodOnly = false;
    self.testOnly = false;
  
    // Unibuilds, an array of class Unibuild.
    self.unibuilds = [];
  
    // Plugins in this package. Map from plugin name to {arch -> JsImage}.
    // Plugins are package-supplied classes and functions that can change the
    // build process: introduce a new source processor (compiler, minifier,
    // linter)
    self.plugins = {};
  
    self.cordovaDependencies = {};
  
    // isobuild:* pseudo-packages which this package depends on.
    self.isobuildFeatures = [];
  
    // -- Information for up-to-date checks --
    // Data in this section is only set if the Isopack was directly created by
    // compiler.compile or read from a package compiled by IsopackCache (with its
    // isopack-buildinfo.json file). They are not set for Isopacks read from
    // the tropohouse.
  
    // XXX this is likely to change once we have build versions
    //
    // A WatchSet for the full transitive dependencies for all plugins in this
    // package, as well as this package's package.js. If any of these dependencies
    // change, our plugins need to be rebuilt... but also, any package that
    // directly uses this package needs to be rebuilt in case the change to
    // plugins affected compilation.
    self.pluginWatchSet = new watch.WatchSet();
  
    // -- Loaded plugin state --
  
    // True if plugins have been initialized (if ensurePluginsInitialized has
    // been called)
    self._pluginsInitialized = false;
  
    // The SourceProcessors registered by plugins defined by this package.  Each
    // value is a SourceProcessorSet. sourceProcessors.compiler includes the
    // legacy source handlers as well.
    // Valid when self._pluginsInitialized is true.
    self.sourceProcessors = {
      compiler: null,
      linter: null,
      minifier: null
    };
  
    // See description in PackageSource. If this is set, then we include a copy of
    // our own source, in addition to any other tools that were originally in the
    // isopack.
    self.includeTool = false;
  
    // This is tools to copy from trees on disk. This is used by the
    // isopack-merge code in tropohouse.
    self.toolsOnDisk = [];
  
    // A map of package dependencies that can provide a plugin for this isopack.
    // In practice, it is every direct dependency and implied packages.
    self.pluginProviderPackageMap = null;
  
    // A directory on disk that plugins can use for caching. Should be created
    // by the code that initializes the Isopack. If not provided, plugins don't
    // get a disk cache.
    self.pluginCacheDir = null;
  
    // An in-memory only buildmessage.MessageSet object that is printed by the
    // build tool when the app is linted. Is also printed when a package
    // represented by Isopack is published.
    self.lintingMessages = null;
  };
  
  Isopack.knownFormats = ["unipackage-pre2", "isopack-1", "isopack-2"];
  
  // These functions are designed to convert isopack metadata between
  // versions. They were designed to convert between unipackage-pre2 and
  // isopack-1. The differences between these formats are essentially syntactical,
  // not semantic, and occur entirely in the isopack.json file, not in the
  // individual unibuild json files. These functions are written assuming those
  // constraints, and were not actually useful in the isopack-1/isopack-2
  // transition,where most of the changes are in the unibuild level, and there's
  // actual semantic changes involved. So they are not actually used as much as
  // they were before.
  Isopack.convertOneStepForward = function (data, fromFormat) {
    var convertedData = _.clone(data);
    // XXX COMPAT WITH 0.9.3
    if (fromFormat === "unipackage-pre2") {
      convertedData.builds = convertedData.unibuilds;
      delete convertedData.unibuilds;
      return convertedData;
    }
    if (fromFormat === "isopack-1") {
      // For now, there's no difference in this direction at the isopack level.
      return convertedData;
    }
  };
  Isopack.convertOneStepBackward = function (data, fromFormat) {
    var convertedData = _.clone(data);
    if (fromFormat === "isopack-1") {
      convertedData.unibuilds = convertedData.builds;
      convertedData.format = "unipackage-pre2";
      delete convertedData.builds;
      return convertedData;
    }
    if (fromFormat === "isopack-2") {
      // The conversion from isopack-2 requires converting the nested
      // unibuild data as well.  This shouldn't happen.
      throw Error("Can't automatically convert backwards from isopack-2");
    }
  };
  Isopack.convertIsopackFormat = Profile(
    "Isopack.convertIsopackFormat", function (data, fromFormat, toFormat) {
    var fromPos = _.indexOf(Isopack.knownFormats, fromFormat);
    var toPos = _.indexOf(Isopack.knownFormats, toFormat);
    var step = fromPos < toPos ? 1 : -1;
  
    if (fromPos === -1) {
      throw new Error("Can't convert from unknown Isopack format: " + fromFormat);
    }
    if (toPos === -1) {
      throw new Error("Can't convert to unknown Isopack format: " + toFormat);
    }
  
    while (fromPos !== toPos) {
      if (step > 0) {
        data = Isopack.convertOneStepForward(data, fromFormat);
      } else {
        data = Isopack.convertOneStepBackward(data, fromFormat);
      }
  
      fromPos += step;
      fromFormat = Isopack.knownFormats[fromPos];
    }
  
    return data;
  });
  
  // Read the correct file from isopackDirectory and convert to current format
  // of the isopack metadata. Returns null if there is no package here.
  Isopack.readMetadataFromDirectory =
    Profile("Isopack.readMetadataFromDirectory", function (isopackDirectory) {
    var metadata = null;
    let originalVersion = null;
  
    // deal with different versions of "isopack.json", backwards compatible
    var isopackJsonPath = files.pathJoin(isopackDirectory, "isopack.json");
    var unipackageJsonPath = files.pathJoin(isopackDirectory, "unipackage.json");
  
    if (files.exists(isopackJsonPath)) {
      var isopackJson = JSON.parse(files.readFile(isopackJsonPath));
  
      if (isopackJson['isopack-2']) {
        metadata = isopackJson['isopack-2'];
        originalVersion = 'isopack-2';
      } else if (isopackJson['isopack-1']) {
        metadata = Isopack.convertIsopackFormat(
          isopackJson['isopack-1'], 'isopack-1', 'isopack-2');
        originalVersion = 'isopack-1';
      } else {
        // This file is from the future and no longer supports this version
        throw new Error("Could not find isopack data supported any supported format (isopack-1 or isopack-2).\n" +
          "This isopack was likely built with a much newer version of Meteor.");
      }
    } else if (files.exists(unipackageJsonPath)) {
      // super old version with different file name
      // XXX COMPAT WITH 0.9.3
      if (files.exists(unipackageJsonPath)) {
        metadata = JSON.parse(files.readFile(unipackageJsonPath));
  
        metadata = Isopack.convertIsopackFormat(metadata,
          "unipackage-pre2", "isopack-2");
        originalVersion = 'unipackage-pre2';
      }
  
      if (metadata.format !== "unipackage-pre2") {
        // We don't support pre-0.9.0 isopacks, but we do know enough to delete
        // them if we find them in an isopack cache somehow (rather than crash).
        if (metadata.format === "unipackage-pre1") {
          throw new exports.OldIsopackFormatError();
        }
  
        throw new Error("Unsupported isopack format: " +
                        JSON.stringify(metadata.format));
      }
    }
  
    return {metadata, originalVersion};
  });
  
  _.extend(Isopack.prototype, {
    // Make a dummy (empty) package that contains nothing of interest.
    // XXX used?
    initEmpty: function (name) {
      var self = this;
      self.name = name;
    },
  
    // This is primarily intended to be used by the compiler. After
    // calling this, call addUnibuild to add the unibuilds.
    initFromOptions: function (options) {
      var self = this;
      self.name = options.name;
      self.metadata = options.metadata;
      self.version = options.version;
      self.isTest = options.isTest;
      self.plugins = options.plugins;
      self.cordovaDependencies = options.cordovaDependencies;
      self.pluginWatchSet = options.pluginWatchSet;
      self.npmDiscards = options.npmDiscards;
      self.includeTool = options.includeTool;
      self.debugOnly = options.debugOnly;
      self.prodOnly = options.prodOnly;
      self.testOnly = options.testOnly;
      self.pluginCacheDir = options.pluginCacheDir || null;
      self.isobuildFeatures = options.isobuildFeatures;
    },
  
    // Programmatically add a unibuild to this Isopack. Should only be
    // called as part of building up a new Isopack using
    // initFromOptions. 'options' are the options to the Unibuild
    // constructor.
    addUnibuild: function (options) {
      var self = this;
      self.unibuilds.push(new Unibuild(self, options));
    },
  
    setPluginProviderPackageMap: function (pluginProviderPackageMap) {
      var self = this;
      self.pluginProviderPackageMap = pluginProviderPackageMap;
    },
  
    getSourceFilesUnderSourceRoot: Profile(
      "Isopack#getSourceFilesUnderSourceRoot", function (sourceRoot) {
      var self = this;
      var sourceFiles = {};
      var anySourceFiles = false;
      var addSourceFilesFromWatchSet = function (watchSet) {
        _.each(watchSet.files, function (hash, filename) {
          if (! hash) {
            // If a file has a falsy hash, that means the file does/should
            // not exist.
            return;
          }
  
          anySourceFiles = true;
          var relativePath = files.pathRelative(sourceRoot, filename);
          // We only want files that are actually under sourceRoot.
          if (relativePath.substr(0, 3) === '..' + files.pathSep) {
            return;
          }
          sourceFiles[relativePath] = true;
        });
      };
      addSourceFilesFromWatchSet(self.pluginWatchSet);
      _.each(self.unibuilds, function (u) {
        addSourceFilesFromWatchSet(u.watchSet);
      });
  
      // Were we actually built from source or loaded from an IsopackCache? If so
      // then there should be at least one source file in some WatchSet. If not,
      // return null.
      if (! anySourceFiles) {
        return null;
      }
      return _.keys(sourceFiles);
    }),
  
    // An sorted array of all the architectures included in this package.
    architectures: Profile("Isopack#architectures", function () {
      var self = this;
      var archSet = {};
      _.each(self.unibuilds, function (unibuild) {
        archSet[unibuild.arch] = true;
      });
      _.each(self._toolArchitectures(), function (arch) {
        archSet[arch] = true;
      });
      _.each(self.plugins, function (plugin, name) {
        _.each(plugin, function (plug, arch) {
          archSet[arch] = true;
        });
      });
      var arches = _.keys(archSet).sort();
      // Ensure that our buildArchitectures string does not look like
      //    web+os+os.osx.x86_64
      // This would happen if there is an 'os' unibuild but a platform-specific
      // tool (eg, in meteor-tool).  This would confuse catalog.getBuildsForArches
      // into thinking that it would work for Linux, since the 'os' means
      // 'works on any Node server'.
      if (_.any(arches, function (a) { return a.match(/^os\./); })) {
        arches = _.without(arches, 'os');
      }
      return arches;
    }),
  
    // A sorted plus-separated string of all the architectures included in this
    // package.
    buildArchitectures: function () {
      var self = this;
      return self.architectures().join('+');
    },
  
    // Returns true if we think that this isopack is platform specific (contains
    // binary builds)
    platformSpecific: function () {
      var self = this;
      return _.any(self.architectures(), function (arch) {
        return arch.match(/^os\./);
      });
    },
  
    tarballName: function () {
      var self = this;
      return colonConverter.convert(self.name) + '-' + self.version;
    },
  
    _toolArchitectures: function () {
      var self = this;
      var toolArches = _.pluck(self.toolsOnDisk, 'arch');
      self.includeTool && toolArches.push(archinfo.host());
      return _.uniq(toolArches).sort();
    },
  
    // Return the unibuild of the package to use for a given target architecture
    // (eg, 'os.linux.x86_64' or 'web'), or throw an exception if that
    // packages can't be loaded under these circumstances.
    getUnibuildAtArch: Profile("Isopack#getUnibuildAtArch", function (arch) {
      var self = this;
  
      let chosenArch = archinfo.mostSpecificMatch(
        arch, _.pluck(self.unibuilds, 'arch'));
      if (! chosenArch && arch.match(/^os\./)) {
        // Special-case: we're looking for a specific server platform and
        // it's not available. (eg, we're deploying from a Mac to Linux and
        // are processing a local package with binary npm deps).  Search
        // again for the host version, which might find the Mac version.
        chosenArch =
          archinfo.mostSpecificMatch(archinfo.host(), _.pluck(self.unibuilds, 'arch'));
      }
      if (! chosenArch) {
        buildmessage.error(
          (self.name || "this app") +
            " is not compatible with architecture '" + arch + "'",
          { secondary: true });
        // recover by returning by no unibuilds
        return null;
      }
      return _.findWhere(self.unibuilds, { arch: chosenArch });
    }),
  
    _checkPluginsInitialized: function () {
      var self = this;
      if (self._pluginsInitialized) {
        return;
      }
      throw Error("plugins not yet initialized?");
    },
  
    // If this package has plugins, initialize them (run the startup
    // code in them so that they register their extensions). Idempotent.
    ensurePluginsInitialized: Profile("Isopack#ensurePluginsInitialized", function () {
      var self = this;
  
      buildmessage.assertInJob();
  
      if (self._pluginsInitialized) {
        return;
      }
  
      self.sourceProcessors.compiler = new buildPluginModule.SourceProcessorSet(
        self.displayName(), { hardcodeJs: true, singlePackage: true });
      self.sourceProcessors.linter = new buildPluginModule.SourceProcessorSet(
        self.displayName(), { singlePackage: true, allowConflicts: true });
      self.sourceProcessors.minifier = new buildPluginModule.SourceProcessorSet(
        self.displayName(), { singlePackage: true });
  
      _.each(self.plugins, function (pluginsByArch, name) {
        var arch = archinfo.mostSpecificMatch(
          archinfo.host(), _.keys(pluginsByArch));
        if (! arch) {
          buildmessage.error("package `" + name + "` is built for incompatible " +
                             "architecture");
          // Recover by ignoring plugin
          // XXX does this recovery work?
          return;
        }
  
        var plugin = pluginsByArch[arch];
        buildmessage.enterJob({
          title: "loading plugin `" + name +
            "` from package `" + self.name + "`"
          // don't necessarily have rootPath anymore
          // (XXX we do, if the isopack was locally built, which is
          // the important case for debugging. it'd be nice to get this
          // case right.)
        }, function () {
          // Make a new Plugin API object for this plugin.
          var Plugin = self._makePluginApi();
          plugin.load({ Plugin: Plugin, Profile: Profile });
        });
      });
  
      // Instantiate each of the registered batch plugins.  Note that we don't
      // do this directly in the registerCompiler (etc) call, because we want
      // to allow people to do something like:
      //   Plugin.registerCompiler({...}, function () { return new C; });
      //   var C = function () {...}
      // and so we want to wait for C to be defined.
      _.each(self.sourceProcessors, (sourceProcessorSet) => {
        _.each(sourceProcessorSet.allSourceProcessors, (sourceProcessor) => {
          sourceProcessor.instantiatePlugin();
        });
      });
  
      self._pluginsInitialized = true;
    }),
  
    _makePluginApi: function () {
      var isopack = this;
  
      /**
       * @global
       * @namespace Plugin
       * @summary The namespace that is exposed inside build plugin files.
       */
      var Plugin = {
        // 'extension' is a file extension without the separation dot
        // (eg 'js', 'coffee', 'coffee.md')
        //
        // 'options' can be omitted. The only known option is 'isTemplate', which
        // is a bit of a hack meaning "in an app, these files should be loaded
        // before non-templates".
        //
        // 'handler' is a function that takes a single argument, a
        // CompileStep (#CompileStep)
  
        /**
         * @summary Inside a build plugin source file specified in
         * [Package.registerBuildPlugin](#Package-registerBuildPlugin),
         * add a handler to compile files with a certain file extension.
         * @param  {String} fileExtension The file extension that this plugin
         * should handle, without the first dot.
         * Examples: `"coffee"`, `"coffee.md"`.
         * @param  {Function} handler  A function that takes one argument,
         * a CompileStep object.
         *
         * Documentation for CompileStep is available [on the GitHub Wiki](https://github.com/meteor/meteor/wiki/CompileStep-API-for-Build-Plugin-Source-Handlers).
         * @memberOf Plugin
         * @locus Build Plugin
         * @deprecated since 1.2
         * XXX COMPAT WITH 1.1
         */
        registerSourceHandler: function (extension, options, handler) {
          if (!handler) {
            handler = options;
            options = {};
          }
  
          // The popular package mquandalle:bower has a call to
          // `registerSourceHandler('json', null)` for some reason, to
          // work around some behavior of Meteor believed to be a bug. We
          // think that new features of registerCompiler like being able
          // to register for filenames will allow them to drop that line,
          // but in the meantime we might as well not choke on it. (The
          // old implementation coincidentally didn't choke.)
          if (!handler) {
            handler = function () {};
          }
  
          isopack.sourceProcessors.compiler.addLegacyHandler({
            extension,
            handler,
            packageDisplayName: isopack.displayName(),
            isTemplate: !!options.isTemplate,
            archMatching: options.archMatching
          });
        },
  
        _registerSourceProcessor: function (
          {extensions, filenames, archMatching, isTemplate},
          factory,
          {sourceProcessorSet, methodName, featurePackage}) {
          if (!isopack.featureEnabled(featurePackage)) {
            // This error is OK because we only define 1.0.0 for each of these
            // feature packages (in compiler.KNOWN_ISOBUILD_FEATURE_PACKAGES).
            buildmessage.error(
              `your package must \`api.use('${ featurePackage }@1.0.0')\` in ` +
                `order for its plugins to call Plugin.${ methodName }`);
            return;
          }
  
          const hasExtensions = (extensions &&
                                 extensions instanceof Array &&
                                 extensions.length > 0);
          const hasFilenames = (filenames &&
                                filenames instanceof Array &&
                                filenames.length > 0);
          if (! (hasExtensions || hasFilenames)) {
            buildmessage.error("Plugin." + methodName + " must specify a " +
                               "non-empty array of extensions or filenames",
                               { useMyCaller: 3 });
            // recover by ignoring
            return;
          }
  
          // Don't let extensions or filenames try to look for directories (in the
          // way that WatchSet expresses them).
          if (extensions && extensions.some(e => e.endsWith('/'))) {
            buildmessage.error(
              `Plugin.${methodName}: extensions may not end in /`);
            // recover by ignoring
            return;
          }
          if (filenames && filenames.some(f => f.endsWith('/'))) {
            buildmessage.error(
              `Plugin.${methodName}: filenames may not end in /`);
            // recover by ignoring
            return;
          }
  
          if (typeof factory !== 'function') {
            buildmessage.error(methodName + " call must "
                               + "specify a factory function",
                               { useMyCaller: 3 });
            // recover by ignoring
            return;
          }
  
          const sp = new buildPluginModule.SourceProcessor({
            isopack: isopack,
            extensions: extensions,
            filenames: filenames,
            archMatching: archMatching,
            isTemplate: isTemplate,
            factoryFunction: factory,
            methodName: methodName
          });
          // This logs a buildmessage on conflicts.
          sourceProcessorSet.addSourceProcessor(sp);
        },
  
        // Compilers are part of the Batch Plugins API.
        //
        // A compiler plugin is provided by packages to participate in
        // the build process. A compiler can register file extensions and
        // filenames it handles and the build tool will call the compiler's
        // `processFilesForTarget` method once for each target (eg, the server
        // or client program) with all of the files in the target.
        //
        // Compilers are run on application bundling (in bundle.js).
        // This is different from the legacy registerSourceHandler API,
        // which runs on a single file at a time when a *package* is built.
        // Published Isopack packages contain the original sources of
        // files handled by registerCompiler, not the generated output,
        // so compilers can be involved in the very end, when the app is bundled
        // (not in package publish time).  (Note that this requires a new
        // Isopack format, 'isopack-2'; versions of packages published with new
        // compilers cannot be used with previous releases of Meteor, but
        // Version Solver knows this and will select an older compatible
        // version if possible.
        //
        // Unlike the legacy API called "source handlers" (deprecated in
        // Meteor 1.2), compiler plugins can handle all files for the target,
        // making independent decisions about caching and dependencies resolution.
        //
        // The factory function must return an instance of a compiler.
        //
        // Note: It's important to ensure that all plugins that want to call
        // plugin compiler use the isobuild:compiler-plugin fake package, so that
        // Version Solver will not let you use registerCompiler plugins with old
        // versions of the tool.
  
        /**
         * @summary Inside a build plugin source file specified in
         * [Package.registerBuildPlugin](#Package-registerBuildPlugin),
         * add a compiler that will handle files with certain extensions or
         * filenames.
         * @param {Object} options
         * @param {String[]} options.extensions The file extensions that this
         * plugin should handle, without the first dot.
         * Examples: `["coffee", "coffee.md"]`.
         * @param {String[]} options.filenames The list of filenames
         * that this plugin should handle. Examples: `["config.json"]`.
         * @param {Function} factory A function that returns an instance
         * of a compiler class.
         *
         * More detailed documentation for build plugins is available [on the GitHub Wiki](https://github.com/meteor/meteor/wiki/Build-Plugins-API).
         * @memberOf Plugin
         * @locus Build Plugin
         */
        registerCompiler: function (options, factory) {
          Plugin._registerSourceProcessor(options || {}, factory, {
            sourceProcessorSet: isopack.sourceProcessors.compiler,
            methodName: "registerCompiler",
            featurePackage: "isobuild:compiler-plugin"
          });
        },
  
        // Linters are part of the Batch Plugin API.
        //
        // A linter plugin provides a Linter instance. The linter is
        // given a batch of source files for the target according to
        // linter's declared file extensions and filenames (e.g.: '*.js',
        // '.jshintrc').
        //
        // Linters don't output any files. They can only raise an error
        // message on one of the source files to force the build tool to
        // print a linting message.
        //
        // The factory function must return an instance of the linter.
        // The linter must have the `processFilesForPackage` method that
        // has two arguments:
        // - inputFiles - LinterFile - sources instances
        // - options - Object
        //   - globals - a list of strings - global variables that can be
        //     used in the target's scope as they are dependencies of the
        //     package or the app. e.g.: "Minimongo" or "Webapp".
        //
        // Unlike compilers and minifiers, linters run on one package
        // at a time.  Linters are run by `meteor run`, `meteor publish`,
        // and `meteor lint`.
  
        /**
         * @summary Inside a build plugin source file specified in
         * [Package.registerBuildPlugin](#Package-registerBuildPlugin),
         * add a linter that will handle files with certain extensions or
         * filenames.
         * @param {Object} options
         * @param {String[]} options.extensions The file extensions that this
         * plugin should handle, without the first dot.
         * Examples: `["js", "es6", "jsx"]`.
         * @param {Function} factory A function that returns an instance
         * of a linter class.
         *
         * More detailed documentation for build plugins is available [on the GitHub Wiki](https://github.com/meteor/meteor/wiki/Build-Plugins-API).
         * @memberOf Plugin
         * @locus Build Plugin
         */
        registerLinter: function (options, factory) {
          Plugin._registerSourceProcessor(options || {}, factory, {
            sourceProcessorSet: isopack.sourceProcessors.linter,
            methodName: "registerLinter",
            featurePackage: "isobuild:linter-plugin"
          });
        },
  
        // Minifiers are part of the Batch Plugin API.
        //
        // The minifiers are applied in the very end of the bundling
        // process, after the linters and compilers. Unlike linters and
        // compilers, minifiers are given the output of compilers and not
        // the source files the application developer supplied.
        //
        // The minifier plugins can fill into 2 types of minifiers: CSS or JS.
        // When the minifier is added to an app, it is used during "bundling" to
        // compress the app code and each package's code separately.
        // Only minifier packages directly used by an app (or implied by a package
        // directly used by the app) are active: using a minifer's package in
        // another package does nothing.
        //
        // So far, the minifiers are only run on client targets such as
        // web.browser and web.cordova.
        //
        // The factory function must return an instance of a
        // minifier. The method `processFilesForBundle` is passed a list of
        // files, possibly a linked file per target (for JavaScript files).
        //
        // - files - processed files to minify
        // - options - Object
        //   - minifyMode - string - 'development' or 'production', based
        //     on the bundling mode
  
        /**
         * @summary Inside a build plugin source file specified in
         * [Package.registerBuildPlugin](#Package-registerBuildPlugin),
         * add a linter that will handle files with certain extensions or
         * filenames.
         * @param {Object} options
         * @param {String[]} options.extensions The file extensions that this
         * plugin should handle, without the first dot. Can only be "js" or "css".
         * Examples: `["js", "css"]`.
         * @param {String[]} options.filenames The list of filenames
         * that this plugin should handle. Examples: `["config.json"]`.
         * @param {Function} factory A function that returns an instance
         * of a minifier class.
         *
         * More detailed documentation for build plugins is available [on the GitHub Wiki](https://github.com/meteor/meteor/wiki/Build-Plugins-API).
         * @memberOf Plugin
         * @locus Build Plugin
         */
        registerMinifier: function (options, factory) {
          var badUsedExtension = _.find(options.extensions, function (ext) {
            return ! _.contains(['js', 'css'], ext);
          });
  
          if (badUsedExtension !== undefined) {
            buildmessage.error(badUsedExtension + ': Minifiers are only allowed to register "css" or "js" extensions.');
            return;
          }
  
          if (options.filenames) {
            buildmessage.error("Plugin.registerMinifier does not accept `filenames`");
            return;
          }
  
          Plugin._registerSourceProcessor(options || {}, factory, {
            sourceProcessorSet: isopack.sourceProcessors.minifier,
            methodName: "registerMinifier",
            featurePackage: "isobuild:minifier-plugin"
          });
        },
  
        nudge: function () {
          Console.nudge(true);
        },
  
        convertToOSPath: files.convertToOSPath,
        convertToStandardPath: files.convertToStandardPath,
        path: {
          join: files.pathJoin,
          normalize: files.pathNormalize,
          relative: files.pathRelative,
          resolve: files.pathResolve,
          dirname: files.pathDirname,
          basename: files.pathBasename,
          extname: files.pathExtname,
          sep: files.pathSep
        },
        fs: files.fsFixPath
      };
      return Plugin;
    },
  
    // Load a Isopack on disk.
    //
    // options:
    // - isopackBuildInfoJson: parsed isopack-buildinfo.json object,
    //   if loading from an IsopackCache.
    initFromPath: Profile(
      "Isopack#initFromPath", function (name, dir, options) {
      var self = this;
      options = _.clone(options || {});
      options.firstIsopack = true;
  
      if (options.pluginCacheDir) {
        self.pluginCacheDir = options.pluginCacheDir;
      }
  
      return self._loadUnibuildsFromPath(name, dir, options);
    }),
  
    _loadUnibuildsFromPath: function (name, dir, options) {
      var self = this;
      options = options || {};
  
      // In the tropohouse, isopack paths are symlinks (which can be updated if
      // more unibuilds are merged in). For any given call to
      // _loadUnibuildsFromPath, let's ensure we see a consistent isopack by
      // realpath'ing dir.
      dir = files.realpath(dir);
  
      var {metadata: mainJson} = Isopack.readMetadataFromDirectory(dir);
      if (! mainJson) {
        throw new Error("No metadata files found for isopack at: " + dir);
      }
  
      // isopacks didn't used to know their name, but they should.
      if (_.has(mainJson, 'name') && name !== mainJson.name) {
        throw new Error("isopack " + name + " thinks its name is " +
                        mainJson.name);
      }
  
      // If we're loading from an IsopackCache, we need to load the WatchSets
      // which will be used by the bundler.  (builtBy is only used by
      // IsopackCache._checkUpToDate. pluginProviderPackageMap will actually be
      // set by IsopackCache afterwards, because it has access to an appropriate
      // PackageMap which can be subset to create a new PackageMap object.)
      var unibuildWatchSets = {};
      if (options.isopackBuildInfoJson) {
        if (! options.firstIsopack) {
          throw Error("can't merge isopacks with buildinfo");
        }
  
        // XXX should comprehensively sanitize (eg, typecheck) everything
        // read from json files
  
        // Read the watch sets for each unibuild
        _.each(
          options.isopackBuildInfoJson.unibuildDependencies,
          function (watchSetJSON, unibuildTag) {
            unibuildWatchSets[unibuildTag] =
              watch.WatchSet.fromJSON(watchSetJSON);
          });
  
        // Read pluginWatchSet. (In the multi-sub-isopack case, these are
        // guaranteed to be trivial (since we check that there's no
        // isopackBuildInfoJson), so no need to merge.)
        self.pluginWatchSet = watch.WatchSet.fromJSON(
          options.isopackBuildInfoJson.pluginDependencies);
      }
  
      // If we are loading multiple isopacks, only take this stuff from the
      // first one.
      if (options.firstIsopack) {
        self.name = name;
        self.metadata = {
          summary: mainJson.summary
        };
        self.version = mainJson.version;
        self.isTest = mainJson.isTest;
        self.debugOnly = !!mainJson.debugOnly;
        self.prodOnly = !!mainJson.prodOnly;
        self.testOnly = !!mainJson.testOnly;
      }
      _.each(mainJson.plugins, function (pluginMeta) {
        rejectBadPath(pluginMeta.path);
  
        var plugin = bundler.readJsImage(files.pathJoin(dir, pluginMeta.path));
  
        if (!_.has(self.plugins, pluginMeta.name)) {
          self.plugins[pluginMeta.name] = {};
        }
        // If we already loaded a plugin of this name/arch, just ignore this one.
        if (!_.has(self.plugins[pluginMeta.name], plugin.arch)) {
          self.plugins[pluginMeta.name][plugin.arch] = plugin;
        }
      });
      self.pluginsBuilt = true;
      _.each(mainJson.builds, function (unibuildMeta) {
        // aggressively sanitize path (don't let it escape to parent
        // directory)
        rejectBadPath(unibuildMeta.path);
  
        // Skip unibuilds we already have.
        var alreadyHaveUnibuild = _.find(self.unibuilds, function (unibuild) {
          return unibuild.arch === unibuildMeta.arch;
        });
        if (alreadyHaveUnibuild) {
          return;
        }
  
        var unibuildJson = JSON.parse(
          files.readFile(files.pathJoin(dir, unibuildMeta.path)));
  
        var unibuildBasePath =
          files.pathDirname(files.pathJoin(dir, unibuildMeta.path));
  
        if (unibuildJson.format !== "unipackage-unibuild-pre1" &&
            unibuildJson.format !== "isopack-2-unibuild") {
          throw new Error("Unsupported isopack unibuild format: " +
                          JSON.stringify(unibuildJson.format));
        }
  
        // Is this unibuild the legacy pre-"compiler plugin" format which contains
        // "prelink" resources of pre-processed JS files (as well as the
        // "packageVariables" field) instead of individual "source" resources (and
        // a "declaredExports" field)?
        var unibuildHasPrelink =
              unibuildJson.format === "unipackage-unibuild-pre1";
  
        var resources = [];
  
        _.each(unibuildJson.resources, function (resource) {
          rejectBadPath(resource.file);
          var data = files.readBufferWithLengthAndOffset(
            files.pathJoin(unibuildBasePath, resource.file),
            resource.length, resource.offset);
  
          if (resource.type === "prelink") {
            if (! unibuildHasPrelink) {
              throw Error("Unexpected prelink resource in " +
                          unibuildJson.format + " at " + dir);
            }
            // We found a "prelink" resource, because we're processing a package
            // published with an older version of Meteor which did not create
            // isopack-2 isopacks and which always preprocessed and linked all JS
            // files instead of leaving that until bundle time.  Let's pretend it
            // was just a single js source file, but leave a "legacyPrelink" field
            // on it so we can not re-link that part (and not re-analyze for
            // assigned variables).
            var prelinkResource = {
              type: "source",
              extension: "js",
              data: data,
              path: resource.servePath,
              // It's a shame to have to calculate the hash here instead of having
              // it on disk, but this only runs for legacy packages anyway.
              hash: watch.sha1(data),
              // Legacy prelink files definitely don't have a source processor!
              // They were created by an Isobuild that didn't even know about
              // source processors!
              usesDefaultSourceProcessor: true,
              legacyPrelink: {
                packageVariables: unibuildJson.packageVariables || []
              }
            };
            if (resource.sourceMap) {
              rejectBadPath(resource.sourceMap);
              prelinkResource.legacyPrelink.sourceMap = files.readFile(
                files.pathJoin(unibuildBasePath, resource.sourceMap), 'utf8');
            }
            resources.push(prelinkResource);
          } else if (resource.type === "source") {
            resources.push({
              type: "source",
              extension: resource.extension,
              usesDefaultSourceProcessor:
                !! resource.usesDefaultSourceProcessor,
              data: data,
              path: resource.path,
              hash: resource.hash,
              fileOptions: resource.fileOptions
            });
          } else if (_.contains(["head", "body", "css", "js", "asset"],
                                resource.type)) {
            resources.push({
              type: resource.type,
              data: data,
              servePath: resource.servePath || undefined,
              path: resource.path || undefined
            });
          } else {
            throw new Error("bad resource type in isopack: " +
                            JSON.stringify(resource.type));
          }
        });
  
        var declaredExports;
        if (unibuildHasPrelink) {
          // Legacy unibuild; it stores packageVariables and says some of them
          // are exports.
          declaredExports = [];
          _.each(unibuildJson.packageVariables, function (pv) {
            if (pv.export) {
              declaredExports.push({
                name: pv.name,
                testOnly: pv.export === 'tests'
              });
            }
          });
        } else {
          declaredExports = unibuildJson.declaredExports || [];
        }
  
        unibuildJson.uses && unibuildJson.uses.forEach((use) => {
          if (!use.weak && compiler.isIsobuildFeaturePackage(use.package) &&
              self.isobuildFeatures.indexOf(use.package) === -1) {
            self.isobuildFeatures.push(use.package);
          }
        });
  
        // Rebuild binary npm packages if unibuild arch matches host arch.
        const rebuildBinaries = archinfo.matches(
          archinfo.host(),
          unibuildMeta.arch
        );
  
        const nodeModulesDirectories = bundler.NodeModulesDirectory
          .readDirsFromJSON(unibuildJson.node_modules, {
            packageName: self.name,
            sourceRoot: unibuildBasePath,
            rebuildBinaries,
          });
  
        self.unibuilds.push(new Unibuild(self, {
          // At some point we stopped writing 'kind's to the metadata file, so
          // default to main.
          kind: unibuildMeta.kind || 'main',
          arch: unibuildMeta.arch,
          uses: unibuildJson.uses,
          implies: unibuildJson.implies,
          watchSet: unibuildWatchSets[unibuildMeta.path],
          nodeModulesDirectories,
          declaredExports: declaredExports,
          resources: resources
        }));
      });
  
      self.cordovaDependencies = mainJson.cordovaDependencies || null;
  
      _.each(mainJson.tools, function (toolMeta) {
        toolMeta.rootDir = dir;
        // XXX check for overlap
        self.toolsOnDisk.push(toolMeta);
      });
  
      return true;
    },
  
    hasCordovaUnibuild: function () {
      var self = this;
      return _.any(self.unibuilds, function (unibuild) {
        return unibuild.arch === 'web.cordova';
      });
    },
  
    canWriteLegacyBuilds() {
      function isResourceSafeForLegacyBuilds(resource) {
        // The only new kind of resource is "source"; other resources are
        // unchanged from legacy builds.
        if (resource.type !== "source") {
          return true;
        }
  
        // CSS is safe for legacy builds. (We assume everyone is using the
        // SourceProcessor from the 'meteor' package.)
        if (resource.extension === "css") {
          return true;
        }
  
        // If this JS resource uses hard-coded support for plain old ES5, then it
        // is safe to write as part of a legacy Isopack.
        if (resource.extension === "js" && resource.usesDefaultSourceProcessor) {
          return true;
        }
  
        // Nope, this package cannot be represented as an isopack-1 Isopack
        // because it uses a file implemented by registerCompiler other than the
        // very basic JS and CSS types.
        return false;
      }
  
      return this.unibuilds.every(
        unibuild => unibuild.resources.every(isResourceSafeForLegacyBuilds)
      );
    },
  
    // options:
    //
    // - includeIsopackBuildInfo: If set, write an isopack-buildinfo.json file.
    // - includePreCompilerPluginIsopackVersions: By default, saveToPath only
    //   creates an isopack of format 'isopack-2', with unibuilds of format
    //   'isopack-2-unibuild'.  These isopacks may contain "source" resources,
    //   which are processed at *bundle* time by compiler plugins.  They cannot be
    //   properly processed by older tools.  If this flag is set, saveToPath also
    //   tries to save data for older formats (isopack-1 and unipackage-pre2),
    //   converting JS and CSS "source" resources into "prelink" and "css"
    //   resources.  This is not possible if there are "source" resources other
    //   than JS or CSS; however, such packages must indirectly depend on the
    //   "isobuild:compiler-plugin" pseudo-package which is not compatible with
    //   older releases.  For packages that can't be converted to the older
    //   format, this function silently only saves the newer format.  (The point
    //   of this flag is allow us to optimize cases that never need to write the
    //   older format, such as the per-app isopack cache.)
    // - isopackCache: isopack cache in which this isopack is registered
    saveToPath: Profile("Isopack#saveToPath", function (outputDir, {
      includePreCompilerPluginIsopackVersions,
      includeIsopackBuildInfo,
      isopackCache = null,
    } = {}) {
      var self = this;
      var outputPath = outputDir;
  
      var builder = new Builder({ outputPath: outputPath });
      try {
        var mainJson = {
          name: self.name,
          summary: self.metadata.summary,
          version: self.version,
          isTest: self.isTest,
          builds: [],
          plugins: []
        };
  
        if (self.debugOnly) {
          mainJson.debugOnly = true;
        }
        if (self.prodOnly) {
          mainJson.prodOnly = true;
        }
        if (self.testOnly) {
          mainJson.testOnly = true;
        }
        if (! _.isEmpty(self.cordovaDependencies)) {
          mainJson.cordovaDependencies = self.cordovaDependencies;
        }
  
        const writeLegacyBuilds = (
          includePreCompilerPluginIsopackVersions
            && self.canWriteLegacyBuilds());
  
        var isopackBuildInfoJson = null;
        if (includeIsopackBuildInfo) {
          isopackBuildInfoJson = {
            builtBy: compiler.BUILT_BY,
            unibuildDependencies: {},
            // pluginDependencies defines a WatchSet that any package that could
            // use this package as a plugin needs to watch. So it always contains
            // our package.js (because modifications to package.js could add a new
            // plugin), as well as any files making up plugins in our package.
            pluginDependencies: self.pluginWatchSet.toJSON(),
            pluginProviderPackageMap: self.pluginProviderPackageMap.toJSON(),
            includeCordovaUnibuild: self.hasCordovaUnibuild()
          };
        }
  
        // XXX COMPAT WITH 0.9.3
        builder.reserve("unipackage.json");
  
        builder.reserve("isopack.json");
        // Reserve this even if includeIsopackBuildInfo is not set, to ensure
        // nothing else writes it somehow.
        builder.reserve("isopack-buildinfo.json");
  
        builder.reserve("head");
        builder.reserve("body");
  
        // Map from absolute path to npm directory in the unibuild, to the
        // generated filename in the isopack we're writing.  Multiple builds
        // can use the same npm modules (though maybe not any more, since tests
        // have been separated?), but also there can be different sets of
        // directories as well (eg, for a isopack merged with from multiple
        // isopacks with _loadUnibuildsFromPath).
        const npmDirsToCopy = Object.create(null);
  
        // Pre-linker versions of Meteor expect all packages in the warehouse to
        // contain a file called "package.js"; they use this as part of deciding
        // whether or not they need to download a new package. Because packages
        // are downloaded by the *existing* version of the tools, we need to
        // include this file until we're comfortable breaking "meteor update" from
        // 0.6.4.  (Specifically, warehouse.packageExistsInWarehouse used to check
        // to see if package.js exists instead of just looking for the package
        // directory.)
        // XXX Remove this once we can.
        builder.write("package.js", {
          data: new Buffer(
            ("// This file is included for compatibility with the Meteor " +
             "0.6.4 package downloader.\n"),
            "utf8")
        });
  
        var unibuildInfos = [];
  
        // Unibuilds
        _.each(self.unibuilds, function (unibuild) {
          // Make up a filename for this unibuild
          var baseUnibuildName = unibuild.arch;
          var unibuildDir =
            builder.generateFilename(baseUnibuildName, { directory: true });
          var unibuildJsonFile =
            builder.generateFilename(baseUnibuildName + ".json");
          mainJson.builds.push({
            kind: unibuild.kind,
            arch: unibuild.arch,
            path: unibuildJsonFile
          });
  
          var jsResourcesForLegacyPrelink = [];
  
          // Save unibuild dependencies. Keyed by the json path rather than thinking
          // too hard about how to encode pair (name, arch).
          if (isopackBuildInfoJson) {
            isopackBuildInfoJson.unibuildDependencies[unibuildJsonFile] =
              unibuild.watchSet.toJSON();
          }
  
          // Figure out where the npm dependencies go.
          let node_modules = {};
          _.each(unibuild.nodeModulesDirectories, nmd => {
            const bundlePath = _.has(npmDirsToCopy, nmd.sourcePath)
              // We already have this npm directory from another unibuild.
              ? npmDirsToCopy[nmd.sourcePath]
              : npmDirsToCopy[nmd.sourcePath] = builder.generateFilename(
                nmd.getPreferredBundlePath("isopack"),
                { directory: true }
              );
            node_modules[bundlePath] = nmd.toJSON();
          });
  
          const preferredPaths = Object.keys(node_modules);
          if (preferredPaths.length === 1) {
            // For backwards compatibility, if there's only one node_modules
            // directory, store it as a single string.
            node_modules = preferredPaths[0];
          }
  
          // Construct unibuild metadata
          var unibuildJson = {
            format: "isopack-2-unibuild",
            declaredExports: unibuild.declaredExports,
            uses: _.map(unibuild.uses, function (u) {
              return {
                'package': u.package,
                // For cosmetic value, leave false values for these options out of
                // the JSON file.
                constraint: u.constraint || undefined,
                unordered: u.unordered || undefined,
                weak: u.weak || undefined
              };
            }),
            implies: (_.isEmpty(unibuild.implies) ? undefined : unibuild.implies),
            resources: []
          };
  
          if (preferredPaths.length > 0) {
            // If there are no node_modules directories, don't confuse older
            // versions of Meteor by storing an empty object.
            unibuildJson.node_modules = node_modules;
          }
  
          const usesModules = ! isopackCache ||
            isopackCache.uses(self, "modules", unibuild.arch);
  
          // Output 'head', 'body' resources nicely
          var concat = { head: [], body: [] };
          var offset = { head: 0, body: 0 };
          _.each(unibuild.resources, function (resource) {
            if (_.contains(["head", "body"], resource.type)) {
              if (concat[resource.type].length) {
                concat[resource.type].push(new Buffer("\n", "utf8"));
                offset[resource.type]++;
              }
              if (! (resource.data instanceof Buffer)) {
                throw new Error("Resource data must be a Buffer");
              }
  
              if (! usesModules &&
                  resource.fileOptions &&
                  resource.fileOptions.lazy) {
                // Omit lazy resources from the unibuild JSON file.
                return;
              }
  
              unibuildJson.resources.push({
                type: resource.type,
                file: files.pathJoin(unibuildDir, resource.type),
                length: resource.data.length,
                offset: offset[resource.type]
              });
  
              concat[resource.type].push(resource.data);
              offset[resource.type] += resource.data.length;
            }
          });
  
          _.each(concat, function (parts, type) {
            if (parts.length) {
              builder.write(files.pathJoin(unibuildDir, type), {
                data: Buffer.concat(concat[type], offset[type])
              });
            }
          });
  
          // Output other resources each to their own file
          _.each(unibuild.resources, function (resource) {
            if (_.contains(["head", "body"], resource.type)) {
              // already did this one
              return;
            }
  
            const generatedFilename =
              builder.writeToGeneratedFilename(
                files.pathJoin(unibuildDir,
                               resource.servePath || resource.path),
                { data: resource.data });
  
            if (! usesModules &&
                resource.fileOptions &&
                resource.fileOptions.lazy) {
              // Omit lazy resources from the unibuild JSON file, but only
              // after they are copied into the bundle (immediately above).
              return;
            }
  
            // If we're going to write a legacy prelink file later, track the
            // original form of the resource object (with the source in a Buffer,
            // etc) instead of the later version.  #HardcodeJs
            if (writeLegacyBuilds &&
                resource.type === "source" &&
                resource.extension == "js") {
              jsResourcesForLegacyPrelink.push({
                data: resource.data,
                hash: resource.hash,
                servePath: unibuild.pkg._getServePath(resource.path),
                bare: resource.fileOptions && resource.fileOptions.bare,
                sourceMap: resource.sourceMap,
                // If this file was actually read from a legacy isopack and is
                // itself prelinked, this will be an object with some metadata
                // about it, and we can skip re-running prelink later.
                legacyPrelink: resource.legacyPrelink
              });
            }
  
            unibuildJson.resources.push({
              type: resource.type,
              extension: resource.extension,
              file: generatedFilename,
              length: resource.data.length,
              offset: 0,
              usesDefaultSourceProcessor:
                resource.usesDefaultSourceProcessor || undefined,
              servePath: resource.servePath || undefined,
              path: resource.path || undefined,
              hash: resource.hash || undefined,
              fileOptions: resource.fileOptions || undefined
            });
          });
  
          // Control file for unibuild
          builder.writeJson(unibuildJsonFile, unibuildJson);
          unibuildInfos.push({
            unibuild: unibuild,
            unibuildJson: unibuildJson,
            jsResourcesForLegacyPrelink: jsResourcesForLegacyPrelink
          });
        });
  
        // If unibuilds included node_modules, copy them in.
        _.each(npmDirsToCopy, (bundlePath, sourcePath) => {
          builder.copyDirectory({
            from: sourcePath,
            to: bundlePath,
            npmDiscards: self.npmDiscards,
            symlink: false
          });
        });
  
        // Plugins
        _.each(self.plugins, function (pluginsByArch, name) {
          _.each(pluginsByArch, function (plugin) {
            // XXX the name of the plugin doesn't typically contain a colon, but
            // escape it just in case.
            var pluginDir = builder.generateFilename(
              'plugin.' + colonConverter.convert(name) + '.' + plugin.arch,
              { directory: true });
            var pluginBuild = plugin.write(builder.enter(pluginDir));
            var pluginEntry = {
              name: name,
              arch: plugin.arch,
              path: files.pathJoin(pluginDir, pluginBuild.controlFile)
            };
            mainJson.plugins.push(pluginEntry);
          });
        });
  
        // Tools
        // First, are we supposed to include our own source as a tool?
        if (self.includeTool) {
          var toolsJson = self._writeTool(builder);
          mainJson.tools = toolsJson;
        }
        // Next, what about other tools we may be merging from other isopacks?
        // XXX check for overlap
        _.each(self.toolsOnDisk, function (toolMeta) {
          toolMeta = _.clone(toolMeta);
          var rootDir = toolMeta.rootDir;
          delete toolMeta.rootDir;
          builder.copyDirectory({
            from: files.pathJoin(rootDir, toolMeta.path),
            to: toolMeta.path,
            symlink: false
          });
          if (!mainJson.tools) {
            mainJson.tools = [];
          }
          mainJson.tools.push(toolMeta);
        });
  
        var mainLegacyJson = null;
        if (writeLegacyBuilds) {
          mainLegacyJson = _.clone(mainJson);
          mainLegacyJson.builds = [];
  
          _.each(unibuildInfos, function (unibuildInfo) {
            var unibuild = unibuildInfo.unibuild;
            var unibuildJson = unibuildInfo.unibuildJson;
            var jsResourcesForLegacyPrelink =
                  unibuildInfo.jsResourcesForLegacyPrelink;
            var legacyFilename = builder.generateFilename(
              unibuild.arch + '-legacy.json');
            var legacyDir = unibuild.arch + '-legacy';
            mainLegacyJson.builds.push({
              kind: unibuild.kind,
              arch: unibuild.arch,
              path: legacyFilename
            });
  
            unibuildJson.format = 'unipackage-unibuild-pre1';
            var newResources = [];
            _.each(unibuildJson.resources, function (resource) {
              if (resource.type !== 'source') {
                newResources.push(resource);
              } else if (resource.extension === 'css') {
                // Convert this resource from a new-style "source" to an
                // old-style "css".
                newResources.push({
                  type: 'css',
                  file: resource.file,
                  length: resource.length,
                  offset: resource.offset,
                  servePath: self._getServePath(resource.path)
                });
              } else if (resource.extension === 'js') {
                // Skip; we saved this in jsResourcesForLegacyPrelink above
                // already, in the format that linker.prelink understands.
              } else {
                throw Error(
                  "shouldn't write legacy builds for non-JS/CSS source "
                    + JSON.stringify(resource));
              }
            });
  
            var prelinkFile, prelinkData, packageVariables;
            if (jsResourcesForLegacyPrelink.length === 1 &&
                jsResourcesForLegacyPrelink[0].legacyPrelink) {
              // Aha!  This isopack was actually a legacy isopack in the first
              // place! So this source file is already the output of prelink,
              // and we don't need to reprocess it.
              prelinkFile = jsResourcesForLegacyPrelink[0];
              // XXX It's weird that the type of object going in and out of
              // linker.prelink is different (so that this prelinkData
              // assignment differs from that below), ah well.
              prelinkData = prelinkFile.data;
              packageVariables =
                jsResourcesForLegacyPrelink[0].legacyPrelink.packageVariables;
            } else {
              // Determine captured variables, legacy way. First, start with the
              // exports. We'll add the package variables after running prelink.
              packageVariables = [];
              var packageVariableNames = {};
              _.each(unibuild.declaredExports, function (symbol) {
                if (_.has(packageVariableNames, symbol.name)) {
                  return;
                }
                packageVariables.push({
                  name: symbol.name,
                  export: symbol.testOnly? "tests" : true
                });
                packageVariableNames[symbol.name] = true;
              });
  
              if (jsResourcesForLegacyPrelink.length) {
                // Not originally legacy; let's run prelink to make it legacy.
                var results = linker.prelink({
                  inputFiles: jsResourcesForLegacyPrelink,
                  // I was confused about this, so I am leaving a comment -- the
                  // combinedServePath is either [pkgname].js or [pluginName]:plugin.js.
                  // XXX: If we change this, we can get rid of source arch names!
                  combinedServePath: (
                    "/packages/" + colonConverter.convert(
                      unibuild.pkg.name +
                        (unibuild.kind === "main" ? "" : (":" + unibuild.kind)) +
                        ".js")),
                  name: unibuild.pkg.name
                });
                if (results.files.length !== 1) {
                  throw Error("prelink should return 1 file, not " +
                              results.files.length);
                }
                prelinkFile = results.files[0];
                prelinkData = new Buffer(prelinkFile.source, 'utf8');
  
                _.each(results.assignedVariables, function (name) {
                  if (_.has(packageVariableNames, name)) {
                    return;
                  }
                  packageVariables.push({
                    name: name
                  });
                  packageVariableNames[name] = true;
                });
              }
            }
  
            if (prelinkFile && prelinkData) {
              var prelinkResource = {
                type: 'prelink',
                file: builder.writeToGeneratedFilename(
                  files.pathJoin(legacyDir, prelinkFile.servePath),
                  { data: prelinkData }),
                length: prelinkData.length,
                offset: 0,
                servePath: prelinkFile.servePath || undefined
              };
              if (prelinkFile.sourceMap) {
                // It appears in node v0.10.x, `new Buffer({someobject: 1})`
                // would be silently swallowed. (leaving an empty buffer)
                // In node v4+ this same code throws an error.
                // XX - Not 100% sure what prelinkFile.sourceMap _can_ be,
                //      so here's some exhaustive checking of things buffer
                //      _will_ accept.
                var acceptedByBuffer = _.isString(prelinkFile.sourceMap)
                      || _.isNumber(prelinkFile.sourceMap)
                      || _.isArray(prelinkFile.sourceMap)
                      || (prelinkFile.sourceMap instanceof Buffer);
                if (!acceptedByBuffer) {
                  prelinkFile.sourceMap = JSON.stringify(prelinkFile.sourceMap);
                }
                // Write the source map.
  
                if (typeof prelinkFile.sourceMap !== "string") {
                  prelinkFile.sourceMap = JSON.stringify(prelinkFile.sourceMap);
                }
  
                prelinkResource.sourceMap = builder.writeToGeneratedFilename(
                  files.pathJoin(legacyDir, prelinkFile.servePath + '.map'),
                  { data: new Buffer(prelinkFile.sourceMap, 'utf8') }
                );
              }
              newResources.push(prelinkResource);
            }
  
            if (packageVariables.length) {
              unibuildJson.packageVariables = packageVariables;
            }
  
            unibuildJson.resources = newResources;
            delete unibuildJson.declaredExports;
            builder.writeJson(legacyFilename, unibuildJson);
          });
  
          // old unipackage.json format/filename.  no point to save this if
          // we can't even support isopack-1.
          // XXX COMPAT WITH 0.9.3
          builder.writeJson(
            "unipackage.json",
            Isopack.convertIsopackFormat(
              // Note that mainLegacyJson is isopack-1 (has no "source" resources)
              // rather than isopack-2.
              mainLegacyJson, "isopack-1", "unipackage-pre2"));
        }
  
        var isopackJson = {};
        isopackJson['isopack-2'] = mainJson;
        if (writeLegacyBuilds) {
          isopackJson['isopack-1'] = mainLegacyJson;
        }
  
        // writes one file with all of the new formats, so that it is possible
        // to invent a new format and have old versions of meteor still read the
        // old format
        //
        // This looks something like:
        // {
        //   isopack-1: {... data ...},
        //   isopack-2: {... data ...}
        // }
        builder.writeJson("isopack.json", isopackJson);
  
        if (isopackBuildInfoJson) {
          builder.writeJson("isopack-buildinfo.json", isopackBuildInfoJson);
        }
        builder.complete();
      } catch (e) {
        builder.abort();
        throw e;
      }
    }),
  
    _writeTool: Profile("Isopack#_writeTool", function (builder) {
      var self = this;
  
      var pathsToCopy = utils.runGitInCheckout(
        'ls-tree',
        '-r',
        '--name-only',
        '--full-tree',
        'HEAD',
        // The actual trees to copy!
        'tools', 'examples', 'LICENSE.txt', 'LICENSES',
        'meteor', 'meteor.bat', 'scripts/admin/launch-meteor',
        'packages/package-version-parser/package-version-parser.js',
        'packages/meteor/flush-buffers-on-exit-in-windows.js');
  
      // Trim blank line and unnecessary examples.
      pathsToCopy = _.filter(pathsToCopy.split('\n'), function (f) {
        return f && !f.match(/^examples\/other/) &&
          !f.match(/^examples\/unfinished/);
      });
  
      // Regexes matching paths to transpile using babel
      var transpileRegexes = [
        /^tools\/[^\/]+\.js$/, // General tools files
        /^tools\/isobuild\/[^\/]+\.js$/, // Isobuild files
        /^tools\/cli\/[^\/]+\.js$/, // CLI files
        /^tools\/tool-env\/[^\/]+\.js$/, // Tool initiation and clean up
        /^tools\/runners\/[^\/]+\.js$/, // Parts of tool process
        /^tools\/packaging\/[^\/]+\.js$/,
        /^tools\/packaging\/catalog\/[^\/]+\.js$/,
        /^tools\/utils\/[^\/]+\.js$/,
        /^tools\/fs\/[^\/]+\.js$/,
        /^tools\/meteor-services\/[^\/]+\.js$/,
        /^tools\/tool-testing\/[^\/]+\.js$/,
        /^tools\/console\/[^\/]+\.js$/,
        /^tools\/cordova\/[^\/]+\.js$/,
        // We don't support running self-test from an install anymore
      ];
  
      // Split pathsToCopy into two arrays - one of files that should be copied
      // directly, and one of files that should be transpiled with Babel
      var pathsToTranspile = [];
      var pathsToCopyStraight = [];
      pathsToCopy.forEach((path) => {
        var shouldTranspile =
          _.some(transpileRegexes, (regex) => path.match(regex));
  
        if (shouldTranspile) {
          pathsToTranspile.push(path);
        } else {
          pathsToCopyStraight.push(path);
        }
      });
  
      // Set up builder to write to the correct directory
      var toolPath = 'mt-' + archinfo.host();
      builder = builder.enter(toolPath);
  
      // Transpile the files we selected
      var babel = require("meteor-babel");
      pathsToTranspile.forEach((path) => {
        var fullPath = files.convertToOSPath(
          files.pathJoin(files.getCurrentToolsDir(), path));
  
        var inputFileContents = files.readFile(fullPath, "utf-8");
  
        // #RemoveInProd
        // We don't actually want to load the babel auto-transpiler when we are
        // in a Meteor installation where everything is already transpiled for us.
        // Therefore, strip out that line in main.js
        if (path === "tools/tool-env/install-babel.js" ||
            path === "tools/tool-env/source-map-retriever-stack.js") {
          inputFileContents = inputFileContents.replace(/^.*#RemoveInProd.*$/mg, "");
        }
  
        var babelOptions = babel.getDefaultOptions();
  
        _.extend(babelOptions, {
          filename: path,
          sourceFileName: "/" + path,
          sourceMapTarget: path + ".map",
          sourceMap: true
        });
  
        var transpiled = babel.compile(inputFileContents, babelOptions);
  
        var sourceMapUrlComment = "//# sourceMappingURL=" + files.pathBasename(path + ".map");
  
        builder.write(path, {
          data: new Buffer(transpiled.code + "\n" + sourceMapUrlComment, 'utf8')
        });
  
        builder.write(path + ".map", {
          data: new Buffer(JSON.stringify(transpiled.map), 'utf8')
        });
      });
  
      var gitSha = utils.runGitInCheckout('rev-parse', 'HEAD');
      builder.reserve('isopackets', {directory: true});
      builder.write('.git_version.txt', {data: new Buffer(gitSha, 'utf8')});
  
      builder.copyDirectory({
        from: files.getCurrentToolsDir(),
        to: '',
        specificFiles: pathsToCopyStraight,
        symlink: false
      });
  
      // Include the dev bundle, but drop a few things that are only used by
      // self-test (which isn't supported from release).
      var devBundleIgnore = _.clone(bundler.ignoreFiles);
      devBundleIgnore.push(/BrowserStackLocal/, /browserstack-webdriver/);
      builder.copyDirectory({
        from: files.pathJoin(files.getDevBundle()),
        to: 'dev_bundle',
        ignore: devBundleIgnore,
        symlink: false
      });
  
      // Build all of the isopackets now, so that no build step is required when
      // you're actually running meteor from a release in order to load packages.
      var isopacketBuildContext = isopackets.makeIsopacketBuildContext();
  
      var messages = buildmessage.capture(function () {
        // We rebuild them in the order listed in ISOPACKETS. This is not strictly
        // necessary here, since any isopackets loaded as part of the build
        // process are going to be the current tool's isopackets, not the
        // isopackets that we're writing out.
        _.each(isopackets.ISOPACKETS, function (packages, isopacketName) {
          buildmessage.enterJob({
            title: "compiling " + isopacketName + " packages for the tool"
          }, function () {
            isopacketBuildContext.isopackCache.buildLocalPackages(packages);
            if (buildmessage.jobHasMessages()) {
              return;
            }
  
            var image = bundler.buildJsImage({
              name: "isopacket-" + isopacketName,
              packageMap: isopacketBuildContext.packageMap,
              isopackCache: isopacketBuildContext.isopackCache,
              use: packages
            }).image;
            if (buildmessage.jobHasMessages()) {
              return;
            }
  
            image.write(
              builder.enter(files.pathJoin('isopackets', isopacketName)));
          });
        });
      });
      // This is a build step ... but it's one that only happens in development,
      // and similar to a isopacket load failure, it can just crash the app
      // instead of being handled nicely.
      if (messages.hasMessages()) {
        Console.error("Errors prevented tool build:");
        Console.error(messages.formatMessages());
        throw new Error("tool build failed?");
      }
  
      return [{
        name: 'meteor',
        arch: archinfo.host(),
        path: toolPath
      }];
    }),
  
    getMergedWatchSet: Profile("Isopack#getMergedWatchSet", function () {
      var self = this;
      var watchSet = self.pluginWatchSet.clone();
      _.each(self.unibuilds, function (unibuild) {
        watchSet.merge(unibuild.watchSet);
      });
      return watchSet;
    }),
  
    getClientWatchSet: Profile("Isopack#getClientWatchSet", function () {
      var watchSet = this.pluginWatchSet.clone();
      _.each(this.unibuilds, function (unibuild) {
        if (/^web\./.test(unibuild.arch)) {
          watchSet.merge(unibuild.watchSet);
        }
      });
      return watchSet;
    }),
  
    getServerWatchSet: Profile("Isopack#getServerWatchSet", function () {
      var watchSet = this.pluginWatchSet.clone();
      _.each(this.unibuilds, function (unibuild) {
        if (! /^web\./.test(unibuild.arch)) {
          watchSet.merge(unibuild.watchSet);
        }
      });
      return watchSet;
    }),
  
    // Similar to PackageSource.getPackagesToLoadFirst, but doesn't include
    // packages used by plugins, because plugin dependencies are already
    // statically included in this built Isopack. Used by
    // IsopackCache._ensurePackageLoaded.
    //
    // Like getPackagesToLoadFirst, it filters out isobuild:* pseudo-packages and
    // should not be used to create input to Version Solver.
    getStrongOrderedUsedAndImpliedPackages: Profile(
      "Isopack#getStrongOrderedUsedAndImpliedPackages", function () {
      var self = this;
      var packages = {};
      var processUse = function (use) {
        if (use.weak || use.unordered) {
          return;
        }
        // Only include real packages, not isobuild:* pseudo-packages.
        if (compiler.isIsobuildFeaturePackage(use.package)) {
          return;
        }
        packages[use.package] = true;
      };
  
      _.each(self.unibuilds, function (unibuild) {
        _.each(unibuild.uses, processUse);
        _.each(unibuild.implies, processUse);
      });
      return _.keys(packages);
    }),
  
    featureEnabled(featurePackageName) {
      return this.isobuildFeatures.indexOf(featurePackageName) !== -1;
    },
  
    _getServePath: function (pathInPackage) {
      var self = this;
      var serveRoot;
      if (self.name) {
        serveRoot = files.pathJoin('/packages/', self.name);
      } else {
        serveRoot = '/';
      }
  
      return colonConverter.convert(
        files.pathJoin(
          serveRoot,
          // XXX or should everything in this API use slash already?
          files.convertToStandardPath(pathInPackage, true)));
    },
  
    displayName() {
      return this.name === null ? 'the app' : this.name;
    }
  });
  
  exports.Isopack = Isopack;
  
  exports.OldIsopackFormatError = function () {
    // This should always be caught anywhere where it can appear (ie, anywhere
    // that isn't definitely loading something from the tropohouse).
    this.toString = function () { return "old isopack format!" };
  };
  