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.host
directory - this is where the packaged.crx
and updatedupdate_manifest.xml
will land in. This directory will be served from your development machine. You typically don’t have to touch this directory.PEMs
directory - this is where the script will organize the private keys used to sign and pack your source directories into a.crx
file. The directory also contains anids.json
file, 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 viakey
attribute in themanifest.json
as 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_app
would 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}.pem
in thePEMs
directory - 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.2
from our update manifest would then be4.2.1
, then4.2.2
,4.2.3
, etc. - Temporarily upate the
version
andupdate_url
fields in themanifest.json
with the new version and givenextension_host_url
- Pack the app using
chrome_path
and the--pack-extension=/path/to/your/awesome_app
argument - Move the packed
.crx
into thehosts directory
- Update the
hosts/update_manifest.xml
to reflect the latest version and.crx
file - 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+D
followed byENTER
to accept - Wait for the device to enable developer mode
- In developer mode, the device will boot to a "developer mode warning" screen. Press
CTRL+D
to skip or wait - More resources here and here
Requires a device in developer mode.
- Press
CTRL+ALT+Refresh (F2)
- Login as
root
atlocalhost 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
--partition
argument, copy the recommended command and run it again. - Run command:
reboot
From shell:
- Run commands:
echo “--remote-debugging-port=9222” >> /etc/chrome_dev.conf
echo “--force-devtools-available” >> /etc/chrome_dev.conf
restart 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
passwd
before
- 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)