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/).

Profiles

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).

Setup

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.

Namespaces

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/

buildflags/DEB_BUILD_OPTIONS

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
CPPFLAGS=-Wdate-time -D_FORTIFY_SOURCE=2
CXXFLAGS=-g -O2 -fdebug-prefix-map=/path/to/package/build/dir=. -fstack-protector-strong -Wformat -Werror=format-security
LDFLAGS=-Wl,-z,relro

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.

Tests

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
endif

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:

override_dh_auto_configure:
        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
else
        qbs config --settings-dir $(CURDIR)/debian profiles.deb.cpp.optimization fast
endif
        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"

override_dh_auto_build:
        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
endif

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

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