Bug 1512436 Add new built-in addon location r=kmag
☠☠ backed out by b86c22898543 ☠ ☠
authorAndrew Swan <aswan@mozilla.com>
Thu, 17 Jan 2019 17:34:47 +0000
changeset 454504 8952c559acfb8e9ef41dd97b8304b15ede5a0c99
parent 454503 cbac4cd15f5f66e4e9538e63e83709ce972aff00
child 454505 611bd5949e8303b918da760d8e01caae8d30ec80
push id35399
push usercsabou@mozilla.com
push dateSat, 19 Jan 2019 09:28:26 +0000
treeherdermozilla-central@64d167665c29 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1512436
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1512436 Add new built-in addon location r=kmag The big change here is breaking the previous invariant that every active instance of XPIState contains a file and path. With the change in place, the implementation of the new "location" for built-in addons is pretty straightforward. Differential Revision: https://phabricator.services.mozilla.com/D15639
toolkit/components/extensions/Extension.jsm
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1429,17 +1429,17 @@ class Extension extends ExtensionData {
 
   // Some helpful properties added elsewhere:
   /**
    * An object used to map between extension-visible tab ids and
    * native Tab object
    * @property {TabManager} tabManager
    */
 
-  static getBootstrapScope(id, file) {
+  static getBootstrapScope() {
     return new BootstrapScope();
   }
 
   get groupFrameLoader() {
     let frameLoader = this._backgroundPageFrameLoader;
     for (let view of this.views) {
       if (view.viewType === "background" && view.xulBrowser) {
         return view.xulBrowser.frameLoader;
@@ -2045,17 +2045,17 @@ class Extension extends ExtensionData {
 
 class Dictionary extends ExtensionData {
   constructor(addonData, startupReason) {
     super(addonData.resourceURI);
     this.id = addonData.id;
     this.startupData = addonData.startupData;
   }
 
-  static getBootstrapScope(id, file) {
+  static getBootstrapScope() {
     return new DictionaryBootstrapScope();
   }
 
   async startup(reason) {
     this.dictionaries = {};
     for (let [lang, path] of Object.entries(this.startupData.dictionaries)) {
       let uri = Services.io.newURI(path.slice(0, -4) + ".aff", null, this.rootURI);
       this.dictionaries[lang] = uri;
@@ -2075,17 +2075,17 @@ class Dictionary extends ExtensionData {
 
 class Langpack extends ExtensionData {
   constructor(addonData, startupReason) {
     super(addonData.resourceURI);
     this.startupData = addonData.startupData;
     this.manifestCacheKey = [addonData.id, addonData.version];
   }
 
-  static getBootstrapScope(id, file) {
+  static getBootstrapScope() {
     return new LangpackBootstrapScope();
   }
 
   async promiseLocales(locale) {
     let locales = await StartupCache.locales
       .get([this.id, "@@all_locales"], () => this._promiseLocaleMap());
 
     return this._setupLocaleData(locales);
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2054,16 +2054,33 @@ var AddonManagerInternal = {
     if (!(aFile instanceof Ci.nsIFile))
       throw Components.Exception("aFile must be a nsIFile",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     return AddonManagerInternal._getProviderByName("XPIProvider")
                                .installTemporaryAddon(aFile);
   },
 
+  /**
+   * Installs an add-on from a built-in location
+   *  (ie a resource: url referencing assets shipped with the application)
+   *
+   * @param  aBase
+   *         A string containing the base URL.  Must be a resource: URL.
+   * @returns a Promise that resolves when the addon is installed.
+   */
+  installBuiltinAddon(aBase) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
+    return AddonManagerInternal._getProviderByName("XPIProvider")
+                               .installBuiltinAddon(aBase);
+  },
+
    syncGetAddonIDByInstanceID(aInstanceID) {
      if (!gStarted)
        throw Components.Exception("AddonManager is not initialized",
                                   Cr.NS_ERROR_NOT_INITIALIZED);
 
      if (!aInstanceID || typeof aInstanceID != "symbol")
        throw Components.Exception("aInstanceID must be a Symbol()",
                                   Cr.NS_ERROR_INVALID_ARG);
@@ -3330,16 +3347,20 @@ var AddonManager = {
   installAddonFromAOM(aBrowser, aUri, aInstall) {
     AddonManagerInternal.installAddonFromAOM(aBrowser, aUri, aInstall);
   },
 
   installTemporaryAddon(aDirectory) {
     return AddonManagerInternal.installTemporaryAddon(aDirectory);
   },
 
+  installBuiltinAddon(aBase) {
+    return AddonManagerInternal.installBuiltinAddon(aBase);
+  },
+
   addManagerListener(aListener) {
     AddonManagerInternal.addManagerListener(aListener);
   },
 
   removeManagerListener(aListener) {
     AddonManagerInternal.removeManagerListener(aListener);
   },
 
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -240,20 +240,24 @@ class AddonsList {
         let file;
         if (dir) {
           file = dir.clone();
           try {
             file.appendRelativePath(addon.path);
           } catch (e) {
             file = new nsFile(addon.path);
           }
-        } else {
+        } else if (addon.path) {
           file = new nsFile(addon.path);
         }
 
+        if (!file) {
+          continue;
+        }
+
         this.xpis.push(file);
 
         if (addon.enabled) {
           addon.type = addon.type || "extension";
 
           if (addon.type == "theme") {
             this.themes.push(file);
           } else {
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -77,16 +77,17 @@ const PREF_EM_AUTO_DISABLED_SCOPES    = 
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_XPI_PERMISSIONS_BRANCH     = "xpinstall.";
 const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
+const KEY_APP_BUILTINS                = "app-builtin";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_TEMPORARY               = "app-temporary";
 
 const DEFAULT_THEME_ID = "default-theme@mozilla.org";
 
@@ -106,17 +107,18 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "updateDate", "applyBackgroundUpdates", "path",
                           "skinnable", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "signedState",
                           "seen", "dependencies",
                           "userPermissions", "icons", "iconURL",
                           "blocklistState", "blocklistURL", "startupData",
-                          "previewImage", "hidden", "installTelemetryInfo"];
+                          "previewImage", "hidden", "installTelemetryInfo",
+                          "rootURI"];
 
 const LEGACY_TYPES = new Set([
   "extension",
 ]);
 
 const SIGNED_TYPES = new Set([
   "extension",
   "locale",
@@ -230,16 +232,17 @@ class AddonInternal {
     this.sourceURI = null;
     this.releaseNotesURI = null;
     this.foreignInstall = false;
     this.seen = true;
     this.skinnable = false;
     this.startupData = null;
     this._hidden = false;
     this.installTelemetryInfo = null;
+    this.rootURI = null;
 
     this.inDatabase = false;
 
     /**
      * @property {Array<string>} dependencies
      *   An array of bootstrapped add-on IDs on which this add-on depends.
      *   The add-on will remain appDisabled if any of the dependent
      *   add-ons is not installed and enabled.
@@ -253,20 +256,16 @@ class AddonInternal {
       if (!this.dependencies)
         this.dependencies = [];
       Object.freeze(this.dependencies);
 
       if (this.location) {
         this.addedToDatabase();
       }
 
-      if (!addonData._sourceBundle) {
-        throw new Error("Expected passed argument to contain a path");
-      }
-
       this._sourceBundle = addonData._sourceBundle;
     }
   }
 
   get wrapper() {
     if (!this._wrapper) {
       this._wrapper = new AddonWrapper(this);
     }
@@ -337,18 +336,19 @@ class AddonInternal {
 
   get isCorrectlySigned() {
     switch (this.location.name) {
       case KEY_APP_SYSTEM_ADDONS:
         // System add-ons must be signed by the system key.
         return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM;
 
       case KEY_APP_SYSTEM_DEFAULTS:
+      case KEY_APP_BUILTINS:
       case KEY_APP_TEMPORARY:
-        // Temporary and built-in system add-ons do not require signing.
+        // Temporary and built-in add-ons do not require signing.
         return true;
 
       case KEY_APP_SYSTEM_SHARE:
       case KEY_APP_SYSTEM_LOCAL:
         // On UNIX platforms except OSX, an additional location for system
         // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons
         // installed there do not require signing.
         if (Services.appinfo.OS != "Darwin")
@@ -1331,22 +1331,24 @@ this.XPIDatabase = {
       }
 
       let forEach = this.syncLoadingDB ? arrayForEach : idleForEach;
 
       // If we got here, we probably have good data
       // Make AddonInternal instances from the loaded data and save them
       let addonDB = new Map();
       await forEach(inputAddons.addons, loadedAddon => {
-        try {
-          loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
-        } catch (e) {
-          // We can fail here when the path is invalid, usually from the
-          // wrong OS
-          logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e);
+        if (loadedAddon.path) {
+          try {
+            loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
+          } catch (e) {
+            // We can fail here when the path is invalid, usually from the
+            // wrong OS
+            logger.warn("Could not find source bundle for add-on " + loadedAddon.id, e);
+          }
         }
         loadedAddon.location = XPIStates.getLocation(loadedAddon.location);
 
         let newAddon = new AddonInternal(loadedAddon);
         if (loadedAddon.location) {
           addonDB.set(newAddon._key, newAddon);
         } else {
           this.orphanedAddons.push(newAddon);
@@ -2327,16 +2329,17 @@ this.XPIDatabaseReconcile = {
 
     // Load the manifest if necessary and sanity check the add-on ID
     let unsigned;
     try {
       if (!aNewAddon) {
         // Load the manifest from the add-on.
         let file = new nsIFile(aAddonState.path);
         aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation);
+        aNewAddon.rootURI = XPIInternal.getURIForResourceInFile(file, "").spec;
       }
       // The add-on in the manifest should match the add-on ID.
       if (aNewAddon.id != aId) {
         throw new Error(`Invalid addon ID: expected addon ID ${aId}, found ${aNewAddon.id} in manifest`);
       }
 
       unsigned = XPIDatabase.mustSign(aNewAddon.type) && !aNewAddon.isCorrectlySigned;
       if (unsigned) {
@@ -2422,16 +2425,19 @@ this.XPIDatabaseReconcile = {
   updateMetadata(aLocation, aOldAddon, aAddonState, aNewAddon) {
     logger.debug(`Add-on ${aOldAddon.id} modified in ${aLocation.name}`);
 
     try {
       // If there isn't an updated install manifest for this add-on then load it.
       if (!aNewAddon) {
         let file = new nsIFile(aAddonState.path);
         aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aLocation, aOldAddon);
+        aNewAddon.rootURI = XPIInternal.getURIForResourceInFile(file, "").spec;
+      } else if (!aNewAddon.rootURI) {
+        aNewAddon.rootURI = aOldAddon.rootURI;
       }
 
       // The ID in the manifest that was loaded must match the ID of the old
       // add-on.
       if (aNewAddon.id != aOldAddon.id)
         throw new Error(`Incorrect id in install manifest for existing add-on ${aOldAddon.id}`);
     } catch (e) {
       logger.warn(`updateMetadata: Add-on ${aOldAddon.id} is invalid`, e);
@@ -2498,16 +2504,17 @@ this.XPIDatabaseReconcile = {
     let checkSigning = (aOldAddon.signedState === undefined &&
                         SIGNED_TYPES.has(aOldAddon.type));
 
     let manifest = null;
     if (checkSigning || aReloadMetadata) {
       try {
         let file = new nsIFile(aAddonState.path);
         manifest = XPIInstall.syncLoadManifestFromFile(file, aLocation);
+        manifest.rootURI = aOldAddon.rootURI;
       } catch (err) {
         // If we can no longer read the manifest, it is no longer compatible.
         aOldAddon.brokenManifest = true;
         aOldAddon.appDisabled = true;
         return aOldAddon;
       }
     }
 
@@ -2543,17 +2550,18 @@ this.XPIDatabaseReconcile = {
    *
    * @param {XPIStateLocation} location
    *        The install location to check.
    * @returns {boolean}
    *        True if this location is part of the application bundle.
    */
   isAppBundledLocation(location) {
     return (location.name == KEY_APP_GLOBAL ||
-            location.name == KEY_APP_SYSTEM_DEFAULTS);
+            location.name == KEY_APP_SYSTEM_DEFAULTS ||
+            location.name == KEY_APP_BUILTINS);
   },
 
   /**
    * Returns true if this install location holds system addons.
    *
    * @param {XPIStateLocation} location
    *        The install location to check.
    * @returns {boolean}
@@ -2603,16 +2611,19 @@ this.XPIDatabaseReconcile = {
     } else if (oldAddon.path != xpiState.path) {
       newAddon = this.updatePath(installLocation, oldAddon, xpiState);
     } else if (aUpdateCompatibility || aSchemaChange) {
       newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
                                           aSchemaChange);
     } else {
       newAddon = oldAddon;
     }
+
+    newAddon.rootURI = newAddon.rootURI || xpiState.rootURI;
+
     return newAddon;
   },
 
   /**
    * Compares the add-ons that are currently installed to those that were
    * known to be installed when the application last ran and applies any
    * changes found to the database. Also sends "startupcache-invalidate" signal to
    * observerservice if it detects that data may have changed.
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -79,26 +79,27 @@ const PREF_PENDING_OPERATIONS         = 
 const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
 const PREF_XPI_ENABLED                = "xpinstall.enabled";
 const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
 const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
 const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
-/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, iterDirectory */
+/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, XPI_PERMISSION, XPIStates, getURIForResourceInFile, iterDirectory */
 const XPI_INTERNAL_SYMBOLS = [
   "BOOTSTRAP_REASONS",
   "KEY_APP_SYSTEM_ADDONS",
   "KEY_APP_SYSTEM_DEFAULTS",
   "PREF_BRANCH_INSTALLED_ADDON",
   "PREF_SYSTEM_ADDON_SET",
   "TEMPORARY_ADDON_SUFFIX",
   "XPI_PERMISSION",
   "XPIStates",
+  "getURIForResourceInFile",
   "iterDirectory",
 ];
 
 for (let name of XPI_INTERNAL_SYMBOLS) {
   XPCOMUtils.defineLazyGetter(this, name, () => XPIInternal[name]);
 }
 
 /**
@@ -204,26 +205,16 @@ class Package {
   }
 
   close() {}
 
   getURI(...path) {
     return Services.io.newURI(path.join("/"), null, this.rootURI);
   }
 
-  async getManifestFile() {
-    if (await this.hasResource("manifest.json")) {
-      return "manifest.json";
-    }
-    if (await this.hasResource("install.rdf")) {
-      return "install.rdf";
-    }
-    return null;
-  }
-
   async readString(...path) {
     let buffer = await this.readBinary(...path);
     return new TextDecoder().decode(buffer);
   }
 
   async verifySignedState(addon) {
     if (!shouldVerifySignedState(addon)) {
       return {
@@ -1522,16 +1513,17 @@ class AddonInstall {
         let file = await this.location.installer.installAddon({
           id: this.addon.id,
           source: stagedAddon,
           existingAddonID,
         });
 
         // Update the metadata in the database
         this.addon._sourceBundle = file;
+        this.addon.rootURI = getURIForResourceInFile(file, "").spec;
         this.addon.visible = true;
 
         if (isUpgrade) {
           this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, file.path);
           let state = this.location.get(this.addon.id);
           if (state) {
             state.syncWithDB(this.addon, true);
           } else {
@@ -3271,16 +3263,17 @@ var XPIInstall = {
                     `manifest at ${state.path}, overwriting`, e);
       }
     } else if (Services.prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
       return null;
     }
 
     // Install the add-on
     addon._sourceBundle = location.installer.installAddon({ id, source: file, action: "copy" });
+    addon.rootURI = XPIInternal.getURIForResourceInFile(addon._sourceBundle, "").spec;
 
     XPIStates.addAddon(addon);
     logger.debug(`Installed distribution add-on ${id}`);
 
     Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true);
 
     return addon;
   },
@@ -3644,19 +3637,85 @@ var XPIInstall = {
    */
   async installTemporaryAddon(aFile) {
     let installLocation = XPIInternal.TemporaryInstallLocation;
 
     if (XPIInternal.isXPI(aFile.leafName)) {
       flushJarCache(aFile);
     }
     let addon = await loadManifestFromFile(aFile, installLocation);
-
-    installLocation.installer.installAddon({ id: addon.id, source: aFile });
-
+    addon.rootURI = getURIForResourceInFile(aFile, "").spec;
+
+    await this._activateAddon(addon, {temporarilyInstalled: true});
+
+    logger.debug(`Install of temporary addon in ${aFile.path} completed.`);
+    return addon.wrapper;
+  },
+
+  /**
+   * Installs an add-on from a built-in location
+   *  (ie a resource: url referencing assets shipped with the application)
+   *
+   * @param  {string} base
+   *         A string containing the base URL.  Must be a resource: URL.
+   * @returns {Promise}
+   *          A Promise that resolves when the addon is installed.
+   */
+  async installBuiltinAddon(base) {
+    let baseURL = Services.io.newURI(base);
+
+    // WebExtensions need to be able to iterate through the contents of
+    // an extension (for localization).  It knows how to do this with
+    // jar: and file: URLs, so translate the provided base URL to
+    // something it can use.
+    if (baseURL.scheme !== "resource") {
+      throw new Error("Built-in addons must use resource: URLS");
+    }
+
+    let root = Services.io.getProtocolHandler("resource")
+                       .QueryInterface(Ci.nsISubstitutingProtocolHandler)
+                       .resolveURI(baseURL);
+    let rootURI = Services.io.newURI(root);
+
+    // Enough of the Package interface to allow loadManifest() to work.
+    let pkg = {
+      rootURI: Services.io.newURI("manifest.json", null, rootURI),
+      filePath: baseURL,
+      file: null,
+      verifySignedState() {
+        return {
+          signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+          cert: null,
+        };
+      },
+      hasResource() {
+        return true;
+      },
+    };
+
+    let addon = await loadManifest(pkg, XPIInternal.BuiltInLocation);
+    addon.rootURI = root;
+    await this._activateAddon(addon);
+  },
+
+  /**
+   * Activate a newly installed addon.
+   * This function handles all the bookkeeping related to a new addon
+   * and invokes whatever bootstrap methods are necessary.
+   * Note that this function is only used for temporary and built-in
+   * installs, it is very similar to AddonInstall::startInstall().
+   * It would be great to merge this function with that one some day.
+   *
+   * @param {AddonInternal} addon  The addon to activate
+   * @param {object} [extraParams] Any extra parameters to pass to the
+   *                               bootstrap install() method
+   *
+   * @returns {Promise<void>}
+   */
+  async _activateAddon(addon, extraParams = {}) {
     if (addon.appDisabled) {
       let message = `Add-on ${addon.id} is not compatible with application version.`;
 
       let app = addon.matchingTargetApplication;
       if (app) {
         if (app.minVersion) {
           message += ` add-on minVersion: ${app.minVersion}.`;
         }
@@ -3664,68 +3723,54 @@ var XPIInstall = {
           message += ` add-on maxVersion: ${app.maxVersion}.`;
         }
       }
       throw new Error(message);
     }
 
     let oldAddon = await XPIDatabase.getVisibleAddonForID(addon.id);
 
-    let extraParams = {};
-    extraParams.temporarilyInstalled = true;
-
     let install = () => {
-      addon.state = AddonManager.STATE_INSTALLED;
-      logger.debug(`Install of temporary addon in ${aFile.path} completed.`);
       addon.visible = true;
-      addon.enabled = true;
       addon.active = true;
-      // WebExtension themes are installed as disabled, fix that here.
       addon.userDisabled = false;
 
-      addon = XPIDatabase.addToDatabase(addon, addon._sourceBundle.path);
+      addon = XPIDatabase.addToDatabase(addon, addon._sourceBundle ? addon._sourceBundle.path : null);
 
       XPIStates.addAddon(addon);
-      XPIDatabase.saveChanges();
       XPIStates.save();
     };
 
-    let promise;
+    AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper);
+
     if (oldAddon) {
       logger.warn(`Addon with ID ${oldAddon.id} already installed, ` +
                   "older version will be disabled");
 
       addon.installDate = oldAddon.installDate;
 
-      promise = XPIInternal.BootstrapScope.get(oldAddon).update(
+      await XPIInternal.BootstrapScope.get(oldAddon).update(
         addon, true, install);
     } else {
       addon.installDate = Date.now();
 
       install();
       let bootstrap = XPIInternal.BootstrapScope.get(addon);
-      promise = bootstrap.install(undefined, true, {temporarilyInstalled: true});
+      await bootstrap.install(undefined, true, extraParams);
     }
 
-    AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
-                                           false);
-
-    await promise;
-
     AddonManagerPrivate.callInstallListeners("onExternalInstall",
                                              null, addon.wrapper,
                                              oldAddon ? oldAddon.wrapper : null,
                                              false);
     AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper);
 
     // Notify providers that a new theme has been enabled.
     if (addon.type === "theme")
       AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false);
-
-    return addon.wrapper;
   },
 
   /**
    * Uninstalls an add-on, immediately if possible or marks it as pending
    * uninstall if not.
    *
    * @param {DBAddonInternal} aAddon
    *        The DBAddonInternal to uninstall
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -81,16 +81,17 @@ const FILE_XPI_STATES                 = 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_ADDON_APP_DIR               = "XREAddonAppDir";
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
 const KEY_APP_FEATURES                = "XREAppFeat";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
+const KEY_APP_BUILTINS                = "app-builtin";
 const KEY_APP_GLOBAL                  = "app-global";
 const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
 const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_SYSTEM_USER             = "app-system-user";
 const KEY_APP_TEMPORARY               = "app-temporary";
 
 const TEMPORARY_ADDON_SUFFIX = "@temporary-addon";
 
@@ -99,17 +100,17 @@ const STARTUP_MTIME_SCOPES = [KEY_APP_GL
                               KEY_APP_SYSTEM_SHARE,
                               KEY_APP_SYSTEM_USER];
 
 const NOTIFICATION_FLUSH_PERMISSIONS  = "flush-pending-permissions";
 const XPI_PERMISSION                  = "install";
 
 const XPI_SIGNATURE_CHECK_PERIOD      = 24 * 60 * 60;
 
-const DB_SCHEMA = 28;
+const DB_SCHEMA = 29;
 
 function encoded(strings, ...values) {
   let result = [];
 
   for (let [i, string] of strings.entries()) {
     result.push(string);
     if (i < values.length)
       result.push(encodeURIComponent(values[i]));
@@ -405,16 +406,17 @@ function migrateAddonLoader(addon) {
 const JSON_FIELDS = Object.freeze([
   "changed",
   "dependencies",
   "enabled",
   "file",
   "loader",
   "lastModifiedTime",
   "path",
+  "rootURI",
   "runInSafeMode",
   "signedState",
   "startupData",
   "telemetryKey",
   "type",
   "version",
 ]);
 
@@ -427,16 +429,22 @@ class XPIState {
     this.type = "extension";
 
     for (let prop of JSON_FIELDS) {
       if (prop in saved) {
         this[prop] = saved[prop];
       }
     }
 
+    // Builds prior to be 1512436 did not include the rootURI property.
+    // If we're updating from such a build, add that property now.
+    if (!("rootURI" in this) && this.file) {
+      this.rootURI = getURIForResourceInFile(this.file, "").spec;
+    }
+
     if (!this.telemetryKey) {
       this.telemetryKey = this.getTelemetryKey();
     }
 
     if (saved.currentModifiedTime && saved.currentModifiedTime != this.lastModifiedTime) {
       this.lastModifiedTime = saved.currentModifiedTime;
       this.changed = true;
     } else if (saved.currentModifiedTime === null) {
@@ -457,17 +465,17 @@ class XPIState {
   /**
    * @property {string} path
    *        The full on-disk path of the add-on.
    */
   get path() {
     return this.file && this.file.path;
   }
   set path(path) {
-    this.file = getFile(path, this.location.dir);
+    this.file = path ? getFile(path, this.location.dir) : null;
   }
 
   /**
    * @property {string} relativePath
    *        The path to the add-on relative to its parent location, or
    *        the full path if its parent location has no on-disk path.
    */
   get relativePath() {
@@ -489,16 +497,17 @@ class XPIState {
    */
   toJSON() {
     let json = {
       dependencies: this.dependencies,
       enabled: this.enabled,
       lastModifiedTime: this.lastModifiedTime,
       loader: this.loader,
       path: this.relativePath,
+      rootURI: this.rootURI,
       runInSafeMode: this.runInSafeMode,
       signedState: this.signedState,
       telemetryKey: this.telemetryKey,
       version: this.version,
     };
     if (this.type != "extension") {
       json.type = this.type;
     }
@@ -576,23 +585,40 @@ class XPIState {
     }
 
     this.telemetryKey = this.getTelemetryKey();
 
     this.dependencies = aDBAddon.dependencies;
     this.runInSafeMode = canRunInSafeMode(aDBAddon);
     this.signedState = aDBAddon.signedState;
     this.file = aDBAddon._sourceBundle;
+    this.rootURI = aDBAddon.rootURI;
 
     if (aUpdated || mustGetMod) {
-      this.getModTime(this.file);
-      if (this.lastModifiedTime != aDBAddon.updateDate) {
-        aDBAddon.updateDate = this.lastModifiedTime;
-        if (XPIDatabase.initialized) {
-          XPIDatabase.saveChanges();
+      let file = this.file;
+
+      // Built-in addons should have jar: rootURIs, use the mod time
+      // for the containing jar file for those.
+      if (!file) {
+        let fileUrl = Services.io.newURI(this.rootURI);
+        if (fileUrl.QueryInterface(Ci.nsIJARURI)) {
+          fileUrl = fileUrl.JARFile;
+        }
+        if (fileUrl.QueryInterface(Ci.nsIFileURL)) {
+          file = fileUrl.file;
+        }
+      }
+
+      if (file) {
+        this.getModTime(file);
+        if (this.lastModifiedTime != aDBAddon.updateDate) {
+          aDBAddon.updateDate = this.lastModifiedTime;
+          if (XPIDatabase.initialized) {
+            XPIDatabase.saveChanges();
+          }
         }
       }
     }
   }
 }
 
 /**
  * Manages the state data for add-ons in a given install location.
@@ -805,16 +831,27 @@ class XPIStateLocation extends Map {
 
   get isTemporary() {
     return false;
   }
 
   get isSystem() {
     return false;
   }
+
+  get isBuiltin() {
+    return false;
+  }
+
+  // If this property is false, it does not implement readAddons()
+  // interface.  This is used for the temporary and built-in locations
+  // that do not correspond to a physical location that can be scanned.
+  get enumerable() {
+    return true;
+  }
 }
 
 class TemporaryLocation extends XPIStateLocation {
   /**
    * @param {string} name
    *        The string identifier for the install location.
    */
   constructor(name) {
@@ -830,28 +867,56 @@ class TemporaryLocation extends XPIState
       uninstallAddon() {},
     };
   }
 
   toJSON() {
     return {};
   }
 
-  readAddons() {
-    return new Map();
-  }
-
   get isTemporary() {
     return true;
   }
+
+  get enumerable() {
+    return false;
+  }
 }
 
 var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
 
 /**
+ * A "location" for addons installed from assets packged into the app.
+ */
+var BuiltInLocation = new class _BuiltInLocation extends XPIStateLocation {
+  constructor() {
+    super(KEY_APP_BUILTINS, null, null);
+    this.locked = false;
+  }
+
+  // The installer object is responsible for moving files around on disk
+  // when (un)installing an addon.  Since this location handles only addons
+  // that are embedded within the browser, these are no-ops.
+  makeInstaller() {
+    return {
+      installAddon() {},
+      uninstallAddon() {},
+    };
+  }
+
+  get isBuiltin() {
+    return true;
+  }
+
+  get enumerable() {
+    return false;
+  }
+}();
+
+/**
  * An object which identifies a directory install location for add-ons. The
  * location consists of a directory which contains the add-ons installed in the
  * location.
  *
  */
 class DirectoryLocation extends XPIStateLocation {
   /**
    * Each add-on installed in the location is either a directory containing the
@@ -986,17 +1051,17 @@ class DirectoryLocation extends XPIState
 }
 
 /**
  * An object which identifies a built-in install location for add-ons, such
  * as default system add-ons.
  *
  * This location should point either to a XPI, or a directory in a local build.
  */
-class BuiltInLocation extends DirectoryLocation {
+class SystemAddonDefaults extends DirectoryLocation {
   /**
    * Read the manifest of allowed add-ons and build a mapping between ID and URI
    * for each.
    *
    * @returns {Map<AddonID, nsIFile>}
    *        A map of add-ons present in this location.
    */
   readAddons() {
@@ -1292,17 +1357,17 @@ var XPIStates = {
       }
       changed = changed || loc.changed;
 
       // Don't bother checking scopes where we don't accept side-loads.
       if (ignoreSideloads && !(loc.scope & gStartupScanScopes)) {
         continue;
       }
 
-      if (loc.isTemporary) {
+      if (!loc.enumerable) {
         continue;
       }
 
       let knownIds = new Set(loc.keys());
       for (let [id, file] of loc.readAddons()) {
         knownIds.delete(id);
 
         let xpiState = loc.get(id);
@@ -1607,21 +1672,20 @@ class BootstrapScope {
               await XPIDatabase.updateAddonDisabledState(addon);
           }
         }
       }
 
       let params = {
         id: addon.id,
         version: addon.version,
-        installPath: this.file.clone(),
-        resourceURI: getURIForResourceInFile(this.file, ""),
+        resourceURI: Services.io.newURI(addon.rootURI),
         signedState: addon.signedState,
         temporarilyInstalled: addon.location.isTemporary,
-        builtIn: addon.location instanceof BuiltInLocation,
+        builtIn: addon.location.isBuiltin,
       };
 
       if (aMethod == "startup" && addon.startupData) {
         params.startupData = addon.startupData;
       }
 
       Object.assign(params, aExtraParams);
 
@@ -1669,43 +1733,43 @@ class BootstrapScope {
 
     // Mark the add-on as active for the crash reporter before loading.
     // But not at app startup, since we'll already have added all of our
     // annotations before starting any loads.
     if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
       XPIProvider.addAddonsToCrashReporter();
     }
 
-    logger.debug(`Loading bootstrap scope from ${this.file.path}`);
+    logger.debug(`Loading bootstrap scope from ${this.addon.rootURI}`);
 
     if (this.addon.isWebExtension) {
       switch (this.addon.type) {
         case "extension":
         case "theme":
-          this.scope = Extension.getBootstrapScope(this.addon.id, this.file);
+          this.scope = Extension.getBootstrapScope();
           break;
 
         case "locale":
-          this.scope = Langpack.getBootstrapScope(this.addon.id, this.file);
+          this.scope = Langpack.getBootstrapScope();
           break;
 
         case "dictionary":
-          this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file);
+          this.scope = Dictionary.getBootstrapScope();
           break;
 
         default:
           throw new Error(`Unknown webextension type ${this.addon.type}`);
       }
     } else {
       let loader = AddonManagerPrivate.externalExtensionLoaders.get(this.addon.loader);
       if (!loader) {
         throw new Error(`Cannot find loader for ${this.addon.loader}`);
       }
 
-      this.scope = loader.loadScope(this.addon, this.file);
+      this.scope = loader.loadScope(this.addon);
     }
   }
 
   /**
    * Unloads a bootstrap scope by dropping all references to it and then
    * updating the list of active add-ons with the crash reporter.
    */
   unloadBootstrapScope() {
@@ -1833,17 +1897,20 @@ class BootstrapScope {
   async _uninstall(reason, callUpdate, extraArgs) {
     if (this.started) {
       await this.shutdown(reason, extraArgs);
     }
     if (!callUpdate) {
       this.callBootstrapMethod("uninstall", reason, extraArgs);
     }
     this.unloadBootstrapScope();
-    XPIInstall.flushJarCache(this.file);
+
+    if (this.file) {
+      XPIInstall.flushJarCache(this.file);
+    }
     XPIInstall.flushChromeCaches();
   }
 
   /**
    * Calls the appropriate sequence of shutdown, uninstall, update,
    * startup, and install methods for updating the current scope's
    * add-on to the given new add-on, depending on the current state of
    * the scope.
@@ -1978,23 +2045,23 @@ var XPIProvider = {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       } catch (e) {
         return null;
       }
       return new DirectoryLocation(aName, dir, aScope, aLocked);
     }
 
-    function BuiltInLoc(name, scope, key, paths) {
+    function SystemDefaultsLoc(name, scope, key, paths) {
       try {
         var dir = FileUtils.getDir(key, paths);
       } catch (e) {
         return null;
       }
-      return new BuiltInLocation(name, dir, scope);
+      return new SystemAddonDefaults(name, dir, scope);
     }
 
     function SystemLoc(aName, aScope, aKey, aPaths) {
       try {
         var dir = FileUtils.getDir(aKey, aPaths);
       } catch (e) {
         return null;
       }
@@ -2018,19 +2085,21 @@ var XPIProvider = {
       [() => TemporaryInstallLocation, TemporaryInstallLocation.name, null],
 
       [DirectoryLoc, KEY_APP_PROFILE, AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR, [DIR_EXTENSIONS], false],
 
       [SystemLoc, KEY_APP_SYSTEM_ADDONS, AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR, [DIR_SYSTEM_ADDONS]],
 
-      [BuiltInLoc, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
+      [SystemDefaultsLoc, KEY_APP_SYSTEM_DEFAULTS, AddonManager.SCOPE_PROFILE,
        KEY_APP_FEATURES, []],
 
+      [() => BuiltInLocation, KEY_APP_BUILTINS, AddonManager.SCOPE_SYSTEM],
+
       [DirectoryLoc, KEY_APP_SYSTEM_USER, AddonManager.SCOPE_USER,
        "XREUSysExt", [Services.appinfo.ID], true],
 
       [RegistryLoc, "winreg-app-user", AddonManager.SCOPE_USER,
        "ROOT_KEY_CURRENT_USER"],
 
       [DirectoryLoc, KEY_APP_GLOBAL, AddonManager.SCOPE_APPLICATION,
        KEY_ADDON_APP_DIR, [DIR_EXTENSIONS], true],
@@ -2729,33 +2798,35 @@ var XPIProvider = {
         XPIDatabase.updateAddonAppDisabledStates();
         break;
       }
     }
   },
 };
 
 for (let meth of ["getInstallForFile", "getInstallForURL", "getInstallsByTypes",
-                  "installTemporaryAddon", "isInstallAllowed",
-                  "isInstallEnabled", "updateSystemAddons"]) {
+                  "installTemporaryAddon", "installBuiltinAddon",
+                  "isInstallAllowed", "isInstallEnabled",
+                  "updateSystemAddons"]) {
   XPIProvider[meth] = function() {
     return XPIInstall[meth](...arguments);
   };
 }
 
 for (let meth of ["addonChanged", "getAddonByID", "getAddonBySyncGUID",
                   "updateAddonRepositoryData", "updateAddonAppDisabledStates"]) {
   XPIProvider[meth] = function() {
     return XPIDatabase[meth](...arguments);
   };
 }
 
 var XPIInternal = {
   BOOTSTRAP_REASONS,
   BootstrapScope,
+  BuiltInLocation,
   DB_SCHEMA,
   KEY_APP_SYSTEM_ADDONS,
   KEY_APP_SYSTEM_DEFAULTS,
   PREF_BRANCH_INSTALLED_ADDON,
   PREF_SYSTEM_ADDON_SET,
   SystemAddonLocation,
   TEMPORARY_ADDON_SUFFIX,
   TemporaryInstallLocation,
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -443,18 +443,18 @@ var SlightlyLessDodgyBootstrapMonitor = 
       equal(params.oldVersion, lastParams.version,
             "params.version should match last call");
     } else {
       equal(params.version, lastParams.version,
             "params.version should match last call");
     }
 
     if (method !== "update" && method !== "uninstall") {
-      equal(params.installPath.path, lastParams.installPath.path,
-            `params.installPath should match last call`);
+      equal(params.resourceURI.spec, lastParams.resourceURI.spec,
+            `params.resourceURI should match last call`);
 
       ok(params.resourceURI.equals(lastParams.resourceURI),
          `params.resourceURI should match: "${params.resourceURI.spec}" == "${lastParams.resourceURI.spec}"`);
     }
   },
 
   checkStarted(id, version = undefined) {
     let started = this.started.get(id);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js
@@ -0,0 +1,60 @@
+"use strict";
+
+/* globals browser */
+
+// Tests installing an extension from the built-in location.
+add_task(async function test_builtin_location() {
+  let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_SYSTEM;
+  Services.prefs.setIntPref("extensions.enabledScopes", scopes);
+  Services.prefs.setBoolPref("extensions.webextensions.background-delayed-startup", false);
+
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+  await promiseStartupManager();
+
+  const ID = "builtin@tests.mozilla.org";
+  let xpi = await AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {gecko: {id: ID}},
+    },
+    background() {
+      browser.test.sendMessage("started");
+    },
+  });
+
+  // The built-in location requires a resource: URL that maps to a
+  // jar: or file: URL.  This would typically be something bundled
+  // into omni.ja but for testing we just use a temp file.
+  let base = Services.io.newURI(`jar:file:${xpi.path}!/`);
+  let resProto = Services.io.getProtocolHandler("resource")
+                         .QueryInterface(Ci.nsIResProtocolHandler);
+  resProto.setSubstitution("ext-test", base);
+
+  let wrapper = ExtensionTestUtils.expectExtension(ID);
+
+  await AddonManager.installBuiltinAddon("resource://ext-test/");
+
+  await wrapper.awaitStartup();
+  await wrapper.awaitMessage("started");
+  ok(true, "Extension was installed successfully in built-in location");
+
+  let addon = await promiseAddonByID(ID);
+  notEqual(addon, null, "Addon is installed");
+  equal(addon.isActive, true, "Addon is active");
+
+  // After a restart, the extension should start up normally.
+  await promiseRestartManager();
+  await wrapper.awaitStartup();
+  await wrapper.awaitMessage("started");
+  ok(true, "Extension in built-in location ran after restart");
+
+  addon = await promiseAddonByID(ID);
+  notEqual(addon, null, "Addon is installed");
+  equal(addon.isActive, true, "Addon is active");
+
+  await wrapper.unload();
+
+  addon = await promiseAddonByID(ID);
+  equal(addon, null, "Addon is gone after uninstall");
+
+  await promiseShutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -58,16 +58,17 @@ tags = blocklist
 skip-if = os == "android"
 tags = blocklist
 [test_blocklist_url_ping_count.js]
 tags = blocklist
 [test_blocklistchange.js]
 # Times out during parallel runs on desktop
 requesttimeoutfactor = 2
 tags = blocklist
+[test_builtin_location.js]
 [test_cache_certdb.js]
 [test_cacheflush.js]
 [test_childprocess.js]
 [test_compatoverrides.js]
 head = head_addons.js head_compat.js
 [test_corrupt.js]
 [test_crash_annotation_quoting.js]
 [test_db_path.js]