Merge mozilla-inbound to mozilla-central. a=merge
authorAndreea Pavel <apavel@mozilla.com>
Sat, 11 Aug 2018 13:25:30 +0300
changeset 486183 d4f49a59a42d
parent 486164 ef30529d43dd (current diff)
parent 486182 7311e3026a47 (diff)
child 486184 d0a17fc80dab
child 486187 bff59a727a39
child 486197 7468e087a90a
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
d4f49a59a42d / 63.0a1 / 20180811103606 / files
nightly linux64
d4f49a59a42d / 63.0a1 / 20180811103606 / files
nightly mac
d4f49a59a42d / 63.0a1 / 20180811103606 / files
nightly win32
d4f49a59a42d / 63.0a1 / 20180811103606 / files
nightly win64
d4f49a59a42d / 63.0a1 / 20180811103606 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central. a=merge
--- a/build/moz.configure/android-ndk.configure
+++ b/build/moz.configure/android-ndk.configure
@@ -15,17 +15,17 @@ option('--with-android-googlevr-sdk', na
        help='location of the Android GoogleVR SDK')
 
 
 @depends(target)
 def min_android_version(target):
     if target.cpu in ['aarch64', 'x86_64', 'mips64']:
         # 64-bit support was added in API 21.
         return '21'
-    return '9'
+    return '16'
 
 
 js_option('--with-android-version',
           nargs=1,
           help='android platform version',
           default=min_android_version)
 
 
--- a/build/moz.configure/node.configure
+++ b/build/moz.configure/node.configure
@@ -3,21 +3,44 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 option('--enable-nodejs',
        help='Require Node.js to build')
 
 
+@depends(host)
+@imports('os')
+@imports(_from='os', _import='environ')
+def node_toolchain_search_path(host):
+    # XXX partly copied from tooltool.py; should be hoisted somewhere central
+
+    # Also add in the location to which `mach bootstrap` or
+    # `mach artifact toolchain` installs clang.
+    mozbuild_state_dir = environ.get('MOZBUILD_STATE_PATH', os.path.expanduser(
+                                     os.path.join('~', '.mozbuild')))
+
+    if host.kernel == "WINNT":
+        mozbuild_node_path = os.path.join(mozbuild_state_dir, 'node')
+    else:
+        mozbuild_node_path = os.path.join(mozbuild_state_dir, 'node', 'bin')
+
+    # We still fallback to the PATH, since on OSes that don't have toolchain
+    # artifacts available to download, Node may be coming from $PATH.
+    path = [environ.get('PATH')]
+    updated_path = [mozbuild_node_path] + path
+
+    return updated_path
+
 # "nodejs" is first in the tuple on the assumption that it's only likely to
 # exist on systems (probably linux distros) where there is a program in the path
 # called "node" that does something else.
 nodejs = check_prog('NODEJS', ('nodejs', 'node',),
-                    allow_missing=True)
+                    allow_missing=True, paths=node_toolchain_search_path)
 
 
 @depends_if(nodejs)
 @checking('for node.js version')
 @imports('os')
 @imports('re')
 @imports('subprocess')
 def nodejs_version(node):
--- a/devtools/client/debugger/new/README.mozilla
+++ b/devtools/client/debugger/new/README.mozilla
@@ -1,13 +1,13 @@
 This is the debugger.html project output.
 See https://github.com/devtools-html/debugger.html
 
-Version 80
+Version 81
 
-Comparison: https://github.com/devtools-html/debugger.html/compare/release-79...release-80
+Comparison: https://github.com/devtools-html/debugger.html/compare/release-80...release-81
 
 Packages:
 - babel-plugin-transform-es2015-modules-commonjs @6.26.2
 - babel-preset-react @6.24.1
 - react @16.4.1
 - react-dom @16.4.1
 - webpack @3.12.0
--- a/devtools/client/debugger/new/src/actions/index.js
+++ b/devtools/client/debugger/new/src/actions/index.js
@@ -55,16 +55,20 @@ var quickOpen = _interopRequireWildcard(
 var _sourceTree = require("./source-tree");
 
 var sourceTree = _interopRequireWildcard(_sourceTree);
 
 var _sources = require("./sources/index");
 
 var sources = _interopRequireWildcard(_sources);
 
+var _tabs = require("./tabs");
+
+var tabs = _interopRequireWildcard(_tabs);
+
 var _debuggee = require("./debuggee");
 
 var debuggee = _interopRequireWildcard(_debuggee);
 
 var _toolbox = require("./toolbox");
 
 var toolbox = _interopRequireWildcard(_toolbox);
 
@@ -77,16 +81,17 @@ function _interopRequireWildcard(obj) { 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 exports.default = { ...navigation,
   ...breakpoints,
   ...expressions,
   ...eventListeners,
   ...sources,
+  ...tabs,
   ...pause,
   ...ui,
   ...fileSearch,
   ...ast,
   ...coverage,
   ...projectTextSearch,
   ...replay,
   ...quickOpen,
--- a/devtools/client/debugger/new/src/actions/moz.build
+++ b/devtools/client/debugger/new/src/actions/moz.build
@@ -20,11 +20,12 @@ DevToolsModules(
     'file-search.js',
     'index.js',
     'navigation.js',
     'preview.js',
     'project-text-search.js',
     'quick-open.js',
     'replay.js',
     'source-tree.js',
+    'tabs.js',
     'toolbox.js',
     'ui.js',
 )
--- a/devtools/client/debugger/new/src/actions/sources/index.js
+++ b/devtools/client/debugger/new/src/actions/sources/index.js
@@ -57,21 +57,9 @@ var _select = require("./select");
 Object.keys(_select).forEach(function (key) {
   if (key === "default" || key === "__esModule") return;
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _select[key];
     }
   });
-});
-
-var _tabs = require("./tabs");
-
-Object.keys(_tabs).forEach(function (key) {
-  if (key === "default" || key === "__esModule") return;
-  Object.defineProperty(exports, key, {
-    enumerable: true,
-    get: function () {
-      return _tabs[key];
-    }
-  });
 });
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/moz.build
+++ b/devtools/client/debugger/new/src/actions/sources/moz.build
@@ -9,10 +9,9 @@ DIRS += [
 
 DevToolsModules(
     'blackbox.js',
     'index.js',
     'loadSourceText.js',
     'newSources.js',
     'prettyPrint.js',
     'select.js',
-    'tabs.js',
 )
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -48,18 +48,24 @@ function loadSourceMaps(sources) {
   return async function ({
     dispatch,
     sourceMaps
   }) {
     if (!sourceMaps) {
       return;
     }
 
-    const originalSources = await Promise.all(sources.map(source => dispatch(loadSourceMap(source.id))));
-    await dispatch(newSources((0, _lodash.flatten)(originalSources)));
+    let originalSources = await Promise.all(sources.map(({
+      id
+    }) => dispatch(loadSourceMap(id))));
+    originalSources = (0, _lodash.flatten)(originalSources).filter(Boolean);
+
+    if (originalSources.length > 0) {
+      await dispatch(newSources(originalSources));
+    }
   };
 }
 /**
  * @memberof actions/sources
  * @static
  */
 
 
@@ -180,30 +186,30 @@ function newSource(source) {
   };
 }
 
 function newSources(sources) {
   return async ({
     dispatch,
     getState
   }) => {
-    const filteredSources = sources.filter(source => source && !(0, _selectors.getSource)(getState(), source.id));
+    sources = sources.filter(source => !(0, _selectors.getSource)(getState(), source.id));
 
-    if (filteredSources.length == 0) {
+    if (sources.length == 0) {
       return;
     }
 
     dispatch({
       type: "ADD_SOURCES",
-      sources: filteredSources
+      sources: sources
     });
 
-    for (const source of filteredSources) {
+    for (const source of sources) {
       dispatch(checkSelectedSource(source.id));
       dispatch(checkPendingBreakpoints(source.id));
     }
 
-    await dispatch(loadSourceMaps(filteredSources)); // We would like to restore the blackboxed state
+    await dispatch(loadSourceMaps(sources)); // We would like to restore the blackboxed state
     // after loading all states to make sure the correctness.
 
-    await dispatch(restoreBlackBoxedSources(filteredSources));
+    await dispatch(restoreBlackBoxedSources(sources));
   };
 }
\ No newline at end of file
--- a/devtools/client/debugger/new/src/actions/sources/select.js
+++ b/devtools/client/debugger/new/src/actions/sources/select.js
@@ -15,17 +15,17 @@ exports.jumpToMappedSelectedLocation = j
 var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
 var _ast = require("../ast");
 
 var _ui = require("../ui");
 
 var _prettyPrint = require("./prettyPrint");
 
-var _tabs = require("./tabs");
+var _tabs = require("../tabs");
 
 var _loadSourceText = require("./loadSourceText");
 
 var _prefs = require("../../utils/prefs");
 
 var _source = require("../../utils/source");
 
 var _location = require("../../utils/location");
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/actions/tabs.js
@@ -0,0 +1,91 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.addTab = addTab;
+exports.moveTab = moveTab;
+exports.closeTab = closeTab;
+exports.closeTabs = closeTabs;
+
+var _editor = require("../utils/editor/index");
+
+var _sources = require("./sources/index");
+
+var _selectors = require("../selectors/index");
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * Redux actions for the editor tabs
+ * @module actions/tabs
+ */
+function addTab(url, tabIndex) {
+  return {
+    type: "ADD_TAB",
+    url,
+    tabIndex
+  };
+}
+
+function moveTab(url, tabIndex) {
+  return {
+    type: "MOVE_TAB",
+    url,
+    tabIndex
+  };
+}
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+
+
+function closeTab(url) {
+  return ({
+    dispatch,
+    getState,
+    client
+  }) => {
+    (0, _editor.removeDocument)(url);
+    const tabs = (0, _selectors.removeSourceFromTabList)((0, _selectors.getSourceTabs)(getState()), url);
+    const sourceId = (0, _selectors.getNewSelectedSourceId)(getState(), tabs);
+    dispatch({
+      type: "CLOSE_TAB",
+      url,
+      tabs
+    });
+    dispatch((0, _sources.selectSource)(sourceId));
+  };
+}
+/**
+ * @memberof actions/tabs
+ * @static
+ */
+
+
+function closeTabs(urls) {
+  return ({
+    dispatch,
+    getState,
+    client
+  }) => {
+    urls.forEach(url => {
+      const source = (0, _selectors.getSourceByURL)(getState(), url);
+
+      if (source) {
+        (0, _editor.removeDocument)(source.id);
+      }
+    });
+    const tabs = (0, _selectors.removeSourcesFromTabList)((0, _selectors.getSourceTabs)(getState()), urls);
+    dispatch({
+      type: "CLOSE_TABS",
+      urls,
+      tabs
+    });
+    const sourceId = (0, _selectors.getNewSelectedSourceId)(getState(), tabs);
+    dispatch((0, _sources.selectSource)(sourceId));
+  };
+}
\ No newline at end of file
--- a/devtools/client/debugger/new/src/components/Editor/Tab.js
+++ b/devtools/client/debugger/new/src/components/Editor/Tab.js
@@ -160,17 +160,17 @@ class Tab extends _react.PureComponent {
     const className = (0, _classnames2.default)("source-tab", {
       active,
       pretty: isPrettyCode
     });
     const path = (0, _source.getDisplayPath)(source, tabSources);
     return _react2.default.createElement("div", {
       className: className,
       key: sourceId,
-      onMouseUp: handleTabClick,
+      onClick: handleTabClick,
       onContextMenu: e => this.onTabContextMenu(e, sourceId),
       title: (0, _source.getFileURL)(source)
     }, _react2.default.createElement(_SourceIcon2.default, {
       source: source,
       shouldHide: icon => ["file", "javascript"].includes(icon)
     }), _react2.default.createElement("div", {
       className: "filename"
     }, (0, _source.getTruncatedFileName)(source), path && _react2.default.createElement("span", null, `../${path}/..`)), _react2.default.createElement(_Button.CloseButton, {
--- a/devtools/client/debugger/new/src/components/Editor/index.js
+++ b/devtools/client/debugger/new/src/components/Editor/index.js
@@ -177,17 +177,17 @@ class Editor extends _react.PureComponen
       }
 
       if (gutter === "CodeMirror-foldgutter") {
         return;
       }
 
       const sourceLine = (0, _editor.toSourceLine)(selectedSource.id, line);
 
-      if (ev.altKey) {
+      if (ev.metaKey) {
         return continueToHere(sourceLine);
       }
 
       if (ev.shiftKey) {
         return addOrToggleDisabledBreakpoint(sourceLine);
       }
 
       return toggleBreakpointsAtLine(sourceLine);
@@ -418,17 +418,18 @@ class Editor extends _react.PureComponen
     } = this.state;
 
     if (!editor || !nextProps.selectedSource || !nextProps.selectedLocation || !nextProps.selectedLocation.line || !(0, _source.isLoaded)(nextProps.selectedSource)) {
       return false;
     }
 
     const isFirstLoad = (!selectedSource || !(0, _source.isLoaded)(selectedSource)) && (0, _source.isLoaded)(nextProps.selectedSource);
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
-    return isFirstLoad || locationChanged;
+    const symbolsChanged = nextProps.symbols != this.props.symbols;
+    return isFirstLoad || locationChanged || symbolsChanged;
   }
 
   scrollToLocation(nextProps) {
     const {
       editor
     } = this.state;
     const {
       selectedLocation,
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTree.js
@@ -157,18 +157,17 @@ class SourcesTree extends _react.Compone
       className: "sources-clear-root-container"
     }, _react2.default.createElement("button", {
       className: "sources-clear-root",
       onClick: () => this.props.clearProjectDirectoryRoot(),
       title: L10N.getStr("removeDirectoryRoot.label")
     }, _react2.default.createElement(_Svg2.default, {
       name: "home"
     }), _react2.default.createElement(_Svg2.default, {
-      name: "breadcrumb",
-      "class": true
+      name: "breadcrumb"
     }), _react2.default.createElement("span", {
       className: "sources-clear-root-label"
     }, rootLabel)));
   }
 
   renderTree() {
     const {
       expanded
--- a/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
+++ b/devtools/client/debugger/new/src/components/PrimaryPanes/SourcesTreeItem.js
@@ -120,17 +120,17 @@ class SourceTreeItem extends _react.Comp
     if (item.path === "webpack://") {
       return _react2.default.createElement(_Svg2.default, {
         name: "webpack"
       });
     } else if (item.path === "ng://") {
       return _react2.default.createElement(_Svg2.default, {
         name: "angular"
       });
-    } else if (item.path === "moz-extension://") {
+    } else if (item.path.startsWith("moz-extension://") && depth === 0) {
       return _react2.default.createElement("img", {
         className: "extension"
       });
     }
 
     if (depth === 0 && projectRoot === "") {
       return _react2.default.createElement("img", {
         className: (0, _classnames2.default)("domain", {
@@ -171,19 +171,16 @@ class SourceTreeItem extends _react.Comp
   renderItemName(name) {
     switch (name) {
       case "ng://":
         return "Angular";
 
       case "webpack://":
         return "Webpack";
 
-      case "moz-extension://":
-        return L10N.getStr("extensionsText");
-
       default:
         return name;
     }
   }
 
   render() {
     const {
       item,
--- a/devtools/client/debugger/new/src/reducers/index.js
+++ b/devtools/client/debugger/new/src/reducers/index.js
@@ -11,16 +11,20 @@ var _expressions2 = _interopRequireDefau
 var _eventListeners = require("./event-listeners");
 
 var _eventListeners2 = _interopRequireDefault(_eventListeners);
 
 var _sources = require("./sources");
 
 var _sources2 = _interopRequireDefault(_sources);
 
+var _tabs = require("./tabs");
+
+var _tabs2 = _interopRequireDefault(_tabs);
+
 var _breakpoints = require("./breakpoints");
 
 var _breakpoints2 = _interopRequireDefault(_breakpoints);
 
 var _pendingBreakpoints = require("./pending-breakpoints");
 
 var _pendingBreakpoints2 = _interopRequireDefault(_pendingBreakpoints);
 
@@ -77,16 +81,17 @@ function _interopRequireDefault(obj) { r
 /**
  * Reducer index
  * @module reducers/index
  */
 exports.default = {
   expressions: _expressions2.default,
   eventListeners: _eventListeners2.default,
   sources: _sources2.default,
+  tabs: _tabs2.default,
   breakpoints: _breakpoints2.default,
   pendingBreakpoints: _pendingBreakpoints2.default,
   asyncRequests: _asyncRequests2.default,
   pause: _pause2.default,
   ui: _ui2.default,
   fileSearch: _fileSearch2.default,
   ast: _ast2.default,
   coverage: _coverage2.default,
--- a/devtools/client/debugger/new/src/reducers/moz.build
+++ b/devtools/client/debugger/new/src/reducers/moz.build
@@ -19,10 +19,11 @@ DevToolsModules(
     'index.js',
     'pause.js',
     'pending-breakpoints.js',
     'project-text-search.js',
     'quick-open.js',
     'replay.js',
     'source-tree.js',
     'sources.js',
+    'tabs.js',
     'ui.js',
 )
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -1,62 +1,52 @@
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.getSelectedSource = exports.getSelectedLocation = exports.getSourcesForTabs = exports.getSourceTabs = exports.getTabs = undefined;
+exports.getSelectedSource = exports.getSelectedLocation = exports.getSourceCount = undefined;
 exports.initialSourcesState = initialSourcesState;
 exports.createSource = createSource;
-exports.removeSourceFromTabList = removeSourceFromTabList;
-exports.removeSourcesFromTabList = removeSourcesFromTabList;
 exports.getBlackBoxList = getBlackBoxList;
-exports.getNewSelectedSourceId = getNewSelectedSourceId;
 exports.getSource = getSource;
 exports.getSourceFromId = getSourceFromId;
 exports.getSourceByURL = getSourceByURL;
 exports.getGeneratedSource = getGeneratedSource;
 exports.getPendingSelectedLocation = getPendingSelectedLocation;
 exports.getPrettySource = getPrettySource;
 exports.hasPrettySource = hasPrettySource;
+exports.getSourceByUrlInSources = getSourceByUrlInSources;
 exports.getSourceInSources = getSourceInSources;
 exports.getSources = getSources;
+exports.getUrls = getUrls;
 exports.getSourceList = getSourceList;
-exports.getSourceCount = getSourceCount;
 
 var _reselect = require("devtools/client/debugger/new/dist/vendors").vendored["reselect"];
 
-var _lodashMove = require("devtools/client/debugger/new/dist/vendors").vendored["lodash-move"];
-
-var _lodashMove2 = _interopRequireDefault(_lodashMove);
-
 var _source = require("../utils/source");
 
 var _devtoolsSourceMap = require("devtools/client/shared/source-map/index.js");
 
-var _lodash = require("devtools/client/shared/vendor/lodash");
-
 var _prefs = require("../utils/prefs");
 
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 /**
  * Sources reducer
  * @module reducers/sources
  */
 function initialSourcesState() {
   return {
     sources: {},
+    urls: {},
     selectedLocation: undefined,
-    pendingSelectedLocation: _prefs.prefs.pendingSelectedLocation,
-    tabs: restoreTabs()
+    pendingSelectedLocation: _prefs.prefs.pendingSelectedLocation
   };
 }
 
 function createSource(source) {
   return {
     id: undefined,
     url: undefined,
     sourceMapURL: undefined,
@@ -120,38 +110,16 @@ function update(state = initialSourcesSt
         url: action.url,
         line: action.line
       };
       _prefs.prefs.pendingSelectedLocation = location;
       return { ...state,
         pendingSelectedLocation: location
       };
 
-    case "ADD_TAB":
-      return { ...state,
-        tabs: updateTabList(state.tabs, action.url)
-      };
-
-    case "MOVE_TAB":
-      return { ...state,
-        tabs: updateTabList(state.tabs, action.url, action.tabIndex)
-      };
-
-    case "CLOSE_TAB":
-      _prefs.prefs.tabs = action.tabs;
-      return { ...state,
-        tabs: action.tabs
-      };
-
-    case "CLOSE_TABS":
-      _prefs.prefs.tabs = action.tabs;
-      return { ...state,
-        tabs: action.tabs
-      };
-
     case "LOAD_SOURCE_TEXT":
       return setSourceTextProps(state, action);
 
     case "BLACKBOX":
       if (action.status === "done") {
         const {
           id,
           url
@@ -223,55 +191,28 @@ function updateSource(state, source) {
   if (!source.id) {
     return state;
   }
 
   const existingSource = state.sources[source.id];
   const updatedSource = existingSource ? { ...existingSource,
     ...source
   } : createSource(source);
+  const existingUrls = state.urls[source.url];
+  const urls = existingUrls ? [...existingUrls, source.id] : [source.id];
   return { ...state,
     sources: { ...state.sources,
       [source.id]: updatedSource
+    },
+    urls: { ...state.urls,
+      [source.url]: urls
     }
   };
 }
 
-function removeSourceFromTabList(tabs, url) {
-  return tabs.filter(tab => tab !== url);
-}
-
-function removeSourcesFromTabList(tabs, urls) {
-  return urls.reduce((t, url) => removeSourceFromTabList(t, url), tabs);
-}
-
-function restoreTabs() {
-  const prefsTabs = _prefs.prefs.tabs || [];
-  return prefsTabs;
-}
-/**
- * Adds the new source to the tab list if it is not already there
- * @memberof reducers/sources
- * @static
- */
-
-
-function updateTabList(tabs, url, newIndex) {
-  const currentIndex = tabs.indexOf(url);
-
-  if (currentIndex === -1) {
-    tabs = [url, ...tabs];
-  } else if (newIndex !== undefined) {
-    tabs = (0, _lodashMove2.default)(tabs, currentIndex, newIndex);
-  }
-
-  _prefs.prefs.tabs = tabs;
-  return tabs;
-}
-
 function updateBlackBoxList(url, isBlackBoxed) {
   const tabs = getBlackBoxList();
   const i = tabs.indexOf(url);
 
   if (i >= 0) {
     if (!isBlackBoxed) {
       tabs.splice(i, 1);
     }
@@ -279,69 +220,16 @@ function updateBlackBoxList(url, isBlack
     tabs.push(url);
   }
 
   _prefs.prefs.tabsBlackBoxed = tabs;
 }
 
 function getBlackBoxList() {
   return _prefs.prefs.tabsBlackBoxed || [];
-}
-/**
- * Gets the next tab to select when a tab closes. Heuristics:
- * 1. if the selected tab is available, it remains selected
- * 2. if it is gone, the next available tab to the left should be active
- * 3. if the first tab is active and closed, select the second tab
- *
- * @memberof reducers/sources
- * @static
- */
-
-
-function getNewSelectedSourceId(state, availableTabs) {
-  const selectedLocation = state.sources.selectedLocation;
-
-  if (!selectedLocation) {
-    return "";
-  }
-
-  const selectedTab = getSource(state, selectedLocation.sourceId);
-
-  if (!selectedTab) {
-    return "";
-  }
-
-  if (availableTabs.includes(selectedTab.url)) {
-    const sources = state.sources.sources;
-
-    if (!sources) {
-      return "";
-    }
-
-    const selectedSource = getSourceByURL(state, selectedTab.url);
-
-    if (selectedSource) {
-      return selectedSource.id;
-    }
-
-    return "";
-  }
-
-  const tabUrls = state.sources.tabs;
-  const leftNeighborIndex = Math.max(tabUrls.indexOf(selectedTab.url) - 1, 0);
-  const lastAvailbleTabIndex = availableTabs.length - 1;
-  const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex);
-  const availableTab = availableTabs[newSelectedTabIndex];
-  const tabSource = getSourceByUrlInSources(state.sources.sources, availableTab);
-
-  if (tabSource) {
-    return tabSource.id;
-  }
-
-  return "";
 } // Selectors
 // Unfortunately, it's really hard to make these functions accept just
 // the state that we care about and still type it with Flow. The
 // problem is that we want to re-export all selectors from a single
 // module for the UI, and all of those selectors should take the
 // top-level app state, so we'd have to "wrap" them to automatically
 // pick off the piece of state we're interested in. It's impossible
 // (right now) to type those wrapped functions.
@@ -353,17 +241,17 @@ function getSource(state, id) {
   return getSourceInSources(getSources(state), id);
 }
 
 function getSourceFromId(state, id) {
   return getSourcesState(state).sources[id];
 }
 
 function getSourceByURL(state, url) {
-  return getSourceByUrlInSources(state.sources.sources, url);
+  return getSourceByUrlInSources(getSources(state), getUrls(state), url);
 }
 
 function getGeneratedSource(state, source) {
   if (!(0, _devtoolsSourceMap.isOriginalId)(source.id)) {
     return source;
   }
 
   return getSourceFromId(state, (0, _devtoolsSourceMap.originalToGeneratedId)(source.id));
@@ -382,45 +270,51 @@ function getPrettySource(state, id) {
 
   return getSourceByURL(state, (0, _source.getPrettySourceURL)(source.url));
 }
 
 function hasPrettySource(state, id) {
   return !!getPrettySource(state, id);
 }
 
-function getSourceByUrlInSources(sources, url) {
-  if (!url) {
+function getSourceByUrlInSources(sources, urls, url) {
+  const foundSources = getSourcesByUrlInSources(sources, urls, url);
+
+  if (!foundSources) {
     return null;
   }
 
-  return (0, _lodash.find)(sources, source => source.url === url);
+  return foundSources[0];
+}
+
+function getSourcesByUrlInSources(sources, urls, url) {
+  if (!url || !urls[url]) {
+    return [];
+  }
+
+  return urls[url].map(id => sources[id]);
 }
 
 function getSourceInSources(sources, id) {
   return sources[id];
 }
 
 function getSources(state) {
   return state.sources.sources;
 }
 
+function getUrls(state) {
+  return state.sources.urls;
+}
+
 function getSourceList(state) {
   return Object.values(getSources(state));
 }
 
-function getSourceCount(state) {
-  return Object.keys(getSources(state)).length;
-}
-
-const getTabs = exports.getTabs = (0, _reselect.createSelector)(getSourcesState, sources => sources.tabs);
-const getSourceTabs = exports.getSourceTabs = (0, _reselect.createSelector)(getTabs, getSources, (tabs, sources) => tabs.filter(tab => getSourceByUrlInSources(sources, tab)));
-const getSourcesForTabs = exports.getSourcesForTabs = (0, _reselect.createSelector)(getSourceTabs, getSources, (tabs, sources) => {
-  return tabs.map(tab => getSourceByUrlInSources(sources, tab)).filter(source => source);
-});
+const getSourceCount = exports.getSourceCount = (0, _reselect.createSelector)(getSources, sources => Object.keys(sources).length);
 const getSelectedLocation = exports.getSelectedLocation = (0, _reselect.createSelector)(getSourcesState, sources => sources.selectedLocation);
 const getSelectedSource = exports.getSelectedSource = (0, _reselect.createSelector)(getSelectedLocation, getSources, (selectedLocation, sources) => {
   if (!selectedLocation) {
     return;
   }
 
   return sources[selectedLocation.sourceId];
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/reducers/tabs.js
@@ -0,0 +1,143 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.getSourcesForTabs = exports.getSourceTabs = exports.getTabs = undefined;
+exports.removeSourceFromTabList = removeSourceFromTabList;
+exports.removeSourcesFromTabList = removeSourcesFromTabList;
+exports.getNewSelectedSourceId = getNewSelectedSourceId;
+
+var _reselect = require("devtools/client/debugger/new/dist/vendors").vendored["reselect"];
+
+var _lodashMove = require("devtools/client/debugger/new/dist/vendors").vendored["lodash-move"];
+
+var _lodashMove2 = _interopRequireDefault(_lodashMove);
+
+var _prefs = require("../utils/prefs");
+
+var _sources = require("./sources");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * Tabs reducer
+ * @module reducers/tabs
+ */
+function update(state = _prefs.prefs.tabs || [], action) {
+  switch (action.type) {
+    case "ADD_TAB":
+      return updateTabList(state, action.url);
+
+    case "MOVE_TAB":
+      return updateTabList(state, action.url, action.tabIndex);
+
+    case "CLOSE_TAB":
+    case "CLOSE_TABS":
+      _prefs.prefs.tabs = action.tabs;
+      return action.tabs;
+
+    default:
+      return state;
+  }
+}
+
+function removeSourceFromTabList(tabs, url) {
+  return tabs.filter(tab => tab !== url);
+}
+
+function removeSourcesFromTabList(tabs, urls) {
+  return urls.reduce((t, url) => removeSourceFromTabList(t, url), tabs);
+}
+/**
+ * Adds the new source to the tab list if it is not already there
+ * @memberof reducers/tabs
+ * @static
+ */
+
+
+function updateTabList(tabs, url, newIndex) {
+  const currentIndex = tabs.indexOf(url);
+
+  if (currentIndex === -1) {
+    tabs = [url, ...tabs];
+  } else if (newIndex !== undefined) {
+    tabs = (0, _lodashMove2.default)(tabs, currentIndex, newIndex);
+  }
+
+  _prefs.prefs.tabs = tabs;
+  return tabs;
+}
+/**
+ * Gets the next tab to select when a tab closes. Heuristics:
+ * 1. if the selected tab is available, it remains selected
+ * 2. if it is gone, the next available tab to the left should be active
+ * 3. if the first tab is active and closed, select the second tab
+ *
+ * @memberof reducers/tabs
+ * @static
+ */
+
+
+function getNewSelectedSourceId(state, availableTabs) {
+  const selectedLocation = state.sources.selectedLocation;
+
+  if (!selectedLocation) {
+    return "";
+  }
+
+  const selectedTab = (0, _sources.getSource)(state, selectedLocation.sourceId);
+
+  if (!selectedTab) {
+    return "";
+  }
+
+  if (availableTabs.includes(selectedTab.url)) {
+    const sources = state.sources.sources;
+
+    if (!sources) {
+      return "";
+    }
+
+    const selectedSource = (0, _sources.getSourceByURL)(state, selectedTab.url);
+
+    if (selectedSource) {
+      return selectedSource.id;
+    }
+
+    return "";
+  }
+
+  const tabUrls = state.tabs;
+  const leftNeighborIndex = Math.max(tabUrls.indexOf(selectedTab.url) - 1, 0);
+  const lastAvailbleTabIndex = availableTabs.length - 1;
+  const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex);
+  const availableTab = availableTabs[newSelectedTabIndex];
+  const tabSource = (0, _sources.getSourceByUrlInSources)((0, _sources.getSources)(state), (0, _sources.getUrls)(state), availableTab);
+
+  if (tabSource) {
+    return tabSource.id;
+  }
+
+  return "";
+} // Selectors
+// Unfortunately, it's really hard to make these functions accept just
+// the state that we care about and still type it with Flow. The
+// problem is that we want to re-export all selectors from a single
+// module for the UI, and all of those selectors should take the
+// top-level app state, so we'd have to "wrap" them to automatically
+// pick off the piece of state we're interested in. It's impossible
+// (right now) to type those wrapped functions.
+
+
+const getTabs = exports.getTabs = state => state.tabs;
+
+const getSourceTabs = exports.getSourceTabs = (0, _reselect.createSelector)(getTabs, _sources.getSources, _sources.getUrls, (tabs, sources, urls) => tabs.filter(tab => (0, _sources.getSourceByUrlInSources)(sources, urls, tab)));
+const getSourcesForTabs = exports.getSourcesForTabs = (0, _reselect.createSelector)(getSourceTabs, _sources.getSources, _sources.getUrls, (tabs, sources, urls) => {
+  return tabs.map(tab => (0, _sources.getSourceByUrlInSources)(sources, urls, tab)).filter(source => source);
+});
+exports.default = update;
\ No newline at end of file
--- a/devtools/client/debugger/new/src/selectors/index.js
+++ b/devtools/client/debugger/new/src/selectors/index.js
@@ -23,16 +23,28 @@ Object.keys(_sources).forEach(function (
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _sources[key];
     }
   });
 });
 
+var _tabs = require("../reducers/tabs");
+
+Object.keys(_tabs).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _tabs[key];
+    }
+  });
+});
+
 var _pause = require("../reducers/pause");
 
 Object.keys(_pause).forEach(function (key) {
   if (key === "default" || key === "__esModule") return;
   Object.defineProperty(exports, key, {
     enumerable: true,
     get: function () {
       return _pause[key];
--- a/devtools/client/debugger/new/src/utils/editor/index.js
+++ b/devtools/client/debugger/new/src/utils/editor/index.js
@@ -188,19 +188,23 @@ function scrollToColumn(codeMirror, line
 
 function isVisible(codeMirror, top, left) {
   function withinBounds(x, min, max) {
     return x >= min && x <= max;
   }
 
   const scrollArea = codeMirror.getScrollInfo();
   const charWidth = codeMirror.defaultCharWidth();
-  const inXView = withinBounds(left, scrollArea.left, scrollArea.left + (scrollArea.clientWidth - 30) - charWidth);
   const fontHeight = codeMirror.defaultTextHeight();
-  const inYView = withinBounds(top, scrollArea.top, scrollArea.top + scrollArea.clientHeight - fontHeight);
+  const {
+    scrollTop,
+    scrollLeft
+  } = codeMirror.doc;
+  const inXView = withinBounds(left, scrollLeft, scrollLeft + (scrollArea.clientWidth - 30) - charWidth);
+  const inYView = withinBounds(top, scrollTop, scrollTop + scrollArea.clientHeight - fontHeight);
   return inXView && inYView;
 }
 
 function markText(_editor, className, {
   start,
   end
 }) {
   return _editor.codeMirror.markText({
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-console-async.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-console-async.js
@@ -23,30 +23,42 @@ function getSplitConsole(dbg) {
 
 function findMessages(win, query) {
   return Array.prototype.filter.call(
     win.document.querySelectorAll(".message"),
     e => e.innerText.includes(query)
   )
 }
 
+async function hasMessage(dbg, msg) {
+  const webConsole = await dbg.toolbox.getPanel("webconsole")
+  return waitFor(async () => findMessages(
+    webConsole._frameWindow,
+    msg
+  ).length > 0)
+}
+
 add_task(async function() {
   Services.prefs.setBoolPref("devtools.toolbox.splitconsoleEnabled", true);
+  Services.prefs.setBoolPref("devtools.map-await-expression", true);
+
   const dbg = await initDebugger("doc-script-switching.html");
 
   await selectSource(dbg, "switching-01");
 
   // open the console
   await getSplitConsole(dbg);
   ok(dbg.toolbox.splitConsole, "Split console is shown.");
 
   const webConsole = await dbg.toolbox.getPanel("webconsole")
   const jsterm = webConsole.hud.jsterm;
 
-  await jsterm.execute(`let sleep = async (time, v) => new Promise(
-    res => setTimeout(() => res(v+'!!!'), time)
-  )`);
+  await jsterm.execute(`
+    let sleep = async (time, v) => new Promise(
+      res => setTimeout(() => res(v+'!!!'), time)
+    )
+  `);
 
-  await jsterm.execute(`await sleep(200, "DONE")`)
+  await hasMessage(dbg, "sleep");
 
-  await waitFor(async () => findMessages(webConsole._frameWindow, "DONE!!!").length > 0)
-
+  await jsterm.execute(`await sleep(200, "DONE")`);
+  await hasMessage(dbg, "DONE!!!");
 });
--- a/devtools/shared/adb/adb-binary.js
+++ b/devtools/shared/adb/adb-binary.js
@@ -1,96 +1,110 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { dumpn } = require("devtools/shared/DevToolsUtils");
+
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
 loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "FileUtils",
                          "resource://gre/modules/FileUtils.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil",
                          "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyGetter(this, "UNPACKED_ROOT_PATH", () => {
   return OS.Path.join(OS.Constants.Path.localProfileDir, "adb");
 });
+loader.lazyGetter(this, "EXTENSION_ID", () => {
+  return Services.prefs.getCharPref("devtools.remote.adb.extensionID");
+});
+loader.lazyGetter(this, "ADB_BINARY_PATH", () => {
+  let adbBinaryPath = OS.Path.join(UNPACKED_ROOT_PATH, "adb");
+  if (Services.appinfo.OS === "WINNT") {
+    adbBinaryPath += ".exe";
+  }
+  return adbBinaryPath;
+});
 
-// FIXME: Bug 1481691 - Read the extension ID from a pref.
-const EXTENSION_ID = "adb@mozilla.org";
+const MANIFEST = "manifest.json";
 
-async function getAdbInfo(adbUri) {
+/**
+ * Read contents from a given uri in the devtools-adb-extension and parse the
+ * contents as JSON.
+ */
+async function readFromExtension(fileUri) {
   return new Promise(resolve => {
     NetUtil.asyncFetch({
-      uri: adbUri,
+      uri: fileUri,
       loadUsingSystemPrincipal: true
     }, (input) => {
       try {
         const string = NetUtil.readInputStreamToString(input, input.available());
         resolve(JSON.parse(string));
       } catch (e) {
-        console.log(`Could not read adb.json in the extension: ${e}`);
+        dumpn(`Could not read ${fileUri} in the extension: ${e}`);
         resolve(null);
       }
     });
   });
 }
 
 /**
  * Unpack file from the extension.
  * Uses NetUtil to read and write, since it's required for reading.
  *
  * @param {string} file
- *        The path name of the file in the extension, such as "win32/adb.exe".
- *        This has to have a '/' in the path string.
+ *        The path name of the file in the extension.
  */
 async function unpackFile(file) {
   const policy = ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
   if (!policy) {
     return;
   }
 
   // Assumes that destination dir already exists.
-  const filePath = OS.Path.join(UNPACKED_ROOT_PATH, file.split("/")[1]);
+  const basePath = file.substring(file.lastIndexOf("/") + 1);
+  const filePath = OS.Path.join(UNPACKED_ROOT_PATH, basePath);
   await new Promise((resolve, reject) => {
     NetUtil.asyncFetch({
       uri: policy.getURL(file),
       loadUsingSystemPrincipal: true
     }, (input) => {
       try {
         // Since we have to use NetUtil to read, probably it's okay to use for
         // writing, rather than bouncing to OS.File...?
         const outputFile = new FileUtils.File(filePath);
         const output = FileUtils.openAtomicFileOutputStream(outputFile);
         NetUtil.asyncCopy(input, output, resolve);
       } catch (e) {
-        console.log(`Could not unpack file ${file} in the extension: ${e}`);
+        dumpn(`Could not unpack file ${file} in the extension: ${e}`);
         reject(e);
       }
     });
   });
   // Mark binaries as executable.
   await OS.File.setPermissions(filePath, { unixMode: 0o744 });
 }
 
 /**
  * Extract files in the extension into local profile directory and returns
- * the path for the adb binary.
+ * if it fails.
  */
-async function extractBinary() {
+async function extractFiles() {
   const policy = ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
   if (!policy) {
-    return null;
+    return false;
   }
   const uri = policy.getURL("adb.json");
-
-  const adbInfo = await getAdbInfo(uri);
+  const adbInfo = await readFromExtension(uri);
   if (!adbInfo) {
-    return null;
+    return false;
   }
 
   let filesForAdb;
   try {
     // The adbInfo is an object looks like this;
     //
     //  {
     //    "Linux": {
@@ -101,45 +115,108 @@ async function extractBinary() {
     //        "linux64/adb"
     //      ]
     //    },
     // ...
     filesForAdb =
       // XPCOMABI looks this; x86_64-gcc3, so drop the compiler name.
       adbInfo[Services.appinfo.OS][Services.appinfo.XPCOMABI.split("-")[0]];
   } catch (e) {
-    return null;
+    return false;
   }
 
+  // manifest.json isn't in adb.json but has to be unpacked for version
+  // comparison
+  filesForAdb.push(MANIFEST);
+
   await OS.File.makeDir(UNPACKED_ROOT_PATH);
 
   for (const file of filesForAdb) {
     try {
       await unpackFile(file);
     } catch (e) {
-      return null;
+      return false;
     }
   }
 
-  let adbBinaryPath = OS.Path.join(UNPACKED_ROOT_PATH, "adb");
+  return true;
+}
+
+/**
+ * Read the manifest from inside the devtools-adb-extension.
+ * Uses NetUtil since data is packed inside the extension, not a local file.
+ */
+async function getManifestFromExtension() {
+  const policy = ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
+  if (!policy) {
+    return null;
+  }
+
+  const manifestUri = policy.getURL(MANIFEST);
+  return readFromExtension(manifestUri);
+}
+
+/**
+ * Returns whether manifest.json has already been unpacked.
+ */
+async function isManifestUnpacked() {
+  const manifestPath = OS.Path.join(UNPACKED_ROOT_PATH, MANIFEST);
+  return OS.File.exists(manifestPath);
+}
 
-  if (Services.appinfo.OS === "WINNT") {
-    adbBinaryPath += ".exe";
+/**
+ * Read the manifest from the unpacked binary directory.
+ * Uses OS.File since this is a local file.
+ */
+async function getManifestFromUnpacked() {
+  if (!await isManifestUnpacked()) {
+    throw new Error("Manifest doesn't exist at unpacked path");
+  }
+
+  const manifestPath = OS.Path.join(UNPACKED_ROOT_PATH, MANIFEST);
+  const binary = await OS.File.read(manifestPath);
+  const json = new TextDecoder().decode(binary);
+  let data;
+  try {
+    data = JSON.parse(json);
+  } catch (e) {
   }
-  return adbBinaryPath;
+  return data;
+}
+
+/**
+ * Check state of binary unpacking, including the location and manifest.
+ */
+async function isUnpacked() {
+  if (!await isManifestUnpacked()) {
+    dumpn("Needs unpacking, no manifest found");
+    return false;
+  }
+
+  const manifestInExtension = await getManifestFromExtension();
+  const unpackedManifest = await getManifestFromUnpacked();
+  if (manifestInExtension.version != unpackedManifest.version) {
+    dumpn(
+      `Needs unpacking, extension version ${manifestInExtension.version} != ` +
+      `unpacked version ${unpackedManifest.version}`
+    );
+    return false;
+  }
+  dumpn("Already unpacked");
+  return true;
 }
 
 /**
  * Get a file object for the adb binary from the 'adb@mozilla.org' extension
  * which has been already installed.
  *
  * @return {nsIFile}
  *        File object for the binary.
  */
 async function getFileForBinary() {
-  const path = await extractBinary();
-  if (!path) {
+  if (!await isUnpacked() &&
+      !await extractFiles()) {
     return null;
   }
-  console.log(`Binary path: ${path}`);
-  return new FileUtils.File(path);
+  return new FileUtils.File(ADB_BINARY_PATH);
 }
+
 exports.getFileForBinary = getFileForBinary;
--- a/devtools/shared/adb/adb-client.js
+++ b/devtools/shared/adb/adb-client.js
@@ -6,68 +6,69 @@
  * A module to track device changes
  * Adapted from adb.js at
  * https://github.com/mozilla/adbhelper/tree/f44386c2d8cb7635a7d2c5a51191c89b886f8327
  */
 
 "use strict";
 
 const { AdbSocket } = require("./adb-socket");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
 
 const OKAY = 0x59414b4f;
 const FAIL = 0x4c494146;
 
 const _sockets = [ ];
 
 // Return buffer, which differs between Gecko versions
 function getBuffer(packet) {
   return packet.buffer ? packet.buffer : packet;
 }
 
 // @param aPacket         The packet to get the length from.
 // @param aIgnoreResponse True if this packet has no OKAY/FAIL.
 // @return                A js object { length:...; data:... }
 function unpackPacket(packet, ignoreResponse) {
   const buffer = getBuffer(packet);
-  console.debug("Len buffer: " + buffer.byteLength);
+  dumpn("Len buffer: " + buffer.byteLength);
   if (buffer.byteLength === 4 && !ignoreResponse) {
-    console.debug("Packet empty");
+    dumpn("Packet empty");
     return { length: 0, data: "" };
   }
   const lengthView = new Uint8Array(buffer, ignoreResponse ? 0 : 4, 4);
   const decoder = new TextDecoder();
   const length = parseInt(decoder.decode(lengthView), 16);
   const text = new Uint8Array(buffer, ignoreResponse ? 4 : 8, length);
   return { length, data: decoder.decode(text) };
 }
 
 // Checks if the response is expected (defaults to OKAY).
 // @return true if response equals expected.
 function checkResponse(packet, expected = OKAY) {
   const buffer = getBuffer(packet);
   const view = new Uint32Array(buffer, 0, 1);
   if (view[0] == FAIL) {
-    console.debug("Response: FAIL");
+    dumpn("Response: FAIL");
   }
-  console.debug("view[0] = " + view[0]);
+  dumpn("view[0] = " + view[0]);
   return view[0] == expected;
 }
 
 // @param aCommand A protocol-level command as described in
 //  http://androidxref.com/4.0.4/xref/system/core/adb/OVERVIEW.TXT and
 //  http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
 // @return A 8 bit typed array.
 function createRequest(command) {
   let length = command.length.toString(16).toUpperCase();
   while (length.length < 4) {
     length = "0" + length;
   }
 
   const encoder = new TextEncoder();
-  console.debug("Created request: " + length + command);
+  dumpn("Created request: " + length + command);
   return encoder.encode(length + command);
 }
 
 function close() {
   _sockets.forEach(function(s) {
     s.close();
   });
 }
--- a/devtools/shared/adb/adb-device.js
+++ b/devtools/shared/adb/adb-device.js
@@ -14,39 +14,21 @@ function Device(id) {
   this.id = id;
 }
 
 Device.prototype = {
   type: "adb",
 
   shell: ADB.shell.bind(ADB),
   forwardPort: ADB.forwardPort.bind(ADB),
-  push: ADB.push.bind(ADB),
-  pull: ADB.pull.bind(ADB),
-  reboot: ADB.reboot.bind(ADB),
-  rebootRecovery: ADB.rebootRecovery.bind(ADB),
-  rebootBootloader: ADB.rebootBootloader.bind(ADB),
-
-  isRoot() {
-    return ADB.shell("id").then(stdout => {
-      if (stdout) {
-        const uid = stdout.match(/uid=(\d+)/)[1];
-        return uid == "0";
-      }
-      return false;
-    });
-  },
-
-  summonRoot() {
-    return ADB.root();
-  },
 
   getModel() {
     if (this._modelPromise) {
       return this._modelPromise;
     }
     this._modelPromise = this.shell("getprop ro.product.model")
                              .then(model => model.trim());
     return this._modelPromise;
   }
+  // push, pull were removed in Bug 1481691.
 };
 
 module.exports = Device;
--- a/devtools/shared/adb/adb-running-checker.js
+++ b/devtools/shared/adb/adb-running-checker.js
@@ -6,85 +6,86 @@
 /*
  * Uses host:version service to detect if ADB is running
  * Modified from adb-file-transfer from original ADB
  */
 
 "use strict";
 
 const client = require("./adb-client");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
 
 exports.check = async function check() {
   let socket;
   let state;
   let timerID;
   const TIMEOUT_TIME = 1000;
 
-  console.debug("Asking for host:version");
+  dumpn("Asking for host:version");
 
   return new Promise(resolve => {
     // On MacOSX connecting to a port which is not started listening gets
     // stuck (bug 1481963), to avoid the stuck, we do forcibly fail the
     // connection after |TIMEOUT_TIME| elapsed.
     timerID = setTimeout(() => {
       socket.close();
       resolve(false);
     }, TIMEOUT_TIME);
 
     function finish(returnValue) {
       clearTimeout(timerID);
       resolve(returnValue);
     }
 
     const runFSM = function runFSM(packetData) {
-      console.debug("runFSM " + state);
+      dumpn("runFSM " + state);
       switch (state) {
         case "start":
           const req = client.createRequest("host:version");
           socket.send(req);
           state = "wait-version";
           break;
         case "wait-version":
           // TODO: Actually check the version number to make sure the daemon
           //       supports the commands we want to use
           const { length, data } = client.unpackPacket(packetData);
-          console.debug("length: ", length, "data: ", data);
+          dumpn("length: ", length, "data: ", data);
           socket.close();
           const version = parseInt(data, 16);
           if (version >= 31) {
             finish(true);
           } else {
-            console.log("killing existing adb as we need version >= 31");
+            dumpn("killing existing adb as we need version >= 31");
             finish(false);
           }
           break;
         default:
-          console.debug("Unexpected State: " + state);
+          dumpn("Unexpected State: " + state);
           finish(false);
       }
     };
 
     const setupSocket = function() {
       socket.s.onerror = function(event) {
-        console.debug("running checker onerror");
+        dumpn("running checker onerror");
         finish(false);
       };
 
       socket.s.onopen = function(event) {
-        console.debug("running checker onopen");
+        dumpn("running checker onopen");
         state = "start";
         runFSM();
       };
 
       socket.s.onclose = function(event) {
-        console.debug("running checker onclose");
+        dumpn("running checker onclose");
       };
 
       socket.s.ondata = function(event) {
-        console.debug("running checker ondata");
+        dumpn("running checker ondata");
         runFSM(event.data);
       };
     };
 
     socket = client.connect();
     setupSocket();
   });
 };
--- a/devtools/shared/adb/adb-scanner.js
+++ b/devtools/shared/adb/adb-scanner.js
@@ -2,18 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const { ConnectionManager } =
   require("devtools/shared/client/connection-manager");
-const { Devices } =
-  require("devtools/shared/apps/Devices.jsm");
+const { Devices } = require("devtools/shared/apps/Devices.jsm");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
 const { RuntimeTypes } =
   require("devtools/client/webide/modules/runtime-types");
 
 const ADBScanner = {
 
   _runtimes: [],
 
   enable() {
@@ -51,19 +51,18 @@ const ADBScanner = {
     }, () => {
       this._updatingPromise = null;
     });
     return this._updatingPromise;
   },
 
   _detectRuntimes: async function(device) {
     const model = await device.getModel();
-    let detectedRuntimes = await FirefoxOSRuntime.detect(device, model);
-    this._runtimes.push(...detectedRuntimes);
-    detectedRuntimes = await FirefoxOnAndroidRuntime.detect(device, model);
+    const detectedRuntimes =
+      await FirefoxOnAndroidRuntime.detect(device, model);
     this._runtimes.push(...detectedRuntimes);
   },
 
   scan() {
     return this._updateRuntimes();
   },
 
   listRuntimes() {
@@ -97,50 +96,16 @@ Runtime.prototype = {
       connection.connect();
     });
   },
   get id() {
     return this.device.id + "|" + this._socketPath;
   },
 };
 
-// FIXME: Bug 1481691 - Drop code for support FirefoxOS.
-function FirefoxOSRuntime(device, model) {
-  Runtime.call(this, device, model, "/data/local/debugger-socket");
-}
-
-FirefoxOSRuntime.detect = async function(device, model) {
-  const runtimes = [];
-  const query = "test -f /system/b2g/b2g; echo $?";
-  let b2gExists = await device.shell(query);
-  // XXX: Sometimes we get an empty response back.  Likely a bug in our shell
-  // code in this add-on.
-  // There are also some Android devices that do not have `test` installed.
-  for (let attempts = 3; attempts > 0; attempts--) {
-    b2gExists = await device.shell(query);
-    if (b2gExists.length == 3) {
-      break;
-    }
-  }
-  if (b2gExists === "0\r\n") {
-    const runtime = new FirefoxOSRuntime(device, model);
-    console.log("Found " + runtime.name);
-    runtimes.push(runtime);
-  }
-  return runtimes;
-};
-
-FirefoxOSRuntime.prototype = Object.create(Runtime.prototype);
-
-Object.defineProperty(FirefoxOSRuntime.prototype, "name", {
-  get() {
-    return this._model || this.device.id;
-  }
-});
-
 function FirefoxOnAndroidRuntime(device, model, socketPath) {
   Runtime.call(this, device, model, socketPath);
 }
 
 // This requires Unix socket support from Firefox for Android (35+)
 FirefoxOnAndroidRuntime.detect = async function(device, model) {
   const runtimes = [];
   // A matching entry looks like:
@@ -154,17 +119,17 @@ FirefoxOnAndroidRuntime.detect = async f
   // It's possible to have multiple lines with the same path, so de-dupe them
   const socketPaths = new Set();
   for (const socketInfo of socketInfos) {
     const socketPath = socketInfo.split(" ").pop();
     socketPaths.add(socketPath);
   }
   for (const socketPath of socketPaths) {
     const runtime = new FirefoxOnAndroidRuntime(device, model, socketPath);
-    console.log("Found " + runtime.name);
+    dumpn("Found " + runtime.name);
     runtimes.push(runtime);
   }
   return runtimes;
 };
 
 FirefoxOnAndroidRuntime.prototype = Object.create(Runtime.prototype);
 
 Object.defineProperty(FirefoxOnAndroidRuntime.prototype, "name", {
--- a/devtools/shared/adb/adb-socket.js
+++ b/devtools/shared/adb/adb-socket.js
@@ -1,22 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Cu } = require("chrome");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
 
 function createTCPSocket(location, port, options) {
-  // Starting with FF57, jsm share the same global and requires some special code
   const { TCPSocket } =
     Cu.getGlobalForObject(Cu.import("resource://gre/modules/Services.jsm", {}));
 
-  // Starting with FF43, TCPSocket is now exposed via WebIDL
   return new TCPSocket(location, port, options);
 }
 
 // Creates a socket connected to the adb instance.
 // This instantiation is sync, and returns before we know if opening the
 // connection succeeds. Callers must attach handlers to the s field.
 class AdbSocket {
   constructor() {
@@ -48,17 +47,17 @@ class AdbSocket {
     for (let i = 0; i < l; i++) {
       const c = array[i];
       if (c < 32 || c > 127) {
         dbg += ".";
       } else {
         dbg += s[i];
       }
     }
-    console.debug(dbg);
+    dumpn(dbg);
   }
 
   // debugging version of tcpsocket.send()
   send(array) {
     this._hexdump(array);
 
     this.s.send(array.buffer, array.byteOffset, array.byteLength);
   }
--- a/devtools/shared/adb/adb.js
+++ b/devtools/shared/adb/adb.js
@@ -4,32 +4,27 @@
 
 // Wrapper around the ADB utility.
 
 "use strict";
 
 const { Cc, Ci } = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
 const client = require("./adb-client");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
 const { getFileForBinary } = require("./adb-binary");
 const { setTimeout } = require("resource://gre/modules/Timer.jsm");
-const { PromiseUtils } = require("resource://gre/modules/PromiseUtils.jsm");
-const { OS } = require("resource://gre/modules/osfile.jsm");
 const { Services } = require("resource://gre/modules/Services.jsm");
 loader.lazyRequireGetter(this, "check",
                          "devtools/shared/adb/adb-running-checker", true);
 
 let ready = false;
 let didRunInitially = false;
 
 const OKAY = 0x59414b4f;
-// const FAIL = 0x4c494146;
-// const STAT = 0x54415453;
-const DATA = 0x41544144;
-const DONE = 0x454e4f44;
 
 const ADB = {
   get didRunInitially() {
     return didRunInitially;
   },
   set didRunInitially(newVal) {
     didRunInitially = newVal;
   },
@@ -89,36 +84,31 @@ const ADB = {
         Services.obs.notifyObservers(null, "adb-ready");
         this.ready = true;
         resolve();
       };
 
       const isAdbRunning = await check();
       if (isAdbRunning) {
         this.didRunInitially = false;
-        console.log("Found ADB process running, not restarting");
+        dumpn("Found ADB process running, not restarting");
         onSuccessfulStart();
         return;
       }
-      console.log("Didn't find ADB process running, restarting");
+      dumpn("Didn't find ADB process running, restarting");
 
       this.didRunInitially = true;
       const process = Cc["@mozilla.org/process/util;1"]
                       .createInstance(Ci.nsIProcess);
       // FIXME: Bug 1481691 - We should avoid extracting files every time.
       const adbFile = await this.adbFilePromise;
       process.init(adbFile);
       // Hide command prompt window on Windows
-      try {
-        // startHidden is 55+
-        process.startHidden = true;
-        // noShell attribute is 58+
-        process.noShell = true;
-      } catch (e) {
-      }
+      process.startHidden = true;
+      process.noShell = true;
       const params = ["start-server"];
       let isStarted = false;
       try {
         await this._runProcess(process, params);
         isStarted = await this._waitUntil(check);
       } catch (e) {
       }
 
@@ -129,115 +119,73 @@ const ADB = {
         reject();
       }
     });
   },
 
   /**
    * Stop the ADB server, but only if we started it.  If it was started before
    * us, we return immediately.
-   *
-   * @param boolean sync
-   *        In case, we do need to kill the server, this param is passed through
-   *        to kill to determine whether it's a sync operation.
    */
-  async stop(sync) {
+  async stop() {
     if (!this.didRunInitially) {
       return; // We didn't start the server, nothing to do
     }
-    await this.kill(sync);
+    await this.kill();
     // Make sure the ADB server stops listening because kill() above doesn't
     // mean that the ADB server stops, it means that 'adb kill-server' command
     // just finished, so that it's possible that the ADB server is still alive.
     await this._waitUntil(async () => {
       return !await check();
     });
   },
 
   /**
    * Kill the ADB server.  We do this by running ADB again, passing it
    * the "kill-server" argument.
-   *
-   * @param {Boolean} sync
-   *        Whether or not to kill the server synchronously.  In general,
-   *        this should be false.  But on Windows, an add-on may fail to update
-   *        if its copy of ADB is running when Firefox tries to update it.
-   *        So add-ons who observe their own updates and kill the ADB server
-   *        beforehand should do so synchronously on Windows to make sure
-   *        the update doesn't race the killing.
    */
-  async kill(sync) {
+  async kill() {
     const process = Cc["@mozilla.org/process/util;1"]
                     .createInstance(Ci.nsIProcess);
     const adbFile = await this.adbFilePromise;
     process.init(adbFile);
     // Hide command prompt window on Windows
-    try {
-      // startHidden is 55+
-      process.startHidden = true;
-      // noShell attribute is 58+
-      process.noShell = true;
-    } catch (e) {
-    }
+    process.startHidden = true;
+    process.noShell = true;
     const params = ["kill-server"];
 
-    if (sync) {
-      process.run(true, params, params.length);
-      console.log("adb kill-server: " + process.exitValue);
-      this.ready = false;
-      this.didRunInitially = false;
-    } else {
-      const self = this;
-      process.runAsync(params, params.length, {
-        observe(subject, topic, data) {
-          switch (topic) {
-            case "process-finished":
-              console.log("adb kill-server: " + process.exitValue);
-              Services.obs.notifyObservers(null, "adb-killed");
-              self.ready = false;
-              self.didRunInitially = false;
-              break;
-            case "process-failed":
-              console.log("adb kill-server failure: " + process.exitValue);
-              // It's hard to say whether or not ADB is ready at this point,
-              // but it seems safer to assume that it isn't, so code that wants
-              // to use it later will try to restart it.
-              Services.obs.notifyObservers(null, "adb-killed");
-              self.ready = false;
-              self.didRunInitially = false;
-              break;
-          }
-        }
-      }, false);
-    }
+    process.run(true, params, params.length);
+    dumpn("adb kill-server: " + process.exitValue);
+    this.ready = false;
+    this.didRunInitially = false;
   },
 
   // Start tracking devices connecting and disconnecting from the host.
   // We can't reuse runCommand here because we keep the socket alive.
   // @return The socket used.
   trackDevices() {
-    console.log("trackDevices");
+    dumpn("trackDevices");
     const socket = client.connect();
     let waitForFirst = true;
     const devices = {};
 
     socket.s.onopen = function() {
-      console.log("trackDevices onopen");
+      dumpn("trackDevices onopen");
       Services.obs.notifyObservers(null, "adb-track-devices-start");
       const req = client.createRequest("host:track-devices");
       socket.send(req);
     };
 
     socket.s.onerror = function(event) {
-      console.log("trackDevices onerror: " + event);
+      dumpn("trackDevices onerror: " + event);
       Services.obs.notifyObservers(null, "adb-track-devices-stop");
     };
 
     socket.s.onclose = function() {
-      console.log("trackDevices onclose");
+      dumpn("trackDevices onclose");
 
       // Report all devices as disconnected
       for (const dev in devices) {
         devices[dev] = false;
         EventEmitter.emit(ADB, "device-disconnected", dev);
       }
 
       Services.obs.notifyObservers(null, "adb-track-devices-stop");
@@ -251,21 +199,21 @@ const ADB = {
                                          // or, spawn a new one
             ADB.trackDevices(); // Re-track devices
           });
         }
       }, 2000);
     };
 
     socket.s.ondata = function(event) {
-      console.log("trackDevices ondata");
+      dumpn("trackDevices ondata");
       const data = event.data;
-      console.log("length=" + data.byteLength);
+      dumpn("length=" + data.byteLength);
       const dec = new TextDecoder();
-      console.log(dec.decode(new Uint8Array(data)).trim());
+      dumpn(dec.decode(new Uint8Array(data)).trim());
 
       // check the OKAY or FAIL on first packet.
       if (waitForFirst) {
         if (!client.checkResponse(data, OKAY)) {
           socket.close();
           return;
         }
       }
@@ -303,17 +251,17 @@ const ADB = {
           }
         }
       }
     };
   },
 
   // Sends back an array of device names.
   listDevices() {
-    console.log("listDevices");
+    dumpn("listDevices");
 
     return this.runCommand("host:devices").then(
       function onSuccess(data) {
         const lines = data.split("\n");
         const res = [];
         lines.forEach(function(line) {
           if (line.length == 0) {
             return;
@@ -323,647 +271,154 @@ const ADB = {
         });
         return res;
       }
     );
   },
 
   // sends adb forward localPort devicePort
   forwardPort(localPort, devicePort) {
-    console.log("forwardPort " + localPort + " -- " + devicePort);
+    dumpn("forwardPort " + localPort + " -- " + devicePort);
     // <host-prefix>:forward:<local>;<remote>
 
     return this.runCommand("host:forward:" + localPort + ";" + devicePort)
                .then(function onSuccess(data) {
                  return data;
                });
   },
 
-  // pulls a file from the device.
-  // send "host:transport-any" why??
-  // if !OKAY, return
-  // send "sync:"
-  // if !OKAY, return
-  // send STAT + hex4(path.length) + path
-  // recv STAT + 12 bytes (3 x 32 bits: mode, size, time)
-  // send RECV + hex4(path.length) + path
-  // while(needs data):
-  //   recv DATA + hex4 + data
-  // recv DONE + hex4(0)
-  // send QUIT + hex4(0)
-  pull(from, dest) {
-    const deferred = PromiseUtils.defer();
+  // Run a shell command
+  async shell(command) {
     let state;
-    let fileData = null;
-    let currentPos = 0;
-    let chunkSize = 0;
-    let pkgData;
-    const headerArray = new Uint32Array(2);
-    let currentHeaderLength = 0;
+    let stdout = "";
 
-    const encoder = new TextEncoder();
-    let infoLengthPacket;
-
-    console.log("pulling " + from + " -> " + dest);
-
-    const shutdown = function() {
-      console.log("pull shutdown");
-      socket.close();
-      deferred.reject("BAD_RESPONSE");
-    };
+    dumpn("shell " + command);
 
-    // extract chunk data header info. to headerArray.
-    const extractChunkDataHeader = function(data) {
-      const tmpArray = new Uint8Array(headerArray.buffer);
-      for (let i = 0; i < 8 - currentHeaderLength; i++) {
-        tmpArray[currentHeaderLength + i] = data[i];
-      }
-    };
-
-    // chunk data header is 8 bytes length,
-    // the first 4 bytes: hex4("DATA"), and
-    // the second 4 bytes: hex4(chunk size)
-    const checkChunkDataHeader = function(data) {
-      if (data.length + currentHeaderLength >= 8) {
-        extractChunkDataHeader(data);
-
-        if (headerArray[0] != DATA) {
-          shutdown();
-          return false;
-        }
-        // remove header info. from socket package data
-        pkgData = data.subarray(8 - currentHeaderLength, data.length);
-        chunkSize = headerArray[1];
-        currentHeaderLength = 0;
-        return true;
-      }
-
-      // If chunk data header info. is separated into more than one
-      // socket package, keep partial header info. in headerArray.
-      const tmpArray = new Uint8Array(headerArray.buffer);
-      for (let i = 0; i < data.length; i++) {
-        tmpArray[currentHeaderLength + i] = data[i];
-      }
-      currentHeaderLength += data.length;
-      return true;
-    };
-
-    // The last remaining package data contains 8 bytes,
-    // they are "DONE(0x454e4f44)" and 0x0000.
-    const checkDone = function(data) {
-      if (data.length != 8) {
-        return false;
-      }
+    return new Promise((resolve, reject) => {
+      const shutdown = function() {
+        dumpn("shell shutdown");
+        socket.close();
+        reject("BAD_RESPONSE");
+      };
 
-      const doneFlagArray = new Uint32Array(1);
-      const tmpArray = new Uint8Array(doneFlagArray.buffer);
-      for (let i = 0; i < 4; i++) {
-        tmpArray[i] = data[i];
-      }
-      // Check DONE flag
-      if (doneFlagArray[0] == DONE) {
-        return true;
-      }
-      return false;
-    };
-
-    const runFSM = function runFSM(data) {
-      console.log("runFSM " + state);
-      let req;
-      switch (state) {
-        case "start":
-          state = "send-transport";
-          runFSM();
-          break;
-        case "send-transport":
-          req = client.createRequest("host:transport-any");
-          socket.send(req);
-          state = "wait-transport";
-          break;
-        case "wait-transport":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          console.log("transport: OK");
-          state = "send-sync";
-          runFSM();
-          break;
-        case "send-sync":
-          req = client.createRequest("sync:");
-          socket.send(req);
-          state = "wait-sync";
-          break;
-        case "wait-sync":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          console.log("sync: OK");
-          state = "send-recv";
-          runFSM();
-          break;
-        case "send-recv":
-          infoLengthPacket = new Uint32Array(1);
-          infoLengthPacket[0] = from.length;
-          socket.send(encoder.encode("RECV"));
-          socket.send(infoLengthPacket);
-          socket.send(encoder.encode(from));
-
-          state = "wait-recv";
-          break;
-        case "wait-recv":
-          // After sending "RECV" command, adb server will send chunks data back,
-          // Handle every single socket package here.
-          // Note: One socket package maybe contain many chunks, and often
-          // partial chunk at the end.
-          pkgData = new Uint8Array(client.getBuffer(data));
-
-          // Handle all data in a single socket package.
-          while (pkgData.length > 0) {
-            if (chunkSize == 0 && checkDone(pkgData)) {
-              OS.File.writeAtomic(dest, fileData, {}).then(
-                function onSuccess(number) {
-                  console.log(number);
-                  deferred.resolve("SUCCESS");
-                },
-                function onFailure(reason) {
-                  console.log(reason);
-                  deferred.reject("CANT_ACCESS_FILE");
-                }
-              );
-
-              state = "send-quit";
-              runFSM();
-              return;
-            }
-            if (chunkSize == 0 && !checkChunkDataHeader(pkgData)) {
+      const runFSM = function runFSM(data) {
+        dumpn("runFSM " + state);
+        let req;
+        let ignoreResponseCode = false;
+        switch (state) {
+          case "start":
+            state = "send-transport";
+            runFSM();
+            break;
+          case "send-transport":
+            req = client.createRequest("host:transport-any");
+            socket.send(req);
+            state = "wait-transport";
+            break;
+          case "wait-transport":
+            if (!client.checkResponse(data, OKAY)) {
               shutdown();
               return;
             }
-            // handle full chunk
-            if (chunkSize > 0 && pkgData.length >= chunkSize) {
-              const chunkData = pkgData.subarray(0, chunkSize);
-              const tmpData = new Uint8Array(currentPos + chunkSize);
-              if (fileData) {
-                tmpData.set(fileData, 0);
-              }
-              tmpData.set(chunkData, currentPos);
-              fileData = tmpData;
-              pkgData = pkgData.subarray(chunkSize, pkgData.length);
-              currentPos += chunkSize;
-              chunkSize = 0;
+            state = "send-shell";
+            runFSM();
+            break;
+          case "send-shell":
+            req = client.createRequest("shell:" + command);
+            socket.send(req);
+            state = "rec-shell";
+            break;
+          case "rec-shell":
+            if (!client.checkResponse(data, OKAY)) {
+              shutdown();
+              return;
             }
-            // handle partial chunk at the end of socket package
-            if (chunkSize > 0 && pkgData.length > 0 && pkgData.length < chunkSize) {
-              const tmpData = new Uint8Array(currentPos + pkgData.length);
-              if (fileData) {
-                tmpData.set(fileData, 0);
-              }
-              tmpData.set(pkgData, currentPos);
-              fileData = tmpData;
-              currentPos += pkgData.length;
-              chunkSize -= pkgData.length;
-              break; // Break while loop.
+            state = "decode-shell";
+            if (client.getBuffer(data).byteLength == 4) {
+              break;
             }
-          }
-
-          break;
-        case "send-quit":
-          infoLengthPacket = new Uint32Array(1);
-          infoLengthPacket[0] = 0;
-          socket.send(encoder.encode("QUIT"));
-          socket.send(infoLengthPacket);
+            ignoreResponseCode = true;
+            // eslint-disable-next-lined no-fallthrough
+          case "decode-shell":
+            const decoder = new TextDecoder();
+            const text = new Uint8Array(client.getBuffer(data),
+                                        ignoreResponseCode ? 4 : 0);
+            stdout += decoder.decode(text);
+            break;
+          default:
+            dumpn("shell Unexpected State: " + state);
+            reject("UNEXPECTED_STATE");
+        }
+      };
 
-          state = "end";
-          runFSM();
-          break;
-        case "end":
-          socket.close();
-          break;
-        default:
-          console.log("pull Unexpected State: " + state);
-          deferred.reject("UNEXPECTED_STATE");
-      }
-    };
-
-    const setupSocket = function() {
+      const socket = client.connect();
       socket.s.onerror = function(event) {
-        console.log("pull onerror");
-        deferred.reject("SOCKET_ERROR");
+        dumpn("shell onerror");
+        reject("SOCKET_ERROR");
       };
 
       socket.s.onopen = function(event) {
-        console.log("pull onopen");
+        dumpn("shell onopen");
         state = "start";
         runFSM();
       };
 
       socket.s.onclose = function(event) {
-        console.log("pull onclose");
-      };
-
-      socket.s.ondata = function(event) {
-        console.log("pull ondata:");
-        runFSM(event.data);
-      };
-    };
-
-    const socket = client.connect();
-    setupSocket();
-
-    return deferred.promise;
-  },
-
-  // pushes a file to the device.
-  // from and dest are full paths.
-  // XXX we should STAT the remote path before sending.
-  push(from, dest) {
-    const deferred = PromiseUtils.defer();
-    let socket;
-    let state;
-    let fileSize;
-    let fileData;
-    let remaining;
-    let currentPos = 0;
-    let fileTime;
-
-    console.log("pushing " + from + " -> " + dest);
-
-    const shutdown = function() {
-      console.log("push shutdown");
-      socket.close();
-      deferred.reject("BAD_RESPONSE");
-    };
-
-    const runFSM = function runFSM(data) {
-      console.log("runFSM " + state);
-      let req;
-      switch (state) {
-        case "start":
-          state = "send-transport";
-          runFSM();
-          break;
-        case "send-transport":
-          req = client.createRequest("host:transport-any");
-          socket.send(req);
-          state = "wait-transport";
-          break;
-        case "wait-transport":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          console.log("transport: OK");
-          state = "send-sync";
-          runFSM();
-          break;
-        case "send-sync":
-          req = client.createRequest("sync:");
-          socket.send(req);
-          state = "wait-sync";
-          break;
-        case "wait-sync":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          console.log("sync: OK");
-          state = "send-send";
-          runFSM();
-          break;
-        case "send-send":
-          // need to send SEND + length($dest,$fileMode)
-          // $fileMode is not the octal one there.
-          const encoder = new TextEncoder();
-
-          const infoLengthPacket = new Uint32Array(1), info = dest + ",33204";
-          infoLengthPacket[0] = info.length;
-          socket.send(encoder.encode("SEND"));
-          socket.send(infoLengthPacket);
-          socket.send(encoder.encode(info));
-
-          // now sending file data.
-          while (remaining > 0) {
-            const toSend = remaining > 65536 ? 65536 : remaining;
-            console.log("Sending " + toSend + " bytes");
-
-            const dataLengthPacket = new Uint32Array(1);
-            // We have to create a new ArrayBuffer for the fileData slice
-            // because nsIDOMTCPSocket (or ArrayBufferInputStream) chokes on
-            // reused buffers, even when we don't modify their contents.
-            const dataPacket = new Uint8Array(new ArrayBuffer(toSend));
-            dataPacket.set(new Uint8Array(fileData.buffer, currentPos, toSend));
-            dataLengthPacket[0] = toSend;
-            socket.send(encoder.encode("DATA"));
-            socket.send(dataLengthPacket);
-            socket.send(dataPacket);
-
-            currentPos += toSend;
-            remaining -= toSend;
-          }
-
-          // Ending up with DONE + mtime (wtf???)
-          const fileTimePacket = new Uint32Array(1);
-          fileTimePacket[0] = fileTime;
-          socket.send(encoder.encode("DONE"));
-          socket.send(fileTimePacket);
-
-          state = "wait-done";
-          break;
-        case "wait-done":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          console.log("DONE: OK");
-          state = "end";
-          runFSM();
-          break;
-        case "end":
-          socket.close();
-          deferred.resolve("SUCCESS");
-          break;
-        default:
-          console.log("push Unexpected State: " + state);
-          deferred.reject("UNEXPECTED_STATE");
-      }
-    };
-
-    const setupSocket = function() {
-      socket.s.onerror = function(event) {
-        console.log("push onerror");
-        deferred.reject("SOCKET_ERROR");
-      };
-
-      socket.s.onopen = function(event) {
-        console.log("push onopen");
-        state = "start";
-        runFSM();
-      };
-
-      socket.s.onclose = function(event) {
-        console.log("push onclose");
+        resolve(stdout);
+        dumpn("shell onclose");
       };
 
       socket.s.ondata = function(event) {
-        console.log("push ondata");
+        dumpn("shell ondata");
         runFSM(event.data);
       };
-    };
-    // Stat the file, get its size.
-    OS.File.stat(from).then(
-      function onSuccess(stat) {
-        if (stat.isDir) {
-          // The path represents a directory
-          deferred.reject("CANT_PUSH_DIR");
-        } else {
-          // The path represents a file, not a directory
-          fileSize = stat.size;
-          // We want seconds since epoch
-          fileTime = stat.lastModificationDate.getTime() / 1000;
-          remaining = fileSize;
-          console.log(from + " size is " + fileSize);
-          const readPromise = OS.File.read(from);
-          readPromise.then(
-            function readSuccess(data) {
-              fileData = data;
-              socket = client.connect();
-              setupSocket();
-            },
-            function readError() {
-              deferred.reject("READ_FAILED");
-            }
-          );
-        }
-      },
-      function onFailure(reason) {
-        console.log(reason);
-        deferred.reject("CANT_ACCESS_FILE");
-      }
-    );
-
-    return deferred.promise;
-  },
-
-  // Run a shell command
-  shell(command) {
-    const deferred = PromiseUtils.defer();
-    let state;
-    let stdout = "";
-
-    console.log("shell " + command);
-
-    const shutdown = function() {
-      console.log("shell shutdown");
-      socket.close();
-      deferred.reject("BAD_RESPONSE");
-    };
-
-    const runFSM = function runFSM(data) {
-      console.log("runFSM " + state);
-      let req;
-      let ignoreResponseCode = false;
-      switch (state) {
-        case "start":
-          state = "send-transport";
-          runFSM();
-          break;
-        case "send-transport":
-          req = client.createRequest("host:transport-any");
-          socket.send(req);
-          state = "wait-transport";
-          break;
-        case "wait-transport":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          state = "send-shell";
-          runFSM();
-          break;
-        case "send-shell":
-          req = client.createRequest("shell:" + command);
-          socket.send(req);
-          state = "rec-shell";
-          break;
-        case "rec-shell":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          state = "decode-shell";
-          if (client.getBuffer(data).byteLength == 4) {
-            break;
-          }
-          ignoreResponseCode = true;
-          // eslint-disable-next-lined no-fallthrough
-        case "decode-shell":
-          const decoder = new TextDecoder();
-          const text = new Uint8Array(client.getBuffer(data),
-                                      ignoreResponseCode ? 4 : 0);
-          stdout += decoder.decode(text);
-          break;
-        default:
-          console.log("shell Unexpected State: " + state);
-          deferred.reject("UNEXPECTED_STATE");
-      }
-    };
-
-    const socket = client.connect();
-    socket.s.onerror = function(event) {
-      console.log("shell onerror");
-      deferred.reject("SOCKET_ERROR");
-    };
-
-    socket.s.onopen = function(event) {
-      console.log("shell onopen");
-      state = "start";
-      runFSM();
-    };
-
-    socket.s.onclose = function(event) {
-      deferred.resolve(stdout);
-      console.log("shell onclose");
-    };
-
-    socket.s.ondata = function(event) {
-      console.log("shell ondata");
-      runFSM(event.data);
-    };
-
-    return deferred.promise;
-  },
-
-  reboot() {
-    return this.shell("reboot");
-  },
-
-  rebootRecovery() {
-    return this.shell("reboot recovery");
-  },
-
-  rebootBootloader() {
-    return this.shell("reboot bootloader");
-  },
-
-  root() {
-    const deferred = PromiseUtils.defer();
-    let state;
-
-    console.log("root");
-
-    const shutdown = function() {
-      console.log("root shutdown");
-      socket.close();
-      deferred.reject("BAD_RESPONSE");
-    };
-
-    const runFSM = function runFSM(data) {
-      console.log("runFSM " + state);
-      let req;
-      switch (state) {
-        case "start":
-          state = "send-transport";
-          runFSM();
-          break;
-        case "send-transport":
-          req = client.createRequest("host:transport-any");
-          socket.send(req);
-          state = "wait-transport";
-          break;
-        case "wait-transport":
-          if (!client.checkResponse(data, OKAY)) {
-            shutdown();
-            return;
-          }
-          state = "send-root";
-          runFSM();
-          break;
-        case "send-root":
-          req = client.createRequest("root:");
-          socket.send(req);
-          state = "rec-root";
-          break;
-        case "rec-root":
-          // Nothing to do
-          break;
-        default:
-          console.log("root Unexpected State: " + state);
-          deferred.reject("UNEXPECTED_STATE");
-      }
-    };
-
-    const socket = client.connect();
-    socket.s.onerror = function(event) {
-      console.log("root onerror");
-      deferred.reject("SOCKET_ERROR");
-    };
-
-    socket.s.onopen = function(event) {
-      console.log("root onopen");
-      state = "start";
-      runFSM();
-    };
-
-    socket.s.onclose = function(event) {
-      deferred.resolve();
-      console.log("root onclose");
-    };
-
-    socket.s.ondata = function(event) {
-      console.log("root ondata");
-      runFSM(event.data);
-    };
-
-    return deferred.promise;
+    });
   },
 
   // Asynchronously runs an adb command.
   // @param command The command as documented in
   // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
   runCommand(command) {
-    console.log("runCommand " + command);
-    const deferred = PromiseUtils.defer();
-    if (!this.ready) {
-      setTimeout(function() {
-        deferred.reject("ADB_NOT_READY");
-      });
-      return deferred.promise;
-    }
-
-    const socket = client.connect();
-
-    socket.s.onopen = function() {
-      console.log("runCommand onopen");
-      const req = client.createRequest(command);
-      socket.send(req);
-    };
-
-    socket.s.onerror = function() {
-      console.log("runCommand onerror");
-      deferred.reject("NETWORK_ERROR");
-    };
-
-    socket.s.onclose = function() {
-      console.log("runCommand onclose");
-    };
-
-    socket.s.ondata = function(event) {
-      console.log("runCommand ondata");
-      const data = event.data;
-
-      const packet = client.unpackPacket(data, false);
-      if (!client.checkResponse(data, OKAY)) {
-        socket.close();
-        console.log("Error: " + packet.data);
-        deferred.reject("PROTOCOL_ERROR");
+    dumpn("runCommand " + command);
+    return new Promise((resolve, reject) => {
+      if (!this.ready) {
+        setTimeout(function() {
+          reject("ADB_NOT_READY");
+        });
         return;
       }
 
-      deferred.resolve(packet.data);
-    };
+      const socket = client.connect();
+
+      socket.s.onopen = function() {
+        dumpn("runCommand onopen");
+        const req = client.createRequest(command);
+        socket.send(req);
+      };
+
+      socket.s.onerror = function() {
+        dumpn("runCommand onerror");
+        reject("NETWORK_ERROR");
+      };
 
-    return deferred.promise;
+      socket.s.onclose = function() {
+        dumpn("runCommand onclose");
+      };
+
+      socket.s.ondata = function(event) {
+        dumpn("runCommand ondata");
+        const data = event.data;
+
+        const packet = client.unpackPacket(data, false);
+        if (!client.checkResponse(data, OKAY)) {
+          socket.close();
+          dumpn("Error: " + packet.data);
+          reject("PROTOCOL_ERROR");
+          return;
+        }
+
+        resolve(packet.data);
+      };
+    });
   }
 };
 
 exports.ADB = ADB;
--- a/devtools/shared/adb/test/test_adb.js
+++ b/devtools/shared/adb/test/test_adb.js
@@ -29,16 +29,17 @@ const ADB_JSON = {
     ],
     "x86_64": [
       "win32/adb.exe",
       "win32/AdbWinApi.dll",
       "win32/AdbWinUsbApi.dll"
     ]
   }
 };
+let extension_version = 1.0;
 
 ExtensionTestUtils.init(this);
 
 function readAdbMockContent() {
   const adbMockFile = do_get_file("adb.py", false);
   const s = Cc["@mozilla.org/network/file-input-stream;1"]
     .createInstance(Ci.nsIFileInputStream);
   s.init(adbMockFile, -1, -1, false);
@@ -60,17 +61,17 @@ add_task(async function testAdbIsNotRunn
   const isAdbRunning = await check();
   // Assume that no adb server running.
   ok(!isAdbRunning, "adb is not running initially");
 });
 
 add_task(async function testNoAdbExtension() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         gecko: { id: "not-adb@mozilla.org" }
       }
     },
   });
 
   await extension.startup();
 
@@ -78,17 +79,17 @@ add_task(async function testNoAdbExtensi
   equal(adbBinary, null);
 
   await extension.unload();
 });
 
 add_task(async function testNoAdbJSON() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         // The extension id here and in later test cases should match the
         // corresponding prefrece value.
         gecko: { id: "adb@mozilla.org" }
       }
     },
   });
 
@@ -100,17 +101,17 @@ add_task(async function testNoAdbJSON() 
   await extension.unload();
 });
 
 add_task({
   skip_if: () => mozinfo.os == "win" // bug 1482008
 }, async function testNoTargetBinaries() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         gecko: { id: "adb@mozilla.org" }
       }
     },
     files: {
       "adb.json": JSON.stringify(ADB_JSON),
     },
   });
@@ -121,17 +122,17 @@ add_task({
   equal(adbBinary, null);
 
   await extension.unload();
 });
 
 add_task(async function testExtract() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         gecko: { id: "adb@mozilla.org" }
       }
     },
     files: {
       "adb.json": JSON.stringify(ADB_JSON),
       "linux/adb": "adb",
       "linux64/adb": "adb",
@@ -150,17 +151,17 @@ add_task(async function testExtract() {
   await extension.unload();
 });
 
 add_task({
   skip_if: () => mozinfo.os == "win" // bug 1482008
 }, async function testStartAndStop() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         gecko: { id: "adb@mozilla.org" }
       }
     },
     files: {
       "adb.json": JSON.stringify(ADB_JSON),
       "linux/adb": adbMock,
       "linux64/adb": adbMock,
@@ -173,28 +174,28 @@ add_task({
 
   await extension.startup();
 
   await ADB.start();
   ok(ADB.ready);
 
   ok(await check(), "adb is now running");
 
-  await ADB.stop(true /* sync */);
+  await ADB.stop();
   ok(!ADB.ready);
 
   await extension.unload();
 });
 
 add_task({
   skip_if: () => mozinfo.os == "win" // bug 1482008
 }, async function testTrackDevices() {
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      version: "1.0",
+      version: (extension_version++).toString(),
       applications: {
         gecko: { id: "adb@mozilla.org" }
       }
     },
     files: {
       "adb.json": JSON.stringify(ADB_JSON),
       "linux/adb": adbMock,
       "linux64/adb": adbMock,
@@ -216,14 +217,14 @@ add_task({
     EventEmitter.on(ADB, "device-connected", deviceId => {
       resolve(deviceId);
     });
     ADB.trackDevices();
   });
 
   equal(receivedDeviceId, "1234567890");
 
-  await ADB.stop(true /* sync */);
+  await ADB.stop();
   ok(!ADB.ready);
 
   await extension.unload();
 });
 
--- a/devtools/shared/preferences/devtools-shared.js
+++ b/devtools/shared/preferences/devtools-shared.js
@@ -54,16 +54,19 @@ pref("devtools.dump.emit", false);
 
 // Disable device discovery logging
 pref("devtools.discovery.log", false);
 // Whether to scan for DevTools devices via WiFi
 pref("devtools.remote.wifi.scan", true);
 // Client must complete TLS handshake within this window (ms)
 pref("devtools.remote.tls-handshake-timeout", 10000);
 
+// The extension ID for devtools-adb-extension
+pref("devtools.remote.adb.extensionID", "adb@mozilla.org");
+
 // URL of the remote JSON catalog used for device simulation
 pref("devtools.devices.url", "https://code.cdn.mozilla.net/devices/devices.json");
 
 // Display the introductory text
 pref("devtools.gcli.hideIntro", false);
 
 // How eager are we to show help: never=1, sometimes=2, always=3
 pref("devtools.gcli.eagerHelper", 2);
--- a/js/src/jit/MacroAssembler.cpp
+++ b/js/src/jit/MacroAssembler.cpp
@@ -1424,36 +1424,38 @@ MacroAssembler::compareStrings(JSOp op, 
                                Label* fail)
 {
     MOZ_ASSERT(left != result);
     MOZ_ASSERT(right != result);
     MOZ_ASSERT(IsEqualityOp(op));
 
     Label done;
     Label notPointerEqual;
-    // Fast path for identical strings.
+    // If operands point to the same instance, the strings are trivially equal.
     branchPtr(Assembler::NotEqual, left, right, &notPointerEqual);
     move32(Imm32(op == JSOP_EQ || op == JSOP_STRICTEQ), result);
     jump(&done);
 
     bind(&notPointerEqual);
 
-    Label notAtom;
-    // Optimize the equality operation to a pointer compare for two atoms.
+    Label leftIsNotAtom;
+    Label setNotEqualResult;
+    // Atoms cannot be equal to each other if they point to different strings.
     Imm32 nonAtomBit(JSString::NON_ATOM_BIT);
-    branchTest32(Assembler::NonZero, Address(left, JSString::offsetOfFlags()), nonAtomBit, &notAtom);
-    branchTest32(Assembler::NonZero, Address(right, JSString::offsetOfFlags()), nonAtomBit, &notAtom);
-
-    cmpPtrSet(JSOpToCondition(MCompare::Compare_String, op), left, right, result);
-    jump(&done);
-
-    bind(&notAtom);
+    branchTest32(Assembler::NonZero, Address(left, JSString::offsetOfFlags()), nonAtomBit,
+                 &leftIsNotAtom);
+    branchTest32(Assembler::Zero, Address(right, JSString::offsetOfFlags()), nonAtomBit,
+                 &setNotEqualResult);
+
+    bind(&leftIsNotAtom);
     // Strings of different length can never be equal.
     loadStringLength(left, result);
     branch32(Assembler::Equal, Address(right, JSString::offsetOfLength()), result, fail);
+
+    bind(&setNotEqualResult);
     move32(Imm32(op == JSOP_NE || op == JSOP_STRICTNE), result);
 
     bind(&done);
 }
 
 void
 MacroAssembler::loadStringChars(Register str, Register dest, CharEncoding encoding)
 {
--- a/memory/mozalloc/mozalloc_abort.cpp
+++ b/memory/mozalloc/mozalloc_abort.cpp
@@ -10,16 +10,17 @@
 #ifdef ANDROID
 # include <android/log.h>
 #endif
 #ifdef MOZ_WIDGET_ANDROID
 # include "APKOpen.h"
 # include "dlfcn.h"
 #endif
 #include <stdio.h>
+#include <string.h>
 
 #include "mozilla/Assertions.h"
 #include "mozilla/Sprintf.h"
 
 void
 mozalloc_abort(const char* const msg)
 {
 #ifndef ANDROID
--- a/mobile/android/config/mozconfigs/android-x86/debug
+++ b/mobile/android/config/mozconfigs/android-x86/debug
@@ -4,17 +4,17 @@
 ac_add_options --enable-debug
 
 # Android
 # Warning: Before increasing the with-android-min-sdk value, please note several places in and out
 # of tree have to be changed. Otherwise, places like Treeherder or archive.mozilla.org will
 # advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
 # If you think you can't handle the whole set of changes, please reach out to the Release
 # Engineering team.
-ac_add_options --target=i386-linux-android
+ac_add_options --target=i686-linux-android
 ac_add_options --with-android-min-sdk=16
 
 export MOZILLA_OFFICIAL=1
 export MOZ_TELEMETRY_REPORTING=1
 export MOZ_ANDROID_POCKET=1
 
 ac_add_options --with-branding=mobile/android/branding/nightly
 
--- a/mobile/android/config/mozconfigs/android-x86/nightly
+++ b/mobile/android/config/mozconfigs/android-x86/nightly
@@ -1,16 +1,16 @@
 . "$topsrcdir/mobile/android/config/mozconfigs/common"
 
 # Warning: Before increasing the with-android-min-sdk value, please note several places in and out
 # of tree have to be changed. Otherwise, places like Treeherder or archive.mozilla.org will
 # advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
 # If you think you can't handle the whole set of changes, please reach out to the Release
 # Engineering team.
-ac_add_options --target=i386-linux-android
+ac_add_options --target=i686-linux-android
 ac_add_options --with-android-min-sdk=16
 
 ac_add_options --with-branding=mobile/android/branding/nightly
 
 export MOZILLA_OFFICIAL=1
 export MOZ_TELEMETRY_REPORTING=1
 export MOZ_ANDROID_POCKET=1
 
--- a/python/mozboot/mozboot/android.py
+++ b/python/mozboot/mozboot/android.py
@@ -9,17 +9,17 @@ import errno
 import os
 import stat
 import subprocess
 import sys
 
 # We need the NDK version in multiple different places, and it's inconvenient
 # to pass down the NDK version to all relevant places, so we have this global
 # variable.
-NDK_VERSION = 'r15c'
+NDK_VERSION = 'r17b'
 
 ANDROID_NDK_EXISTS = '''
 Looks like you have the Android NDK installed at:
 %s
 '''
 
 ANDROID_SDK_EXISTS = '''
 Looks like you have the Android SDK installed at:
--- a/python/mozboot/mozboot/archlinux.py
+++ b/python/mozboot/mozboot/archlinux.py
@@ -6,20 +6,20 @@ from __future__ import absolute_import, 
 
 import os
 import sys
 import tempfile
 import subprocess
 import glob
 
 from mozboot.base import BaseBootstrapper
-from mozboot.linux_common import StyloInstall
+from mozboot.linux_common import NodeInstall, StyloInstall
 
 
-class ArchlinuxBootstrapper(StyloInstall, BaseBootstrapper):
+class ArchlinuxBootstrapper(NodeInstall, StyloInstall, BaseBootstrapper):
     '''Archlinux experimental bootstrapper.'''
 
     SYSTEM_PACKAGES = [
         'autoconf2.13',
         'base-devel',
         'nodejs',
         'python2',
         'python2-setuptools',
--- a/python/mozboot/mozboot/base.py
+++ b/python/mozboot/mozboot/base.py
@@ -152,19 +152,20 @@ MODERN_PYTHON_VERSION = LooseVersion('2.
 
 # Upgrade rust older than this.
 MODERN_RUST_VERSION = LooseVersion('1.28.0')
 
 
 class BaseBootstrapper(object):
     """Base class for system bootstrappers."""
 
-    def __init__(self, no_interactive=False):
+    def __init__(self, no_interactive=False, no_system_changes=False):
         self.package_manager_updated = False
         self.no_interactive = no_interactive
+        self.no_system_changes = no_system_changes
         self.state_dir = None
 
     def install_system_packages(self):
         '''
         Install packages shared by all applications. These are usually
         packages required by the development (like mercurial) or the
         build system (like autoconf).
         '''
@@ -255,16 +256,23 @@ class BaseBootstrapper(object):
     def ensure_stylo_packages(self, state_dir, checkout_root):
         '''
         Install any necessary packages needed for Stylo development.
         '''
         raise NotImplementedError(
             '%s does not yet implement ensure_stylo_packages()'
             % __name__)
 
+    def ensure_node_packages(self, state_dir, checkout_root):
+        '''
+        Install any necessary packages needed to supply NodeJS'''
+        raise NotImplementedError(
+            '%s does not yet implement ensure_node_packages()'
+            % __name__)
+
     def install_toolchain_artifact(self, state_dir, checkout_root, toolchain_job):
         mach_binary = os.path.join(checkout_root, 'mach')
         mach_binary = os.path.abspath(mach_binary)
         if not os.path.exists(mach_binary):
             raise ValueError("mach not found at %s" % mach_binary)
 
         # If Python can't figure out what its own executable is, there's little
         # chance we're going to be able to execute mach on its own, particularly
--- a/python/mozboot/mozboot/bootstrap.py
+++ b/python/mozboot/mozboot/bootstrap.py
@@ -70,29 +70,29 @@ use and re-run the bootstrapper.
 
 Would you like to create this directory?
 
   1. Yes
   2. No
 
 Your choice: '''
 
-STYLO_DIRECTORY_MESSAGE = '''
-Stylo packages require a directory to store shared, persistent state.
-On this machine, that directory is:
+STYLO_NODEJS_DIRECTORY_MESSAGE = '''
+Stylo and NodeJS packages require a directory to store shared, persistent
+state.  On this machine, that directory is:
 
   {statedir}
 
 Please restart bootstrap and create that directory when prompted.
 '''
 
-STYLO_REQUIRES_CLONE = '''
-Installing Stylo packages requires a checkout of mozilla-central. Once you
-have such a checkout, please re-run `./mach bootstrap` from the checkout
-directory.
+STYLE_NODEJS_REQUIRES_CLONE = '''
+Installing Stylo and NodeJS packages requires a checkout of mozilla-central.
+Once you have such a checkout, please re-run `./mach bootstrap` from the
+checkout directory.
 '''
 
 FINISHED = '''
 Your system should be ready to build %s!
 '''
 
 SOURCE_ADVERTISE = '''
 Source code can be obtained by running
@@ -166,23 +166,25 @@ DEBIAN_DISTROS = (
     '"elementary"'
 )
 
 
 class Bootstrapper(object):
     """Main class that performs system bootstrap."""
 
     def __init__(self, finished=FINISHED, choice=None, no_interactive=False,
-                 hg_configure=False):
+                 hg_configure=False, no_system_changes=False):
         self.instance = None
         self.finished = finished
         self.choice = choice
         self.hg_configure = hg_configure
+        self.no_system_changes = no_system_changes
         cls = None
-        args = {'no_interactive': no_interactive}
+        args = {'no_interactive': no_interactive,
+                'no_system_changes': no_system_changes}
 
         if sys.platform.startswith('linux'):
             distro, version, dist_id = platform.linux_distribution()
 
             if distro in ('CentOS', 'CentOS Linux', 'Fedora'):
                 cls = CentOSFedoraBootstrapper
                 args['distro'] = distro
             elif distro in DEBIAN_DISTROS:
@@ -257,57 +259,103 @@ class Bootstrapper(object):
             choice = self.instance.prompt_int(prompt=CLONE_MERCURIAL_NOT_EMPTY.format(dest,
                                               newdest), low=1, high=3)
             if choice == 1:
                 return newdest
             if choice == 2:
                 continue
             return ''
 
-    def bootstrap(self):
-        if self.choice is None:
-            # Like ['1. Firefox for Desktop', '2. Firefox for Android Artifact Mode', ...].
-            labels = ['%s. %s' % (i + 1, name) for (i, (name, _)) in enumerate(APPLICATIONS_LIST)]
-            prompt = APPLICATION_CHOICE % '\n'.join(labels)
-            prompt_choice = self.instance.prompt_int(prompt=prompt, low=1, high=len(APPLICATIONS))
-            name, application = APPLICATIONS_LIST[prompt_choice-1]
-        elif self.choice not in APPLICATIONS.keys():
-            raise Exception('Please pick a valid application choice: (%s)' %
-                            '/'.join(APPLICATIONS.keys()))
-        else:
-            name, application = APPLICATIONS[self.choice]
-
-        self.instance.install_system_packages()
-
-        # Like 'install_browser_packages' or 'install_mobile_android_packages'.
-        getattr(self.instance, 'install_%s_packages' % application)()
-
-        hg_installed, hg_modern = self.instance.ensure_mercurial_modern()
-        self.instance.ensure_python_modern()
-        self.instance.ensure_rust_modern()
-
-        # The state directory code is largely duplicated from mach_bootstrap.py.
-        # We can't easily import mach_bootstrap.py because the bootstrapper may
-        # run in self-contained mode and only the files in this directory will
-        # be available. We /could/ refactor parts of mach_bootstrap.py to be
-        # part of this directory to avoid the code duplication.
+    # The state directory code is largely duplicated from mach_bootstrap.py.
+    # We can't easily import mach_bootstrap.py because the bootstrapper may
+    # run in self-contained mode and only the files in this directory will
+    # be available. We /could/ refactor parts of mach_bootstrap.py to be
+    # part of this directory to avoid the code duplication.
+    def try_to_create_state_dir(self):
         state_dir, _ = get_state_dir()
 
         if not os.path.exists(state_dir):
             if not self.instance.no_interactive:
                 choice = self.instance.prompt_int(
                     prompt=STATE_DIR_INFO.format(statedir=state_dir),
                     low=1,
                     high=2)
 
                 if choice == 1:
                     print('Creating global state directory: %s' % state_dir)
                     os.makedirs(state_dir, mode=0o770)
 
         state_dir_available = os.path.exists(state_dir)
+        return state_dir_available, state_dir
+
+    def maybe_install_private_packages_or_exit(self, state_dir,
+                                               state_dir_available,
+                                               have_clone,
+                                               checkout_root):
+        # Install the clang packages needed for developing stylo, as well
+        # as the version of NodeJS that we currently support.
+        if not self.instance.no_interactive:
+            # The best place to install our packages is in the state directory
+            # we have.  If the user doesn't have one, we need them to re-run
+            # bootstrap and create the directory.
+            #
+            # XXX Android bootstrap just assumes the existence of the state
+            # directory and writes the NDK into it.  Should we do the same?
+            if not state_dir_available:
+                print(STYLO_NODEJS_DIRECTORY_MESSAGE.format(statedir=state_dir))
+                sys.exit(1)
+
+            if not have_clone:
+                print(STYLE_NODEJS_REQUIRES_CLONE)
+                sys.exit(1)
+
+            self.instance.state_dir = state_dir
+            self.instance.ensure_stylo_packages(state_dir, checkout_root)
+            self.instance.ensure_node_packages(state_dir, checkout_root)
+
+    def bootstrap(self):
+        if self.choice is None:
+            # Like ['1. Firefox for Desktop', '2. Firefox for Android Artifact Mode', ...].
+            labels = ['%s. %s' % (i + 1, name) for (i, (name, _)) in enumerate(APPLICATIONS_LIST)]
+            prompt = APPLICATION_CHOICE % '\n'.join(labels)
+            prompt_choice = self.instance.prompt_int(prompt=prompt, low=1, high=len(APPLICATIONS))
+            name, application = APPLICATIONS_LIST[prompt_choice-1]
+        elif self.choice not in APPLICATIONS.keys():
+            raise Exception('Please pick a valid application choice: (%s)' %
+                            '/'.join(APPLICATIONS.keys()))
+        else:
+            name, application = APPLICATIONS[self.choice]
+
+        if self.instance.no_system_changes:
+            state_dir_available, state_dir = self.try_to_create_state_dir()
+            # We need to enable the loading of hgrc in case extensions are
+            # required to open the repo.
+            r = current_firefox_checkout(
+                check_output=self.instance.check_output,
+                env=self.instance._hg_cleanenv(load_hgrc=True),
+                hg=self.instance.which('hg'))
+            (checkout_type, checkout_root) = r
+            have_clone = bool(checkout_type)
+
+            self.maybe_install_private_packages_or_exit(state_dir,
+                                                        state_dir_available,
+                                                        have_clone,
+                                                        checkout_root)
+            sys.exit(0)
+
+        self.instance.install_system_packages()
+
+        # Like 'install_browser_packages' or 'install_mobile_android_packages'.
+        getattr(self.instance, 'install_%s_packages' % application)()
+
+        hg_installed, hg_modern = self.instance.ensure_mercurial_modern()
+        self.instance.ensure_python_modern()
+        self.instance.ensure_rust_modern()
+
+        state_dir_available, state_dir = self.try_to_create_state_dir()
 
         # We need to enable the loading of hgrc in case extensions are
         # required to open the repo.
         r = current_firefox_checkout(check_output=self.instance.check_output,
                                      env=self.instance._hg_cleanenv(load_hgrc=True),
                                      hg=self.instance.which('hg'))
         (checkout_type, checkout_root) = r
 
@@ -335,34 +383,20 @@ class Bootstrapper(object):
             dest = self.input_clone_dest()
             if dest:
                 have_clone = clone_firefox(self.instance.which('hg'), dest)
                 checkout_root = dest
 
         if not have_clone:
             print(SOURCE_ADVERTISE)
 
-        # Install the clang packages needed for developing stylo.
-        if not self.instance.no_interactive:
-            # The best place to install our packages is in the state directory
-            # we have.  If the user doesn't have one, we need them to re-run
-            # bootstrap and create the directory.
-            #
-            # XXX Android bootstrap just assumes the existence of the state
-            # directory and writes the NDK into it.  Should we do the same?
-            if not state_dir_available:
-                print(STYLO_DIRECTORY_MESSAGE.format(statedir=state_dir))
-                sys.exit(1)
-
-            if not have_clone:
-                print(STYLO_REQUIRES_CLONE)
-                sys.exit(1)
-
-            self.instance.state_dir = state_dir
-            self.instance.ensure_stylo_packages(state_dir, checkout_root)
+        self.maybe_install_private_packages_or_exit(state_dir,
+                                                    state_dir_available,
+                                                    have_clone,
+                                                    checkout_root)
 
         print(self.finished % name)
         if not (self.instance.which('rustc') and self.instance._parse_version('rustc')
                 >= MODERN_RUST_VERSION):
             print("To build %s, please restart the shell (Start a new terminal window)" % name)
 
         # Like 'suggest_browser_mozconfig' or 'suggest_mobile_android_mozconfig'.
         getattr(self.instance, 'suggest_%s_mozconfig' % application)()
--- a/python/mozboot/mozboot/centosfedora.py
+++ b/python/mozboot/mozboot/centosfedora.py
@@ -2,20 +2,20 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import platform
 
 from mozboot.base import BaseBootstrapper
-from mozboot.linux_common import StyloInstall
+from mozboot.linux_common import NodeInstall, StyloInstall
 
 
-class CentOSFedoraBootstrapper(StyloInstall, BaseBootstrapper):
+class CentOSFedoraBootstrapper(NodeInstall, StyloInstall, BaseBootstrapper):
     def __init__(self, distro, version, dist_id, **kwargs):
         BaseBootstrapper.__init__(self, **kwargs)
 
         self.distro = distro
         self.version = version
         self.dist_id = dist_id
 
         self.group_packages = []
--- a/python/mozboot/mozboot/debian.py
+++ b/python/mozboot/mozboot/debian.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function
 
 from mozboot.base import BaseBootstrapper
-from mozboot.linux_common import StyloInstall
+from mozboot.linux_common import NodeInstall, StyloInstall
 
 
 MERCURIAL_INSTALL_PROMPT = '''
 Mercurial releases a new version every 3 months and your distro's package
 may become out of date. This may cause incompatibility with some
 Mercurial extensions that rely on new Mercurial features. As a result,
 you may not have an optimal version control experience.
 
@@ -23,17 +23,17 @@ How would you like to continue?
 1) Install a modern Mercurial via pip (recommended)
 2) Install a legacy Mercurial via apt
 3) Do not install Mercurial
 
 Choice:
 '''.strip()
 
 
-class DebianBootstrapper(StyloInstall, BaseBootstrapper):
+class DebianBootstrapper(NodeInstall, StyloInstall, BaseBootstrapper):
     # These are common packages for all Debian-derived distros (such as
     # Ubuntu).
     COMMON_PACKAGES = [
         'autoconf2.13',
         'build-essential',
         'nodejs',
         'python-dev',
         'python-pip',
--- a/python/mozboot/mozboot/gentoo.py
+++ b/python/mozboot/mozboot/gentoo.py
@@ -1,19 +1,19 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 from mozboot.base import BaseBootstrapper
-from mozboot.linux_common import StyloInstall
+from mozboot.linux_common import NodeInstall, StyloInstall
 
 
-class GentooBootstrapper(StyloInstall, BaseBootstrapper):
+class GentooBootstrapper(NodeInstall, StyloInstall, BaseBootstrapper):
     def __init__(self, version, dist_id, **kwargs):
         BaseBootstrapper.__init__(self, **kwargs)
 
         self.version = version
         self.dist_id = dist_id
 
     def install_system_packages(self):
         self.run_as_root(['emerge', '--noreplace', '--quiet', 'nodejs'])
--- a/python/mozboot/mozboot/linux_common.py
+++ b/python/mozboot/mozboot/linux_common.py
@@ -1,18 +1,27 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # An easy way for distribution-specific bootstrappers to share the code
-# needed to install Stylo dependencies.  This class must come before
+# needed to install Stylo and Node dependencies.  This class must come before
 # BaseBootstrapper in the inheritance list.
 
 from __future__ import absolute_import
 
 
 class StyloInstall(object):
     def __init__(self, **kwargs):
         pass
 
     def ensure_stylo_packages(self, state_dir, checkout_root):
         from mozboot import stylo
         self.install_toolchain_artifact(state_dir, checkout_root, stylo.LINUX)
+
+
+class NodeInstall(object):
+    def __init__(self, **kwargs):
+        pass
+
+    def ensure_node_packages(self, state_dir, checkout_root):
+        from mozboot import node
+        self.install_toolchain_artifact(state_dir, checkout_root, node.LINUX)
--- a/python/mozboot/mozboot/mach_commands.py
+++ b/python/mozboot/mozboot/mach_commands.py
@@ -20,20 +20,26 @@ class Bootstrap(object):
     @Command('bootstrap', category='devenv',
              description='Install required system packages for building.')
     @CommandArgument('--application-choice',
                      default=None,
                      help='Pass in an application choice (see mozboot.bootstrap.APPLICATIONS) '
                      'instead of using the default interactive prompt.')
     @CommandArgument('--no-interactive', dest='no_interactive', action='store_true',
                      help='Answer yes to any (Y/n) interactive prompts.')
-    def bootstrap(self, application_choice=None, no_interactive=False):
+    @CommandArgument('--no-system-changes', dest='no_system_changes',
+                     action='store_true',
+                     help='Only execute actions that leave the system '
+                          'configuration alone.')
+    def bootstrap(self, application_choice=None, no_interactive=False, no_system_changes=False):
         from mozboot.bootstrap import Bootstrapper
 
-        bootstrapper = Bootstrapper(choice=application_choice, no_interactive=no_interactive)
+        bootstrapper = Bootstrapper(choice=application_choice,
+                                    no_interactive=no_interactive,
+                                    no_system_changes=no_system_changes)
         bootstrapper.bootstrap()
 
 
 @CommandProvider
 class VersionControlCommands(object):
     def __init__(self, context):
         self._context = context
 
--- a/python/mozboot/mozboot/mozillabuild.py
+++ b/python/mozboot/mozboot/mozillabuild.py
@@ -8,18 +8,19 @@ import os
 import sys
 import subprocess
 
 from mozboot.base import BaseBootstrapper
 
 
 class MozillaBuildBootstrapper(BaseBootstrapper):
     '''Bootstrapper for MozillaBuild to install rustup.'''
-    def __init__(self, no_interactive=False):
-        BaseBootstrapper.__init__(self, no_interactive=no_interactive)
+    def __init__(self, no_interactive=False, no_system_changes=False):
+        BaseBootstrapper.__init__(self, no_interactive=no_interactive,
+                                  no_system_changes=no_system_changes)
         print("mach bootstrap is not fully implemented in MozillaBuild")
 
     def which(self, name):
         return BaseBootstrapper.which(self, name + '.exe')
 
     def install_system_packages(self):
         pass
 
@@ -43,16 +44,21 @@ class MozillaBuildBootstrapper(BaseBoots
 
     def install_mobile_android_artifact_mode_packages(self):
         pass
 
     def ensure_stylo_packages(self, state_dir, checkout_root):
         from mozboot import stylo
         self.install_toolchain_artifact(state_dir, checkout_root, stylo.WINDOWS)
 
+    def ensure_node_packages(self, state_dir, checkout_root):
+        from mozboot import node
+        self.install_toolchain_artifact(
+            state_dir, checkout_root, node.WINDOWS)
+
     def _update_package_manager(self):
         pass
 
     def run(self, command):
         subprocess.check_call(command, stdin=sys.stdin)
 
     def pip_install(self, *packages):
         pip_dir = os.path.join(os.environ['MOZILLABUILD'], 'python', 'Scripts', 'pip.exe')
new file mode 100644
--- /dev/null
+++ b/python/mozboot/mozboot/node.py
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+WINDOWS = 'win64-node'
+LINUX = 'linux64-node'
+OSX = 'macosx64-node'
--- a/python/mozboot/mozboot/osx.py
+++ b/python/mozboot/mozboot/osx.py
@@ -501,16 +501,21 @@ class OSXBootstrapper(BaseBootstrapper):
                     sys.exit(1)
 
         return active_name.lower()
 
     def ensure_stylo_packages(self, state_dir, checkout_root):
         # We installed these via homebrew earlier.
         pass
 
+    def ensure_node_packages(self, state_dir, checkout_root):
+        # XXX from necessary?
+        from mozboot import node
+        self.install_toolchain_artifact(state_dir, checkout_root, node.OSX)
+
     def install_homebrew(self):
         print(PACKAGE_MANAGER_INSTALL % ('Homebrew', 'Homebrew', 'Homebrew', 'brew'))
         bootstrap = urlopen(url=HOMEBREW_BOOTSTRAP, timeout=20).read()
         with tempfile.NamedTemporaryFile() as tf:
             tf.write(bootstrap)
             tf.flush()
 
             subprocess.check_call(['ruby', tf.name])
--- a/python/mozboot/mozboot/windows.py
+++ b/python/mozboot/mozboot/windows.py
@@ -70,16 +70,21 @@ class WindowsBootstrapper(BaseBootstrapp
 
     def install_mobile_android_artifact_mode_packages(self):
         raise NotImplementedError('We do not support building Android on Windows. Sorry!')
 
     def ensure_stylo_packages(self, state_dir, checkout_root):
         from mozboot import stylo
         self.install_toolchain_artifact(state_dir, checkout_root, stylo.WINDOWS)
 
+    def ensure_node_packages(self, state_dir, checkout_root):
+        from mozboot import node
+        self.install_toolchain_artifact(
+            state_dir, checkout_root, node.WINDOWS)
+
     def _update_package_manager(self):
         self.pacman_update()
 
     def run(self, command):
         subprocess.check_call(command, stdin=sys.stdin)
 
     def pacman_update(self):
         command = ['pacman', '--sync', '--refresh']