There are a couple other tools that take the same approach, including PyOxidizer[3], which was written by a Mercurial maintainer.
[1] https://docs.bazel.build/versions/master/be/python.html#py_r...
[2] https://github.com/erain/bazel-python-example
[3] https://pyoxidizer.readthedocs.io/en/stable/overview.html
As far as I know the only language making static binaries easily is Go, but it was a first class language design principle.
For everybody else it’s a jenky 1000 line Makefile. And don’t get me started on cross-compiling!
Rust does this as well.
The official high level build tool, `cargo`, uses a declarative TOML file for dependency management and supports lock files for deterministic builds. The default output is a single, statically linked binary.
Rust does depend on libc (like Go) which brings in dynamic linking on some platforms. But Cargo supports easy cross-compilation, and the `x86_64-unknown-linux-musl` target will produce a fully static binary.
Your milage might vary, but I think the former is still very much a work in progress.
It can't do this for non-Linux executables (e.g. Windows, Mac). On those OSs, the executables are dynamically linked with system libraries.
Edit: I mention cgo as many who want to cross-compile a statically linked binary may want to interface with other libs via FFI and this is a huge gotcha. It is a bit tangential to strict "static linking binary building".
I decided "to heck with it" and went back to bash. There's no built-in JSON parser but I can use 'grep' and 'cut' as well as anyone so the end result is the same. I push it to our repo, I tell coworkers to run it, and I wash my hands of the thing.
Another thing to consider is more of a middle-ground approach. Most systems do have a python interpreter, so you can use a lot of base python without worrying about dependency hell. I use inline python in bash all the time, e.g.
ls | python -c 'import sys,json;lines=sys.stdin.read();print(json.dumps(list(filter(bool,lines.split("\n"))),sort_keys=True,indent=2))'
You can even use variable substitution, if you surround the python code in double quotes. Even mix f-strings and bash substitution python -c "print(f'Congrats, ${USER}, you are visitor number ${RANDOM}. This is {__name__}, running in $(pwd)')" # python << EOPYTHON
print("Congrats, ${USER}")
print("You are visitor ${RANDOM}")
print("This is {__name__}, running in ${pwd}")
print("It's a heredoc to allow both quote characters")
EOPYTHONedit: You probably already know this, but for anyone reading along, piping `ls` is unsafe if you plan to use the paths for anything except for printing them out. A path on linux can contain any byte except for NULL, so when `ls` prints them out, you can get broken behavior if you try to break on newlines.
With Bash, your best bet for portability is to run scripts in a Docker container. If you want portable code, you have to bundle your dependencies--there's no free lunch here, including Bash.
This didn't solve the multiple versions of python on the host. That was managed by having a bootstrap script written in python2 that would set up the development environment to a consistent state (i.e. install homebrew, install required packages) that anyone wanting to run the tools would run (no "getting started guides") which also versioned itself & was idempotent (generally robust against running multiple times). We also shipped this to our external partners in the factory. Generally worked well as once you ran the necessary scripts once no further internet access was required.
It wasn't easy but eventually it worked super reliably.
This was critical for devs because this was the underlying thing for all scripts devs ran (build system, terminal to device, unit tests, etc etc). Startup latency was key & I spent time optimizing that to feel as instant as a native executable unless the virtual environment changed which isolated the expensive part (& generally happened more & more rarely for any given tool as I found the dependency set to mature & freeze pretty quickly).
This had a great side benefit making it super-easy to run the scripts once on an internet-connected device & then use that as the base image for all the factory machines that could then be offline because all the virtual envs had been initialized.
It's Scala, runs on the JVM, and is perfect for writing scripts. (It has a great built in dependency resolver, I mean it uses Ivy, but it downloads the dep by itself, you just import it via the "maven coordinate" - http://ammonite.io/#IvyDependencies )
It gives you a lot more safety/correctness than Python, and it's a bit simpler to install too. (No need to compile extensions, just get JDK8 and it'll run.)
There are lots of ways to handle this problem, but if you're handling lots of systems, you presumably already have a method you use to keep them up to date and secure. You presumably are also installing Python from the system packages (if not, you probably shouldn't be writing system utils in it unless you can ensure it's the same on every system you guys maintain, in which case your dependency problem shouldn't be a problem), so tie into that mechanism. It's a lot easier to reason about when there aren't two competing systems, and presumably you aren't going to do away with the security updates the distro provides.
I recently received a script from partner company that used such of script for forwarding data to their API. It was quite long and had few dependencies that were not visible until you (stupidly) executed it.
Few random thoughts:
- Bash scripts can be ran in environments where all dependencies to binaries are not met. In these cases the script might cause damage if they expect that everything is available.
- When someone is unexpectedly required to modify the script it can be difficult or cause issues when this is done by inexperienced developer (in this age I wouldn't be surprised)
- If the script uses a program that is required to be certain version for getting wanted results it may cause issues
- The environment where script is ran is usually not a vacuum. Another scripts might change environment variables or change/remove programs in general
While dependencies with Python can cause issues in the future. The trade-off is having some sort of control as long you don't execute other binaries directly.
There is the question of the dependencies of a shell script, but I find in practice just checking for deps like `curl` at the beginning leads to be a better user experience. It's unlikely that there is going to be a ton of tools you require, and the tools you do require are probably going to be good about backwards compatibility [curl again as an example].
Sure. The argument is that it's a lot easier for the user of the program to read an error message that says "jq is required. Run apt-get install jq or homebrew install jq" than to fuck around with the python or ruby ecosystem, especially if they don't work in those languages.
> Finally there are differences in how `/bin/sh` will interpret things (which there shouldn't be) depending upon underlying shell is running ksh, zsh, bash, dash
Do you have an example of code that is written to the POSIX standard of shell that runs differently? I only write POSIX shell, and use https://github.com/koalaman/shellcheck to verify that to prevent that exact thing.
There are differences between versions. I can't even remember what they are off the top of my head like I used to, which makes them all the more aggravating to discover again.
But I would recommend sticking to a subset of bash, not any of the new fancy features like 'globstar' which allows recursively globbing.
There are tools to manage these kinds of tests, like bashenv. But you're in the same problem scope at that point.
[1] Well, it has $@, which you can use as a general-purpose array with some hacks[2], but that's no way to live.
But even the nicest shell doesn't solve the dependency problem like statically compiled programs. If I could take my currently running Python code and produce some artifact that would run with nothing other than the python binary I think we'd be in a much better place.
Ohh apparently all I've needed in my life is zipapps.
One drawback is that if you want your Bash script to work on macOS, you need to restrict yourself to features that exist on version 3.2 (from 2006) because that's the latest version that will ever be included on macOS by default.
> If I could take my currently running Python code and produce some artifact that would run with nothing other than the python binary I think we'd be in a much better place.
See my other comment: https://news.ycombinator.com/item?id=23338316
Let me put it like this: I'm a programmer. I don't mind making programming a bit harder for myself if it means that I get to avoid a lot of the non-programing minutia that's part of a modern interpreted environment.
Also, if you're willing to take a dependency on jq, the issue goes away completely.
We even how have a nice tool to automate the bundling for you:
https://pypi.org/project/shiv/
Of course you still have to figure out how to get a Python installed on the final machine, that's the price to pay to be an interpreted language.
We don't have yet a story to ship a beautiful exe/dmg/deb/rpm that embeds the zipapp and libpython in an easy way.
This is a non-trivial thing to handwave away.
Using an interpretted language always leads to this.
I know no popular interpretted language with a seamless experience to ship a standalone exe.
In fact, Python is probably the one with the best story here, since it has nuitka (https://nuitka.net/), which allows to compile Python code into a fully standalone exe.
But then you need to install a compiler, headers, etc. And no cross compilation of course. Not to mention on Linux, you have to ensure you target the lowest version of libc you can.
You are still very far from Go or Rust, and I'm hoping one day that RustPython will succeed because that would mean an amazing deployment story.
Meanwhile, you trade the ease of deployment of compiled languages for the ease of development of interpretted ones.
I think it's a fare trade for most people: you dev the program much more often that you deploy it.
That doesn't mean we shouldn't work, as a community, to improve the deployment story. It's a serious hindrance.
That's the raison d'être of the Briefcase project (https://beeware.org/project/projects/tools/briefcase/). It's still in progress, but the last prez I saw on it was quite impressive already.
Isn't RustPython just an alternative interpreter to CPython, implemented in Rust instead of C?
How would RustPython offer better deployment than CPython?
So if one day RustPython gets compatible enought with CPython that you can use it as a drop in replacement, you can start creating a tool that compiles any Python VM for any target, and bring along your program with it. Making a standalone version of it would become much easier.
Right now, doing so either requires you to bring in a pre-compile version of cpython for your target (which is what briefcase does) or compile the thing yourself with gcc + headers + deps(which is what nuitka does).
It's not easy.
I don't think this will ever happen unless the community converges on a standard C-extension interface. Presently Python leans so hard on C-extensions, but there is no standard interface--if you're writing a C-extension library, you just depend on whatever obscure corner of CPython that suits your purpose. If you're writing an alternative Python interpreter, you have to implement the entire surface area of CPython, which generally means you must implement CPython exactly and you are severely restricted on the improvements you can make. At that point, why even bother?
Fortunately, I think there are emerging candidate interfaces, but the community needs to either update C-extension packages to use those interfaces or support packages (and maintainers) who already do. https://github.com/pyhandle/hpy.
The rest is not popular enought to be a blocker. You will hear them scream a lot, but they will be like 0.00001% of the user base, and we can just tell them to stay on CPython with its limitations. They don't lose anything, just not gain anything either.
Those C extensions authors are directly in communication with Python core devs, when they are not core devs themselves, so if HPY is adopted, we can expect a total adoption under 5 years.
Numpy authors already said it would take 1 year to adopt it.
Give the huge number of benefits of HPY, I deeply hope it will be a success.
Indeed.
I don't know exactly how it goes but I'm pretty sure it's a horror story.
That being said, a lot of extensions are pre-compiled and provided as wheel, which is the case for tensorflow (I don't know for CUDA, I can't test on a laptop without a GPU).
Let's see what this means:
$ py -m venv test
$ test\Scripts\activate
$ pip install tensorflow
$ code hello_tensor.py
# import tensorflow as tf
# def main():
# with tf.compat.v1.Session() as sess:
# a = tf.constant(3.0)
# b = tf.constant(4.0)
# c = a+b
# print(sess.run(c))
Now with shiv: $ copy hello_tensor.py test\Lib\site-packages
$ shiv -e hello_tensor.main --site-packages test\Lib\site-packages\ -o hello_tensor.pyz
$ python hello_tensor.pyz
...
2020-05-28 17:31:46.580704: I tensorflow/compiler/xla/service/service.cc:176] StreamExecutor device (0): Host, Default Version
7.0
So it works fine, but remember:- it will only run on the system this particular wheel has been designed to run on. In my case cp38-win_amd64.
- it will come bundled with tensorflow, which is a behemot, meaning your hello world pyz will around 500 Mo.
- it needs to unzip, so the first run will be REALLY slow
For something like this, I would advice a more generic deployment tool, like fabric 2 if it's remote, or a make-like tool such as doit if it's local only.
Make your deployment script, and zipapp that.
If so, the extraction is going to make startup very slow. If not, that’s just messy. Either way, it’s not ideal.
Yes, it's not ideal.
But it beats shipping your entire dev env to the server.
I find it a good compromise. The extraction is done is $HOME/.shiv/{zipappname}_{zipapphash} so it's not a horrible mess. But if your project is big, you do have to clean up the old install because it can eat a significant amount of space.
For you regular cmd tool though, it's a blip.
Also for Java. The installs usually include their own favorite version of Java alongside the actual application.
For PATH with cron or others, I use the full path to the virtualenv such as /path/to/project/.venv/bin/python. The path can be extracted by "which" or "Get-Command" when the venv is active.
Using a python version different from the system python version is probably the messiest part but well, targeting 3.6 is alright.
I do agree it could be better and it's not quite as streamlined as other ecosystems.
So a huge number of cases can be handled with just that. It will be "reproducible" enought for a lot of people.
That's why I use "poetry export -f requirements.txt > requirements.txt" instead of pip freeze. It only exports prod requirements from the poettry lock file.
You pass cron the full path to your entry point. Where’s the rigamarole?
0 0 * * * /path/to/bash/script/to/init/pyenv && /my/path/to/poetry run /my/path/to/python.py -arg1
0 0 * * * /path/to/interpreter/created/by/poetry/bin/python myscript.py
I seem to recall that with python2.7 that calling bin/python in a virtualenv without activating the virtualenv did not used to "work" (i.e. it would use the system packages). Did this change at some point or is my memory just wrong?
An anecdote: Homebrew uses this method for shipping python executables.
The exception is if your python thingy is "the main thing" running on a server, i.e. your customer facing webapp.
My $.10 anyway
This of course leads to other issues to solve, now that your development environment doesn't actually mirror production.
I wonder how many others are doing the same but are keeping it close to their chest because it’s such an amazing advantage.
And if you're on Windows, your Docker host is in a virtual machine, so networking and volumes are not so simple anymore.
Replacing one kind of complexity by another is not a solution, it is a trade-off.
According to the user needs, your dockerized application will run with different base distro. Alpine and musl for small OS footprint ? Or Debia(or debian-slim) for glibc compatibility ?
Those concerns are the same with or without Docker. Docker makes things easy, just not those things because it is not its purpose.
The packager of an application is part of the project's team. It's not up to the user to package your application.
- Runtime comes with a package manager
- Dependencies (not just imports, but tooling) are fully manifested in a project-local file
- Installs dependencies in a project-local directory
- Can specify exact package versions if you want maximum stability
- Left-pad can't happen again due to policy changes: https://docs.npmjs.com/cli/unpublish#description
- Doesn't require any build steps or extra hoops if you're fine with skipping static types
In general it just does a really great job isolating from the environment. No messing with environment variables, most things even run fine on Windows out of the box. All you need is node itself installed and you're off to the races, whether you're starting a new project or running one you checked out from github.
What about pip freeze?
Having to ship a compiler to a host|container to build python with pyenv, needing all kinds of development headers like libffi for poetry. The hoopla around poetry init (or pipenv's equivalent) to get a deterministic result of the environment and all packages. Or you use requirements files, and don't get deterministic results.
Or you use effectively an entire operating system on top of your OS to use a conda derivative.
And we still haven't executed a lick of python.
Then there's the rigmarole around getting one of these environments to play nice with cron, having to manipulate your PATH so you can manipulate your PATH further to make the call.
It's really gotten me started questioning assumptions on what language "quick wins" should be written in.