At the core of xdg-app is a small helper binary that uses Linux features like namespaces to set up sandbox for the application. The main difference between this helper and a full-blown container system is that it runs entirely as the user. It does not require root privileges, and can never allow you to get access to things you would not otherwise have.
This is obviously very useful for desktop application sandboxing, but has all sort of other uses. For instance, you can sandbox your builds to avoid them being polluted from the host, or you can run a nonprivileged service with even less privileges.
Bubblewrap is a wrapper tool, similar to sudo or chroot. You pass it an executable and its argument on the command line. However, the executable is run in a custom namespace, which starts out completely empty, with just a tmpfs mounted as the root, and nothing else. You can then use commandline arguments to build up the sandbox.
For example, a very simple use of bubblewrap to run a binary with access to everything, but readonly:
$ bwrap --ro-bind / / touch ~/foo touch: cannot touch ‘/home/alex/foo’: Read-only file system
Or you can use bubblewrap as an regular chroot that doesn’t require you to be root to use it:
$ bwrap --bind /some/chroot / /bin/sh
Here is a more complicated example with a read-only host /usr (and no other host files), a private pid namespace, and no network access:
$ bwrap --ro-bind /usr /usr \ --dir /tmp \ --proc /proc \ --dev /dev \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --symlink usr/bin /bin \ --chdir / \ --unshare-pid \ --unshare-net \ /bin/sh
See the bubblewrap manpage for more details.
Today I changed xdg-app to use bubblewrap instead of its own helper, which means if you start an app with xdg-app it is using bubblewrap underneath.
For now it is using its own copy of the code (using git submodules), but as bubblewrap starts to get deployed more widely we can start using the system installed version of it.