The goal of this repository is to provide a script infra that will take care of generating root filesystem tarballs for arbitrary distros, suitable for distribution or usage.
It requires root to run, as certain tasks cannot be done without it (such as making sure the permissions are correct). But in order to not use root more than necessary, the scripts will switch to an unprivileged user where possible.
A rootfs generation process consists of the following stages:
- Download - run as user, optional; downloads data necessary to build the rootfs, such as an upstream rootfs tarball
- Initial bootstrap - run as root, either bootstraps the system from scratch or extracts some data downloaded in the prior step
- Secondary bootstrap - run as root, configures the system
bootstrapped in stage 2; may make use of user-mode
qemu
when not run on the target architecture - Configuration - run as root, does any kind of post-installation on the generated rootfs, such as creating users and setting up networks
- Shell - run as root and only executed upon request, you can also gain a shell environment inside the configured rootfs for manual tinkering
- Packaging - run as root, takes care of generating the rootfs archive; the reason it has to run as root is to perform cleanup in the rootfs as well as be able to package files that are only root-readable into the final archive
- Cleanup - run as root, removes the unpackaged rootfs and any downloaded files; this must be run as root because the rootfs files are owned by root and cannot be otherwise directly manipulated
The stages need to be run in that order. The scripts take care of making sure the dependencies are satisfied. If any stage fails, you can continue where it left off after figuring out the problem by running the script again. You can also explicitly request a stage if you wish to run the process manually.
You generally need to specify 3 parameters, the distro being one and the user
and group being the other. Any other parameters are optional and can be found
by passing -h
.
./mkrootfs.sh -d void-musl -u youruser -g yourgroup
The script needs to be run as root. If you don't run it as root, it will not do anything. The script does its own checks.
You can invoke each stage on its own by passing -s
. This is useful if some
stage fails and you've fixed the problem. To for example do the packaging
stage. simply run
./mkrootfs.sh -d mydistro -u youruser -g yourgroup -s package
The script automatically makes sure the dependencies are met, so it will not let you run an incorrect stage.
You can also run the entire process with a custom shell run just before
packaging by passing -S
. This is useful if you want to do some specific
customization in your rootfs before packaging it. Don't forget to clean up
things the scripts wouldn't.
When writing distribution support for the rootfs generator, two things are needed. One is a set of scripts for the distro; the other is its template file.
The distros
directory contains a sub-directory for each distro script set.
One script set may be shared between multiple distro templates; for exmaple,
Void Linux has glibc
and musl
variants and each needs its own template
but they can share the scripts.
The scripts set can contain the following scripts:
01-download.sh
02-bootstrap1.sh
03-bootstrap2.sh
04-configure.sh
05-shell.sh
06-package.sh
07-cleanup.sh
Only bootstrap1
, bootstrap2
and configure
are mandatory; these need to
be written separately for each distribution. The others can be supplied from
the default
directory. This directory provides default scripts for when
they're not written; the mandatory ones have defaults that exit with failure,
the optional ones have reasonable default behavior, with the exception of the
download
stage, which simply does nothing by default.
Each distribution needs its template file, in distros
, named my-distro.sh
.
That is then invoked when generating for my-distro
. It's an ordinary shell
script; it shall export environment variables that the distro scripts need or
that the generator needs.
The following variables are mandatory:
MKROOTFS_SCRIPT_DIR
- the directory with the distro scripts, e.g.void
The following variables are optional:
MKROOTFS_ROOT_PASSWORD
- the root password for the generated rootfs, by defaultpixelc
MKROOTFS_ROOT_DIR
- the directory ingenerated/my-distro
where the unpacked rootfs resides before packaging, by defaultrootfs
, you should not have any reason to change thisMKROOTFS_ROOT_GID
- the group name or group ID (the latter is portable as it does not depend on what is available in the running system) of the rootfs files; by default this is0
. This will be the correct value for most distros but you can change it if necessary. The root directory made bymake_rootfs
will be owned by this group, so any files inside should inherit that correctly, but if they do not, it's up to the distro scripts to make sure they do.MKROOTFS_ENV_BIN
- the path to theenv
binary in the resulting rootfs, needed for environment setting and command invocation; by default this is/usr/bin/env
which will match vast majority of distros but may not always be correctMKROOTFS_ENV_PATH
- the value of thePATH
environment variable in the rootfs,/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
by defaultMKROOTFS_ENV_TERM
- the value of theTERM
environment variable in the rootfs, by default your own environment's value ofTERM
MKROOTFS_ENV_HOME
- the value of theHOME
environment variable in the rootfs, by default/root
MKROOTFS_ENV_SHELL
- the value of theSHELL
environment variable in the rootfs and the shell that will get invoked,/bin/sh
by default, must be a Bourne shell
It may additionally contain any distro-dependent environment variables.
There is the utils.sh
script meant to be included in distro scripts like
this:
. ./utils.sh
It will provide functions to simplify writing of distro scripts. The following functions are provided.
Runs a command, but as an unprivileged user rather than as root.
Executes a function called mkrootfs_[name]_hook
but only if it exists. No
parameters are passed to this function.
Switches into the generated/my-distro
directory. Should be called after
including utils.sh
.
Similar to uname -m
, but always returns amd64
for x86_64
platforms.
Logs into standard output in format NN-stage: ...
, with NN
being the
stage number and stage
being the stage name, like 01-download
. The
arguments are concatenated and separated with space.
Logs into standard output in format --> ...
, similarly to above.
Logs into standard output in format `ERROR: ..., exitting...
Calls error_log
with message
and exits with either 1
or error_code
.
Like running a command, but all output is silenced.
Fetches a file from url
into output
as an unprivileged user.
Like above, but as root.
Given a function name, this function will be called upon error. Meant to clean up any resources not meant to persist into the next stage.
Same as above, but to be called on success.
If run on a non-aarch64
architecture, this will register the qemu
user mode
interpreter so that chroot
can happen. On success, it will also make sure
that unregister_binfmt
is called on cleanup.
If a handler was registered, this will unregister it. Typically no need to call this manually as it will be called upon cleanup.
This should be called before entering chroot
for the first time, but never
after. It will copy the qemu
interpreter into the rootfs
directory so
that the binfmt
handler has something to execute. Does nothing when run
on aarch64
.
Should be called as a part of cleanups before packaging the rootfs
archive.
It will remove the qemu
interpreter from the directory.
Mounts pseudo-filesystems /proc
, /dev
, /sys
into the rootfs
. Also
registers cleanup handler to un-mount them upon exit/error. This is usually
necessary for full function of the rootfs
.
Unmounts the pseudo-filesystems when mounted by above. Automatically called
upon cleanup when the above is called. It is also good to call as a part of
the packaging process, just to make sure, in case a shell
stage didn't
automatically unmount it.
Makes sure the rootfs contains a resolv.conf
for network access inside the
chroot. Automatically cleans up using unprepare_net
at the end, so you do
not need to call it manually.
Cleans up resolv.conf
left by prepare_net
; typically called automatically
but it's good to make sure in packaging stage anyway.
Use this to test whether a rootfs
directory exists. It will error if it
does not.
Use this to create the rootfs
directory and error if it already exists,
if it cannot be created or if the permissions cannot be set. It also defines
a cleanup handler using add_cleanup
that will remove the rootfs
if the
stage fails. The resulting root directory will be owned by MKROOTFS_ROOT_GID
and that alone should be enough for most distro scripts to result in proper
permissions all across the rootfs.
This will run command
in the rootfs, passing it any additional arguments.
The command
will be run with only the HOME
, TERM
, PATH
and SHELL
environment varibles set, the values of them depend on the template file.
Every distro script should begin with the following lines:
#!/bin/sh
. ./utils.sh
switch_dir
This will include the utility library and switch to the directory in which everything is being generated, which is important for proper function of all of the utility library.
You can also extend default scripts easily by using hooks. That means you can create a scripts that will define a hook function and include the default script; the default script will execute the hook at the specified point. Example:
#!/bin/sh
necessary_hook() {
# do stuff
}
. ./default/NN-whatever.sh
In this script you will want to fetch everything that is necessary for the bootstrap process; it doesn't mean it should fetch the root filesystem itself, more like any tools needed for it etc. for example a static binary of the package manager you'll be bootstrapping with. It can also do nothing, as is default.
No hook functionality is provided, because it does nothing.
Here you will want to use whatever from the previous stage (or from scratch)
to fetch an initial version of the root filesystem that may not be configured,
but will be at least chroot
able. It will look roughly like this:
#!/bin/sh
. ./utils.sh
switch_dir
# make the rootfs directory
make_rootfs
##############################
# fetch rootfs contents here #
##############################
################################
# do initial preparations here #
################################
##########################################################
# maybe some pre-configuration necessary to enter chroot #
##########################################################
# success, so do not remove root
remove_cleanup cleanup_root
No hook functionality is provided, because it must be specified per-distro.
This will prepare_binfmt
, register_binfmt
, mount_pseudo
and then
use in_rootfs
to do initial configuration on the root filesystem. This
will result in the root filesystem being technically ready and functional
as if it was generated by the upstream distro, but without any adjustments
necessary for the Pixel C and maybe missing some custom packages etc. Example:
#!/bin/sh
. ./utils.sh
switch_dir
# preparations
test_rootfs
prepare_binfmt
register_binfmt
mount_pseudo
################################
# perform stuff in chroot here #
################################
# e.g. in_rootfs my-cool-package-manager reconfigure -a -f
This part tends to be rather simple.
No hook functionality is provided, because it must be specified per-distro.
This will do stuff like setting root password, pre-configuring hardware such as Bluetooth, enabling services to make sure a graphical environment starts and so on.
#!/bin/sh
. ./utils.sh
switch_dir
# preparations, note how there is no prepare_binfmt
test_rootfs
register_binfmt
mount_pseudo
prepare_net
################################
# perform stuff in chroot here #
################################
No hook functionality is provided, because it must be specified per-distro.
You shouldn't need to override this, as there is a functional default version
already provided out of box in the default
directory. IF you happen to need
to change something, copy it into your distro and modify.
You can do that using a hook. The mkrootfs_shell_hook
function is invoked
after the chroot
exits, if it exists.
There is a default version in default
. Chances are you will want to actually
override this to do e.g. custom cleanups.
The default version does roughly this:
test_rootfs
,umount_pseudo
,unprepare_binfmt
,unprepare_net
- flush
/var/cache
,/var/log
,/var/tmp
and/tmp
- compress into
my-distro-YYYYMMDD.tar.xz
, preserving permissions - change ownership of the resulting archive to the unprivileged user/group
If you can think of any additional cleanups, go ahead and change it. You can
use a hook for that; the mkrootfs_package_hook
is invoked after doing the
default cleanups and before making the archive.
This is also provided by default. It removes all of genrated/my-distro
by
default. It also executes mkrootfs_cleanup_hook
after the removal.