How to package a python project with Cosmopolitan
Knowledge is half the battle.
Cosmopolitan Libc is a way of building Actual Portable Executables which run, unmodified, on Windows, Linux, and macOS and on multiple architectures.
I first heard about Cosmopolitan from Simon Willison in the context of llamafile, but it was Shalev NessAiver who showed me how I could package up my python project into a single executable that can run everywhere.
There are three main things you need to do:
- Compile the
.py
files to.pyc
. - Add them to the
python
executable zip file. - Add a special
.args
file that runs your program instead of the python REPL.
I'll illustrate how this works using ds
which I used to build a Cosmopolitan version of itself.
# inside your pyproject.toml
[tool.ds.scripts]
_.env.PKG = "ds"
_.env.DIR_SITE = "dist/cosmo/Lib/site-packages"
cosmo-clean = "rm -rf $DIR_SITE || true"
cosmo-setup = """
mkdir -p $DIR_SITE;
cp -r src/$PKG $DIR_SITE;
printf -- "-m\\n$PKG\\n..." > dist/cosmo/.args;
"""
cosmo-fetch.cwd = "dist/cosmo"
cosmo-fetch.shell = """
wget -N https://cosmo.zip/pub/cosmos/bin/python;
chmod +x python;
cp python $PKG.zip;
"""
cosmo-compile = """
./dist/cosmo/python -m compileall -b $DIR_SITE/$PKG;
find $DIR_SITE/$PKG -name "*.py" -delete -print;
"""
cosmo-update.cwd = "dist/cosmo"
cosmo-update.shell = """
zip -r $PKG.zip .args Lib;
zip -d $PKG.zip 'Lib/site-packages/pip*';
zip -d $PKG.zip 'usr/*';
mv $PKG.zip $PKG;
"""
cosmo = ["cosmo-*"]
We could, of course, do this as a single command (which is how it currently is in ds
), but I think this example illustrates how ds
works in practice.
The _.env
lines at the beginning set some environment variables for all tasks and cosmo
at the end will run all the tasks that start with cosmo-
(in the order in which they appear in the file).
First, we have cosmo-clean
which deletes any previous compiled code. Next, cosmo-setup
creates all the necessary directories (and their parents, if missing) and the special .args
file which are the arguments to the python
executable to run so that we don't trigger the REPL.
Then cosmo-fetch
downloads the latest Cosmopolitan Python which is a fancy zip file. The wget -N
will only download the file if it is newer than the one we already have. We set the executable bit and copy it to a .zip
extension so that the zip
program can operate on it properly. The place I copied this from did a clever thing of also downloading a Cosmpolitan Zip which runs everywhere.
cosmo-compile
then uses the Cosmopolitan Python executable to compile all the .py
files into .pyc
and delete all the .py
files. We delete them so that they don't get added to the .zip
.
Finally, we add the .args
file and Lib
folder to the .zip
. We remove the pre-packaged pip
and usr/
directories to remove about 3.4 MB of files we don't need packaged with our executable. We rename the .zip
and we're done.
We run this whole process with ds cosmo
.