Qbs is a modern build tool for software projects which is most notably used for Qt-based projects. It is also directly supported by Qt Creator.

Documentation is here: http://doc.qt.io/qbs/

It is not currently well-supported in debian. Qbs is packaged, but tools like debhelper know nothing about it, so packaging packages that build using Qbs requires some work.

Qbs defaults to 'build and install' in a build directory. The build and install steps can be separated, to fit better to Debian build and binary rules, (with qbs build --no-install modules.qbs.installRoot:$(CURDIR)/debian/tmp/, followed by qbs install --no-build --install-root $(CURDIR)/debian/tmp/).


Qbs has a concept of a 'profile' in which toolchain/dependency info is stored, possibly along with other settings. And builds can be done for each desired profile. Builds and installs are done in a specified directory. How it is specified changed twice over versions 1.8-1.11. For Qbs 1.8 and earlier the build and install were done in a dir named after the profile (so 'default' for profile:default). From Qbs 1.9 there is a 'configuration name' which is used as the build and install dir. given as a bare name for 1.9 and 1.10, and as config:name from 1.11. i.e. qbs build qbs-build (<=v1.10) or qbs build config:qbs-build (v1.11 onwards).


Qbs requires a 'toolchain setup' phase where needed build tools and dependencies are found, and profiles setup to describe them. The design assumes that you will do this once and the toochain info is stored under $HOME/.config/QtProject/qbs by default. This is incompatible with debian buildds which do not have $HOME available. It is possible to specify a different location for the profiles using the --settings-dir option. Having changed the default you have to use this option for _every_ Qbs command otherwise they will be ignoring each other. There does not seem to be an environment variable that we can use to set this property, which is a pity as that would be neater. Perhaps we should ask for one. Setting this to /tmp is not safe (paths could be changed by 3rd party), so set it to $(CURDIR)/debian (which will create a 'qbs' dir in there, which you can clean on each rebuild).

It is possible to treat the Qbs toolchain setup/config commands as a 'configure' stage and do it for every build. This works well where $HOME is not present, but take care if using Qbs in your normal environment, where it will use your default $HOME/.config/QtProject/qbs profiles if you don't tell it not to.

For C/C++ packages there is a qbs-setup-toolchains --detect command which finds debian binutils/gcc/clang tools correctly, and creates profiles if it finds them. Before v1.13 it just made 'gcc' or 'clang' and if both were present you had to explicitly select one. From v1.17 (maybe earlier) it makes profiles like 'aarch64-linux-gnu-gcc-10' and the build no longer needs to specify one explicitly. As a result a build no longer needs to run qbs-setup-toolchains --detect to have a working build profile.

Qbs is often used for Qt projects in which case the path to qmake must be specified, and this will determine which version of Qt is in use. Because debian allows the installation of both Qt4 and Qt5 at the same time this is not sufficient and the QT_SELECT=5 environment variable must be used to set the desired Qt variant to use for the build.


Note that in commands you have to distinguish between modules, projects and products (because the same names might exist in any). In the profile namespace this is not necessary because only module properties are set. This means that (slightly confusingly) you refer to the same thing differently when setting it in a profile with config, than when passing it on a command line. So these two commands are referring to the same property:

  qbs config profiles.default.cpp.linkerFlags "-z,relro"                                    
  qbs build modules.cpp.linkerFlags:"-z,relro"

(This confused me for some time!)

Multiarch paths

Qbs has no built-in 'libdir' concept. It just has installDir which is then set to suitable values for each type of file. File types are set with a 'tags' concept. This is all quite sensible, but it means you can't just pass in --libDir from the rules file (passing in qbs.installDir would cause all files to be installed into the library dir).

However, it's straightforward to define a library path variable, and pass in a multiarch setting for it:

Project {
  name: package
  property string libDir: "lib"

A multiarch setting for it can then be passed in in the rules file (along with a /usr prefix):   qbs install --settings-dir $(CURDIR)/debian --install-root $(CURDIR)/debian/tmp/ profile:default modules.qbs.installPrefix usr/ project.libDir:lib/$(DEB_HOST_MULTIARCH)

And can be used as required, e.g in the ?DynamicLibrary section:

  Group {
    fileTagsFilter: [ "dynamiclibrary", "dynamiclibrary_symlink" ]
    qbs.installDir: libDir
    qbs.install: true

Note that libDir does not include the '/usr' part, which is set as qbs.installPrefix.

Also note that you cannot set project.libDir in a profile as it will always be overwritten by the project property string LibDir: "lib" line. You can set qbs.installPrefix in the profile (so long as it is not set again in the project file: qbs config --settings-dir $(CURDIR)/debian profiles.default.qbs.installPrefix usr/


Making Qbs take note of dpkg-buildflags or DEB_BUILD_OPTIONS is fiddly. Qbs will not just take the $(CFLAGS), $(LDFLAGS) etc variables 'as is'. Some options translate to Qbs properties (like -g (cpp.debugInformation) and -O2 (cpp.optimization:fast)). Others map to cpp.*Flags properties, but syntax changes are required, because it's not shell or make, it's javascript.

The precedence for module properties works like this: In decreasing order:

Regarding list semantics: Command-line overrides wipe out everything else, but in all other contexts lists are merged. In particular, profile contents simply replace the default values from the module prototype and thus get merged with what's in the project files.

So these dpkg-buildoptions:

CFLAGS=-g -O2 -fdebug-prefix-map=/path/to/package/build/dir=. -fstack-protector-strong -Wformat -Werror=format-security
CXXFLAGS=-g -O2 -fdebug-prefix-map=/path/to/package/build/dir=. -fstack-protector-strong -Wformat -Werror=format-security

map as follows:

qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.debugInformation true
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.optimization fast
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.commonCompilerFlags -Wdate-time
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.defines '"FORTIFY_SOURCE=2"'
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.cFlags '[ "-fdebug-prefix-map=$(CURDIR)=.", "-fstack-protector-strong", "-Wformat", "-Werror=format-security" ]'
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.cxxFlags '[ "-fdebug-prefix-map=$(CURDIR)=.", "-fstack-protector-strong", "-Wformat", "-Werror=format-security" ]'
qbs config --settings-dir $(CURDIR)/debian profiles.debian.cpp.linkerFlags "-z,relro"

As you can see the -Wl escape must be removed from cpp.Linkerflags, and values with an '=' in need to be double-quoted to avoid the shell messing with them. Things with multiple flags need them to be entered as lists.

There is no module to support fortran or gcj settings yet (so FCFLAGS, FFLAGS, GCJFLAGS are not supported), but one could be made.

Verbose logs

use --command-echo-mode command-line on the Qbs build line to get full commands shown (without too much noise). This lets the QA machinery check for things like hardening build options in build logs.

Clean rule

qbs clean doesn't work very well for our purposes. Particularly because it always leaves/adds a file (!). It always creates a binary file <build-dir>/<build-dir>.bg if it doesn't exist, so then you have to clean that up yourself by hand otherwise dpkg-source will complain about the unexpected file. In the examples below the build-dir is set to 'qbs-build'. It could also be the name of the profile (on older Qbs versions) or 'default'. qbs may add a clean --wipe or similar 'distclean'-style option at some point. For the time being, simply using rm -r qbs-build (or whatever your build-dir is called) in the clean target is the best approach.


Test programs would normally be built as a separate Qbs 'product'. In which case they can be run with qbs run controlled by DEB_BUILD_OPTIONS in the usual way:

ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
        qbs run --settings-dir $(CURDIR)/debian --no-build -p test-product-name \
           profile:deb config:qbs-build

Note that if your test needs to find a library the binaries need an rpath set to point to the temporary install location Qbs uses. So

cpp.rpaths: ["qbs-build/install-root/usr/lib/x86_64-linux-gnu/"]

is needed in the .qbs file. (and cpp.useRPath:true (which is the Qbs default))

Better is to use installprefix and libDir (see multiarch above) to get the right per-arch path.

cpp.rpaths: ["qbs-build/install-root/" + qbs.installPrefix + project.libDir]

and probably best is to make the path relative to the test binary using $ORIGIN:

cpp.rpaths: ["$ORIGIN/../install-root/" + qbs.installPrefix + project.libDir]

(Note that you don't want $ORIGIN to be expanded by shell or make - you want the actual '$ORIGIN' string in the runpath/rpath)

It is possible that Qbs can be made to get this right automagically, but have not yet found a way.

Example debhelper rules

Here is a simplified dh rules file (for a package which installs a library) using Qbs:

        QT_SELECT=5 qbs-setup-qt --settings-dir $(CURDIR)/debian /usr/bin/qmake deb
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.qbs.installPrefix usr/
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.debugInformation true
ifneq (,$(filter noopt,$(DEB_BUILD_OPTIONS)))
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.optimization none
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.optimization fast
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.commonCompilerFlags -Wdate-time
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.defines '"FORTIFY_SOURCE=2"'
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.cFlags '[ "-fdebug-prefix-map=$(CURDIR)=.", "-fstack-protector-strong", "-Wformat", "-Werror=format-security" ]'
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.cxxFlags '[ "-fdebug-prefix-map=$(CURDIR)=.", "-fstack-protector-strong", "-Wformat", "-Werror=format-security" ]'
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.linkerFlags "-z,relro"

        qbs build --settings-dir $(CURDIR)/debian -v --no-install \
          modules.qbs.installRoot:$(CURDIR)/debian/tmp \
           project.libDir:lib/$(DEB_HOST_MULTIARCH) \
           profile:deb config:qbs-build
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
        qbs run --settings-dir $(CURDIR)/debian --no-build -p dewalls-test \
           --install-root $(CURDIR)/qbs-build/install-root \
           project.libDir:lib/$(DEB_HOST_MULTIARCH) \
           profile:deb config:qbs-build

        qbs install --settings-dir $(CURDIR)/debian --no-build \
           --install-root $(CURDIR)/debian/tmp \
           project.libDir:lib/$(DEB_HOST_MULTIARCH) \
           profile:deb config:qbs-build

        # tidy up qbs profile builddirs
        - rm -r qbs-build
        - rm -r $(CURDIR)/debian/qbs