Dual OS Single Boot
It’s increasingly common to run into programs that are not portable. One mild version is people who assume Bash is available without naming it explicitly as a dependency. A more irksome version of this is the #!/bin/bash
script. Creeping Linuxisms are everywhere now. A third version is the language-specific build and packaging systems that everyone now uses. These of course pull in numerous dependencies that frequently introduce their own non-portability issues.
There’s little hope in helping the people who create these packages to write portable code. It’s no longer sufficiently fashionable. The current fashion makes some sense if you spend your entire programming career in the context of a single language. In that case it may make sense to have a separate virtual directory for each separate project, and not worry about making a stand-alone executable that is not tied to such a context. It may also make sense to have a different dependency-handling mechanism for each different programming language. After all, you care only about one, right?
For those of us who programmed before all of this developed it looks like a reinvention of many of the problems we battled 25 years ago. But each new generation must fight its own battles, invented or otherwise.
As a FreeBSD user I frequently need to work around these problems. The two bit thorns in my side today are the creeping Linuxisms and the Python venv. Fortunately there is a fairly elegant solution.
The current FreeBSD Linux emulator is solid. While it doesn’t cover everything, I haven’t had problems running user-facing Linux programs with it. For stand-alone programs that’s all you need. You can run the linux binaries from FreeBSD. For programs that require build from source you need to work around the issues of the build system. The Python venv is an example. There is supposed to be a switch for uv pip install
to tell it what OS to assume, but I’ve had no success with it. Fortunately there is chroot -n
.
The -n
option to chroot
disables any setuid behavior by the program it executes. This means no sudo
, for example. Consequently, chroot -n
does not itself require root privilege. An ordinary user can call it.
One can set up a local Linux environment several ways. I use debootstrap
with Debian, in the default base directory /compat/linux
. With my home directory mounted inside /compat/linux
under its full path, I can invoke chroot -n /bin/bash
and have what looks like an ordinary Debian environment.
With that environment in place I can install software as if running Linux. And I can install Python dependencies in a venv under Linux. And to run them I need only invoke chroot -n /compat/linux && cd whatever && execute-the-command
. That’s an easy wrapper to create. I call it linu
:
#!/bin/sh
# Get the current working directory
linux_cwd="$(pwd)"
# Ensure we have at least one argument (the program to run)
if test $# -eq 0; then
echo "$0: usage: $0 prog" >&2
exit 100
fi
# Enter Linux chroot, change to the equivalent directory, and run the command
exec chroot -n /compat/linux /bin/sh -c 'cd "$1" && shift && exec "$@"' "$0" "${linux_cwd}" "$@"
And for the Python venv? That wrapper takes a path followed by a program, sources the path, and executes the program. I call that sour
:
#!/bin/sh
shout() { echo "$0: $*" >&2; }
barf() { shout "fatal: $*"; exit 111; }
usage() { shout "usage: $0 venv-dir prog"; exit 100; }
test $# -gt 1 || usage
#_# _sour file
#_# source file
_sour() {
sh -c '. "$1" 2>/dev/null' "$0" "$1" && . "$1"
}
_sour "$1" || barf "cannot source $1"
shift
"$@"
It’s slightly convoluted in order to capture the sourcing error, but still fairly straightforward.
And that’s it. Now from outside of the Linux environment I can call linu sour path prog
. And of course that can be stuffed into a new wrapper for any program you want to call frequently.
This meshes beautifully with unix filters, of course, proving again the power of the construct.
So, at least in FreeBSD you can easily have a dual OS configuration within a single-boot context. It works!