Bundling Python inside an Electron app#
For Datasette Desktop I chose to bundle a full version of Python 3.9 inside my Datasette.app
application. I did this in order to support installation of plugins via pip install
- you can read more about my reasoning in Datasette Desktop—a macOS desktop application for Datasette.
I used python-build-standalone for this, which provides a version of Python that is designed for easy of bundling - it’s also used by PyOxidize. Both projects are created and maintained by Gregory Szorc.
In development mode#
In my Electron app’s root folder I ran the following:
1wget https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-apple-darwin-install_only-20210724T1424.tar.gz2tar -xzvf cpython-3.9.6-x86_64-apple-darwin-install_only-20210724T1424.tar.gz
This gave me a python/
subfolder containing a full standalone Python, ready to run on my Mac.
Running python/bin/python3.9 --version
confirms that this is working correctly.
Calling Python from Electron#
I used the Node.js child_process.execFile()
function to execute Python scripts from inside Electron, like this:
1const cp = require("child_process");2const util = require("util");3const execFile = util.promisify(cp.execFile);4
5await execFile(path_to_python, ["-m", "random"]);
path_to_python
needs to be the path to that python3.9
executable. I wrote a findPython()
function to find that, like so:
1const fs = require("fs");2
3function findPython() {4 const possibilities = [5 // In packaged app6 path.join(process.resourcesPath, "python", "bin", "python3.9"),7 // In development8 path.join(__dirname, "python", "bin", "python3.9"),9 ];10 for (const path of possibilities) {11 if (fs.existsSync(path)) {12 return path;13 }14 }15 console.log("Could not find python3, checked", possibilities);16 app.quit();17}
The resourcesPath
bit here works for the packaged and deployed application.
Packaging the app#
I needed that python
folder to be bundled up as part of the Datasette.app
application bundle. I achieved that by adding the following to my "build"
section in package.json
:
1 "extraResources": [2 {3 "from": "python",4 "to": "python",5 "filter": [6 "**/*"7 ]8 }9 ]
This causes electron-builder
to copy the contents of that python
folder into Datasette.app/Contents/Resources/python
in the packaged application.
My findPython()
function earlier knows to look for it there by creating a path to it starting with process.resourcesPath
.
Signing and notarizing#
I wrote extensive notes on signing and notarizing in Signing and notarizing an Electron app for distribution using GitHub Actions, which includes details on how I signed the Python binaries that ended up included in the package due to this pattern.