Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
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.gzThis 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.