hendrich@ - June 2023
Typically, you would develop and build your Chrome app on a Windows/Mac/Linux device. In the past, you were able to run the Chrome app directly on that platform using the local Chrome browser until Chrome apps were deprecated (M104). There used to be a policy-based escape hatch (ChromeAppsEnabled), which stopped working in M114. The only remaining escape hatch is an allowlist via command line flag (chrome.exe --enable-features=ChromeAppsDeprecation:allow_list/mplpmdejoamenolpcojgegminhcnmibo), but that one will also not be around forever (TBD).
This document outlines how you can modify your development flow to continue developing on Windows/Mac/Linux and then testing the built Chrome app on a ChromeOS device with ease. Several engineers at Google have used the same flow for developing Chrome apps/extensions.
Typically, you would have just launched the local Chrome browser, navigated to the chrome://extensions page and clicked the Load unpacked to load the source directory of your app.
In theory, said flow is also possible from the Chromebook. However, this would require you to move the source files to the Chromebook every time (e.g. sync via GoogleDrive) and therefore does not allow quick development cycles. The alternative would be to upload the source directory to Chrome webstore, but that would take even longer.
Instead, we will
- pack the Chrome app source directory into a packaged
.crx - Serve the packaged app from the development machine via self-hosting
- Force-install it to the Chromebook
- Refresh the Chromebook to pick up the latest changes
For steps 1+2, I have built an python script that automates these steps for us. That script will be executed whenever you make a change to the sources. Force-installing the extension (step 3) only needs to be set up once. Whenever you want the changes to be reflected on the Chromebook, you can navigate to chrome://extensions and with a click on Update, the Chromebook will automatically pull the latest version of the app from your development machine. The script will automatically increase the app’s minor version in the manifest to trigger an update.
update_local_extension_host.py- the script you will run to package and serve the updated source directory.hostdirectory - this is where the packaged.crxand updatedupdate_manifest.xmlwill land in. This directory will be served from your development machine. You typically don’t have to touch this directory.PEMsdirectory - this is where the script will organize the private keys used to sign and pack your source directories into a.crxfile. The directory also contains anids.jsonfile, which contains the mappings from your source directory names (= app identifier) to the used key. You can have the script generate a key for your (resulting in a random extension ID) or use an existing key (resulting in a known extension ID). More on that below. It is not possible to control the extension ID viakeyattribute in themanifest.jsonas you would do in the “load unpacked” flow.
Let's imagine you have built a Chrome app/extension at /path/to/your/awesome_app with version: 4.2 in its manifest.json.
You can run the python script as following:
$ python3 update_local_extension_host.py <path_to_extension_directory_directory> [--extension_host_url <extension_host_url>] [--chrome_path <chrome_path>]
The arguments are:
path_to_extension_directory- the path to your source directory containing themanifest.json, i.e./path/to/your/awesome_app. In this case the final directory nameawesome_appwould be used as app identifier internally. This parameter is mandatory.extension_host_url- the URL to where your packaged extensions will be hosted from (see below). Optional, defaults to http://127.0.0.1:8888. If you plan to use the packed app on another device, you likely want to use your development machine's local IP address / hostname here instead.chrome_path- the path to the chrome binary that should be used for packaging. Optional, defaults togoogle-chrome(works on linux). On Windows/Mac, you need to update this parameter to your local Chrome's binary path.
The script will do the following:
- Read the app's version from the manifest.json
- Check if the app/extension (identified via directory name) has been served before / has a key. If not:
- Generate a new key and store it as
${EXTENSION_ID}.pemin thePEMsdirectory - insert mapping
${DIRECTORY_NAME}: ${EXTENSION_ID}intoPEMs/ids.json - add an
<app>item intohost/update_manifest.xml
- Generate a new key and store it as
- Read the app's last served version from
hosts/update_manifest.xml - Compute the new app version, i.e.
new_version = increase_minor_version(max(manifest_version, last_served_version)). The initial4.2from our update manifest would then be4.2.1, then4.2.2,4.2.3, etc. - Temporarily upate the
versionandupdate_urlfields in themanifest.jsonwith the new version and givenextension_host_url - Pack the app using
chrome_pathand the--pack-extension=/path/to/your/awesome_appargument - Move the packed
.crxinto thehosts directory - Update the
hosts/update_manifest.xmlto reflect the latest version and.crxfile - Print out the extension ID and newly served version
If you already have a private key (.pem file) and want the script to use that in order to receive the associated extension ID, simply move the .pem file into the PEMs directory named as ${EXTENSION_ID}.pem and add an entry ${DIRECTORY_NAME}: ${EXTENSION_ID} into PEMs/ids.json. Next time you run the script, it will use your given private key for signing the .crx file, which you can confirm by the logged extension ID at the end.
In order to self-host the packed apps/extensions, they need to be available from your test device. We simply do so by hosting the host directory with a simple http server on a given port (e.g. 8888).
$ cd hosts
$ python3 -m http.server 8888 --bind 127.0.0.1
This process needs to be kept running. You can interrupt/cancel it with CTRL+C when done.
You can test that everything worked by navigating to http://YOUR_LOCAL_IP_HERE:8888/update_manifest.xml, which should show you an XML document with an entry for your app/extension.
Configure the ExtensionInstallForcelist to force-install the self-hosted and packaged app/extension by specifying the extension ID and insert URL to the update_manifest.xml (http://YOUR_LOCAL_IP_HERE:8888/update_manifest.xml) as From custom URL.
To use this, your device must be in developer mode (see below), with rootfs verification removed (see below) and should not receive policy from anywhere else (otherwise ExtensionInstallForcelist would conflict on different sources).
On the device, open a shell (see below) and create a policy.json file in /etc/opt/chrome/policies/managed with the following contents:
{
"ExtensionInstallForcelist": ["${EXTENSION_ID};http://YOUR_LOCAL_IP_HERE:8888/update_manifest.xml"]
}The entry should show up as ["${EXTENSION_ID};http://YOUR_LOCAL_IP_HERE:8888/update_manifest.xml"] on the device's chrome://policy page then. You should also see the packed app/extension on the chrome://extensions page.
Every time you want to test a code change to your app/extension, you would run the update_local_extension_host command and then go to the chromebook's chrome://extensions page and click the Update button. The device should pick up the latest version of the .crx immediately and show the updated version.
Typically, you would use chrome://inspect page to access the logs of your app/extension. When the app/extension now runs on a different device, you can still use the chrome://inspect page on that device or use the "remote debugging" explained here to access the developer tools of your testing device on your development device.
WARNING: This wipe all data on your device!
- Enter recovery mode first: Hold
ESC+Refresh(F2) and pressPower - In recovery mode, press
CTRL+Dfollowed byENTERto accept - Wait for the device to enable developer mode
- In developer mode, the device will boot to a "developer mode warning" screen. Press
CTRL+Dto skip or wait - More resources here and here
Requires a device in developer mode.
- Press
CTRL+ALT+Refresh (F2) - Login as
rootatlocalhost login:prompt (insert password if required, see below)
From shell:
- Run command:
sudo /usr/share/vboot/bin/make_dev_ssd.sh --remove_rootfs_verification - If the command does not succeed and it recommends running it with a
--partitionargument, copy the recommended command and run it again. - Run command:
reboot
From shell:
- Run commands:
echo “--remote-debugging-port=9222” >> /etc/chrome_dev.confecho “--force-devtools-available” >> /etc/chrome_dev.confrestart ui
From shell:
- Run command:
/usr/libexec/debugd/helpers/dev_features_ssh - Set password by running command
passwd
From shell:
- Press
CTRL+ALT+Back (F1)
- Click on the clock in bottom right corner to open quick settings
- Click on Ethernet/Wifi
- Click on
(i)(top right corner)
- Run command
ssh -L 9222:localhost:9222 root@LOCAL_IP_ADDR_OF_YOUR_CHROMEBOOK_HERE - enter password configured via
passwdbefore
- Navigate to chrome://inspect
- You should now see the developer tools for all open tabs/apps/extensions from the test device (might take a second to load)