2023-02-24

Qbs tutorial part 5/5: Apps

The apps

In the last part we looked at how to select files with the group feature in Qbs , and in this last part of the series we will look at how to build our apps with src/apps.qbs.

But first our file tree as a reference:

.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js

The final part of the puzzle is our app.qbs definition which looks like this:

OctoMYApp {
    name: "Agent"
    useCombinedLibs: true
    property string path2:path
}

Again, I hope you put two and two together to realize that OctoMYApp is defined in ./integrations/qbs/OctoMYApp.qbs and represents a reusable component that is included in all our app.qbs files for centralized management for convenience.

If we follow the white rabbit again, ./integrations/qbs/OctoMYApp.qbs looks like this:

import qbs.FileInfo

Application {
    name: "OctoMYApp"
    property string appDir: FileInfo.cleanPath(path2) + FileInfo.pathSeparator()
    property string srcDir: FileInfo.cleanPath(FileInfo.joinPaths(project.sourceDirectory, "src")) + FileInfo.pathSeparator()
    property string libsDir: FileInfo.cleanPath(FileInfo.joinPaths(srcDir, "libs")) + FileInfo.pathSeparator()
    property bool useCombinedLibs: true
    
    // Add dependency on all Qt components that we need
    OctoMYQtDepends {}

    // Add dependency on cpp
    Depends{
        name: "cpp"
    }

    // Enumerate OctoMY lib folders
    OctoMYLibProbe {
        id: octomyLibs
        searchPath: libsDir
    }

    //"QT_DISABLE_DEPRECATED_BEFORE=0x060000" // disables all the APIs deprecated before Qt 6.0.0
    // Tell cpp to look for header files in all the lib folders
    cpp.includePaths: base.concat(octomyLibs.libFolders)

    // Tell cpp all the auto-generated defines for OctoMY libraries
    cpp.defines: base.concat(octomyLibs.libDefines)

    // Tell cpp what version and stdlib we want
    cpp.cxxLanguageVersion: "c++20"
    cpp.cxxStandardLibrary: "libc++"

    install: true
    installDir: qbs.targetOS.contains("qnx") ? FileInfo.joinPaths("/tmp", name, "bin") : base

    OctoMYFiles{
        name: "app_sources"
        prefix: appDir
    }
    // We DON'T use combined libs, include all lib sources directly here
    OctoMYFiles{
        condition: !useCombinedLibs
        name: "lib_sources"
        prefix: libsDir
    }
    // We DO use combined libs, depend on combined lib here
    Depends { 
        condition: useCombinedLibs
        name: "combined"
    }
}

This time we use a bunch of helpers, some of which you already know:
  • OctoMYQtDepends: Our helper to include dependency on all the Qt modules that OctoMY™ needs
  • OctoMYLibProbe: Our helper to enumeratelibrary folders
  • OctoMYFiles: Our helper to list source files in a project that we care about (this is equivalent to tile fil Group in our StaticLibrary above).
Please note the user defined property we made called useCombinedLibs. It will include two different blocks depending on it's value. If it is true, then we have a Depends{ name: "combined"} and if it is not true we include a file group OctoMYFiles that basically lists all the source files of all our libraries in one big list.

This allows us to choose wether we want to build libraries separately and depnd on them or build the sources of the libraries directly into the app.



This concludes the fifth and final part in a series on using Qbs to our benefit  and joy in a non-trivial project, namely OctoMY™

I hope this series was useful, and that it helped you see the many benefits and few shortcommings of Qbs so that you may implement it in your own projects.

Good luck!


Illustrations borrowed from the amazing collection at https://ericjoyner.com/

Qbs tutorial part 4/5: File Groups

 




Selecting files with Groups





In the last part we went into the sub-project file src/libs.qbs in detail, looking at how libs were built. But we left out one important part, namely the Group type in Qbs. This will be the focus of this part! But first our file tree as a reference:

.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js


Groups are maybe the most important of all the types in Qbs. This is Qbs' very convenient way of enumerating a named set of files used for input to a build. If we look at the StaticLibrary in the last part, we found this:

// Enumerate source files for this library
    Group{
        name: FileInfo.fileName(path2)+"_sources"
        prefix: fulldir
        files: [
            "**/*.cpp",
            "**/*.hpp",
            "**/*.c",
            "**/*.h",
            "**/*.ui",
            "**/*.qrc"
        ]
    }

Groups are named using the name property which comes in very handy as you will see the names inside QtCreator.

The files property is where you specify the files. In a simple project you can just list files by fulll filename, but Groups also support an intuitive set of glob style wildcards. From the documentation:

When specifying files, you can use the wildcards "*", "?" and "[]", which have their usual meaning. By default, matching files are only picked up directly from the parent directory, but you can tell Qbs to consider the whole directory tree. It is also possible to exclude certain files from the list. The pattern ** used in a pathname expansion context will match all files and zero or more directories and subdirectories.
The prefix property allows you to avoid repeating a long pathname by specifying it only once.

⚠️DANGER⚠️ Please note this very common and dangerous pitfall: the prefix parameter is string based and NOT file based! This is by design. This means that if you omit the directory separator at the end of your prefix then the result will be completely differnt. For example:

Group{
   name: "example1"
   prefix: "mydir/mysubdir"
  files:[ "a.cpp", "b.cpp", "c.cpp"
}
// Results in mydir/mysubdira.cpp, mydir/mysubdirb.cpp, mydir/mysubdirc.cpp which could be what you wanted, but maybe not

Group{
   name: "example2"
   prefix: "mydir/mysubdir/"
  files:[ "a.cpp", "b.cpp", "c.cpp"]
// Results in mydir/mysubdir/a.cpp, mydir/mysubdir/b.cpp, mydir/mysubdir/c.cpp which could be what you wanted, but maybe not

In our StaticLibrary, you will see two strange things in the Group. One strange thing is that we somehow just grab ALL the files of certain types:

files: [
    "**/*.cpp",
    "**/*.hpp",
    "**/*.c",
    "**/*.h",
    "**/*.ui",
    "**/*.qrc"
]

How is this possible?

This is another very powerful feature: Qbs will determine how to process a file from that file's type through the use of FileTaggers and Rules. You no longer have to manually segregate files by types like you had to in qmake through the SOURCES and HEADERS variables. Instead Qbs will simply identify what is a header and what is a source, a resource, a designer ui file, a qml file and so on. In fact, Qbs supports a quite large and growing set of languages, platforms and toolchains. At first this might not seem like a big deal, but it has many benefits.

For one, projects which mix languages and platforms now suddenly will have "nirvana" in the form of one build tool to build it all.

Furthermore, managing projects becomes more fluid. Simply list the filetypes you care about and let Qbs figure out the rest.

Another benefit is that all the rules for building to the different platforms and toolchians is defined in  Qbs syntax, so you can very well extend it to suit your own needs, for example if you create your own domain spesific languages or encouter archaic or exotic toolchains that you depend on in your build, you can create the necessary Rules, FileTaggers and so on to handle themin your Qbs build.

Final note on file Groups is that we use the globstar (**) wildcard. This means "zero or more path elements". We use this because each of the libraries in OctoMY™ will have an unknown number of possibly nested subfolders, and we want to grab sources from all of them. Very convenient!

Now we know how files are selected in Qbs🎉!

This concludes the fourth part in a series on using Qbs to our benefit  and joy in a non-trivial project, namely OctoMY™

In the next part we will look at app.qbs and how it pulls in the necesary dependencies to build our apps.

Illustrations borrowed from the amazing collection at https://ericjoyner.com/

Qbs tutorial part 3/5: Libraries

Building libraries

In the last part we took a look at test/tests.qbs and how it related to the main project file. In this part we will go into the sub-project file src/libs.qbs in detail. But first our file tree as a reference:

.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js


Libraries work much the same way as tests, so if you look inside src/libs.qbs you will find a similar reference to OctoMyLibsProbe as you did inside libs/libs.qbs. This probe will expectedly enumerate all folders that start with "lib" and that contain a file called lib.qbs under libs/

So now that all our libs and tests have been enumerated, we can look at the anatomy of a library. Let's use libs/libagent/lib.qbs as an example:


OctoMYAutoLib {
    property string path2:path
}


Again, you might have put two together and realized that OctoMYAutoLib refers to integration/qbs/imports/OctoMYAutoLib.qbs which again contains some sort of magic. This time it is not a Probe, however. It looks like this:


import qbs.FileInfo

StaticLibrary {
    Depends { 
        name: "cpp"
    }
    OctoMYQtDepends {}

    property string libdir: FileInfo.relativePath(project.sourceDirectory, path2)+FileInfo.pathSeparator()
    property string fulldir: FileInfo.cleanPath(path2)+FileInfo.pathSeparator()
    
    name:{
        var n=FileInfo.fileName(path2);
        console.info("Autolib    libdir=" + libdir+", n="+n+", path2="+path2+", fulldir="+fulldir);
        return n;
    }
    
    Export {
        Depends {
            name: "cpp"
        }
        OctoMYQtDepends {}
        property string e:{
            console.info("Auto Lib '"+exportingProduct.name+"' exportingProduct.sourceDirectory="+exportingProduct.sourceDirectory)
            return exportingProduct.sourceDirectory + FileInfo.pathSeparator();
        }
        cpp.includePaths: [ e ]
        cpp.defines: [ "USE_LIB_" + exportingProduct.name.toUpperCase() ]
    }
    
    cpp.includePaths: [ fulldir ]
    cpp.cxxLanguageVersion: "c++20"
    cpp.cxxStandardLibrary: "libc++"
    
    // Enumerate source files for this library
    Group{
        name: FileInfo.fileName(path2)+"_sources"
        prefix: fulldir
        files: [
            "**/*.cpp",
            "**/*.hpp",
            "**/*.c",
            "**/*.h",
            "**/*.ui",
            "**/*.qrc"
        ]
    }
}

This time you can see that we are defining an actual artifact called StaticLibrary. And this introduces a whole slew of new concepts. Let's go thorugh them one at the time from the top.

First we see a definition Depends{ name: "cpp"}

This is powerful feature in Qbs where you can depend on certain features by name to bring them into your project like magic. For example the cpp module contains all the properties and rules for building C++ applications. It is quite extensive. Qbs has a bunch of these modules that you can depen on for all sorts of languages and platforms and toolchains.

Just with that simple line we now have C++ toolcahin at our disposal.

Next comes a friend of ours from the ./integrations/qbs/ folder called OctoMYQtDepends. This time it is a reusable depend statement to specify what Qt modules we want to include in every build of OctoMY. It looks like this:


Depends {
    name: "Qt"
    submodules: [
          "bluetooth"
        , "concurrent"
        , "core"
        , "gui"
        , "multimedia"
        , "multimediawidgets"
        , "openglwidgets"
        , "positioning"
        , "printsupport"
        , "sensors"
        , "serialport"
        , "svgwidgets"
        , "widgets"
        , "xml"
    ]
}

As you can see, it uses the optional submodules syntax of Qbs Depends to specify a list of sub-modules. OctoMY™ consumes pretty much all of Qt so the list of modules is long. Here is the list of Qt modules available.

Next we will look at the Export entry. This is how we tell anyone wishing to link against our StaticLibrary how to do that. We export our dependency on C++ though the Depends {name: "cpp"}, our dependence on Qt through OctoMYQtDepends {}. We also specify where consumers of our library can find the include files through cpp.includePaths and we can specify which settings to use for the C++ compiler and linker with cpp.*.

Ideally these should match the ones used when building the library, and in this case they do, as you can clearly see it is duplicated.

The Group defines the collection of files from which this StaticLibrary will be built, and it deserves a whole part for itself, so we will go through this concept in the next part of this series.

So to summarize our StaticLibrary definition, what does the full statement mean?

In short we put the declaration for each library from one template that will recursively build all source files into a static library. This will ensure that maintaining our 40+ librarie's build instructions which should remain identical at all times is easily done in one place. We don't have to scurry around maintaining 40+ project files which would be a hazzle.

We could also just make one big library that found all the source files in each library's folder. So why did we not go this route?

There are some reasons. For one, we might want to have special commands for some of the libraries while keeping the rest identical. This is now easily carried out by reokacing or extending the project file for that single project.

Another reason is that we might gain some build speed by keeping libraries instead of re-building from source every time.

Now our libraries are built, what comes next?

This concludes the third part in a series on using Qbs to our benefit and joy in a non-trivial project, namely OctoMY™

In the next part we will look at the Group type in Qbs and how we can use it to enumerate the source fiels of our builds.

Illustrations borrowed from the amazing collection at https://ericjoyner.com/

Qbs tutorial part 2/5: Tests

Tests


In the last part we looked at the overal structure of the project as well as the main project file OctoMY.qbs and how it referred to sub-projects. In this part we will go into the sub-project file test/tests.qbs in detail. But first our file tree as a reference:

.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js


Looking at test/tests.qbs it looks like this:


import qbs.FileInfo

Project{
    name: "Tests"
    property string srcDir: FileInfo.cleanPath(FileInfo.joinPaths(project.sourceDirectory, "src")) + FileInfo.pathSeparator()
    property string projectDir: FileInfo.cleanPath(project.sourceDirectory) + FileInfo.pathSeparator()
    property string testDir: FileInfo.cleanPath(FileInfo.joinPaths(projectDir, "test")) + FileInfo.pathSeparator()
    property string commonTestDir: testDir + "common_test" + FileInfo.pathSeparator()
    property string commonTestLib: commonTestDir + "lib.qbs"
    
    // Enumerate OctoMY test projects
    OctoMYTestProbe {
        id: octomyTests
        searchPath: testDir
    }
    references: octomyTests.testFiles.concat([commonTestDir])
}


As you can clearly see, this is also a project. Since it is referred to by the top-level project it is in fact a sub-project, and you can see that the name it will display in QtCreator is "Tests".

Further you can see that it has some notable properties:
  • srcDir
  • projectDir
  • testDir
  • commonTestDir
  • commonTestLib
These are set using JavaScript expressions that use the Qbs FileInfo service. Qbs has many services or libraries with functions that help us with all kinds of things ranging from file and string manipulation to getting information about the platform.

Next we see a new item called OctoMYTestProbe. You might have put two together already and realized where this was defined. In our main project file we used a property called qbsSearchPaths to ask Qbs to look for our user defined re-usable components. In that folder there was an item called OctoMYTestProbe.qbs that Qbs now conveniently has loaded and made available to us.

NOTE: components are only made available in files referred to by the main project for technical reasons. This is one of many reasons why it is a good idea to separate your Qbs project into multiple files, for all but the smalest of projects.

Qbs has a concept of Probes. The job of a probe is to look for things or perform expensive calculations. Probes are executed before the build starts and Qbs has some clever caching mechanisms that ensure they are only executed once. If you are doing anything intense, be it search through folders, calculate hashes or whatever else, Probes are your best friend. Probes also promote code re-use.

Our OctoMYTestProbe.qbs probe looks like this:

import qbs.File
import qbs.FileInfo

Probe{
    // Parameters
    property string name: "OctoMYTestProbe"
    property string searchPath: FileInfo.cleanPath(FileInfo.joinPaths(project.sourceDirectory, "test")) + FileInfo.pathSeparator()
    property string testFileName: "test.qbs"
    property string testDefinePrefix: "OC_USE_TEST_"
    property string commonTestDir:  FileInfo.cleanPath(FileInfo.joinPaths(searchPath, "common_test")) + FileInfo.pathSeparator()
    // Outputs
    property stringList testNames: []
    property stringList testFolders: []
    property stringList testFiles: []
    property stringList testDefines: []
    configure: {
        found=false;
        var raw = File.directoryEntries(searchPath, File.Dirs | File.NoDot | File.NoDotDot);
        testNames = (raw || []).filter(function(dir){
            if(testFileName){
                return dir.startsWith("test") && File.exists(FileInfo.joinPaths(searchPath, dir, testFileName));
            }
            else{
                return dir.startsWith("test");
            }
        });
        testFolders = testNames.map(function(testName){
            return FileInfo.joinPaths(searchPath, testName);
        })
        testFiles = testFolders.map(function(testFolder){
            return FileInfo.joinPaths(testFolder, testFileName);
        })
        testDefines = testNames.map(function(testName){
            return testDefinePrefix + testName.toUpperCase();
        })
        found = testNames.length > 0;
        console.info("Probing returned for '" + name+"':");
        console.info(" + found="+found);
        console.info(" + searchPath="+JSON.stringify(searchPath, null, "\t"));
        console.info(" + testNames="+JSON.stringify(testNames, null, "\t"));
        console.info(" + testFolders="+JSON.stringify(testFolders, null, "\t"));
        console.info(" + testFiles="+JSON.stringify(testFiles, null, "\t"));
    }
}

As you can see, the probe uses the File service to look for directories beginning with "test" that contains a file called "test.qbs".

It sets the special property found to true if the number of folders found is more than zero.

It also logs a bunch of stuff to the console.

Qbs Supports printing of debug strings to the console using console.info() which can be very useful while debugging.

The probe will be ran and produce some useful properties for us:
  • testNames - a list of the names of tests found in our project
  • testFolders - the folders for each of our tests
  • testFiles - the test.qbs file for each test
  • testDefines - some defines that refer to our test names
Now when we go back to test/tests.qbs and have a closer look, we will see this:

...
    OctoMYTestProbe {
        id: octomyTests
        searchPath: testDir
    }
    references: octomyTests.testFiles.concat([commonTestDir])
...

As you can tell, we are making an instance of the OctoMYTestProbe called octomyTests, and we pass the calcualted property testFiles, which is a list of qbs files, one for each test, to the references. In other words, we are telling Qbs to read in all these test project qbs files as part of the Test project. In QtCreator you will see that it genereates a full list of tests in the project tree.

So why did we go to all this trouble instead of just naming the tests manually in a list?

Well, now we never have to change this project file again. Whenever we create a new test, it will magically appear thanks to our clever Probe🎉.


This concludes the second part in a series on using Qbs to our benefit and joy in a non-trivial project, namely OctoMY™

In the next part we will look at src/libs.qbs and how it relates to the main project file.

Illustrations borrowed from the amazing collection at https://ericjoyner.com/

Qbs tutorial part 1/5: Main project file



After spending a few gruelling weeks porting my brain to "think in Qbs", I felt ready to take on the non-trivial task of comitting 100% to porting the build system of OctoMY™ from qmake to Qbs. This was in large part possible thanks to the super-human patience and wisdom demonstrated by the super awesome Qbs community, which lives on Discord. Shoutout to ABBAPOH, Psy-Kai and Janet Blackquill, your help was indispensible!

I thought I would document my decisions here for future reference and maybe to serve as a template for others trying to get into Qbs with their non-trivial projects. I will also introduce some concepts and tips & tricks that I picked up along the way.

So first up: top level project structure. OctoMY™ is divided coarsely into 3 parts:

  • Apps - The actual programs that we run when we want to use OctoMY™
  • Libs - The software components which contains the code of OctoMY™, liked into the apps
  • Tests - A bunch of programs, each testing one aspect of the codebase



The file structure of the project looks like this:
.
├── OctoMY.qbs
├── src
│   ├── agent
│   │   ├── AgentMain.cpp
│   │   ├── AgentMain.hpp
│   │   ├── app.qbs
│   │   └── README.md
│   ├── apps.qbs
│   ├── combined
│   │   └── lib.qbs
│   ├── libs
│   │   ├── libagent
│   │   │   ├── agent
│   │   │   │   ├── Agent.cpp
│   │   │   │   ├── Agent.hpp
│   │   │   │   └── AgentWindow.ui
│   │   │   ├── lib.qbs
│   │   │   └── README.md
│   ├── libs.qbs
├── test
│   ├── common_test
│   │   ├── Common_test.hpp
│   │   ├── CommsTester.cpp
│   │   ├── CommsTester.hpp
│   │   ├── mock
│   │   │   ├── MockCourier.cpp
│   │   │   └── MockCourier.hpp
│   │   ├── resources
│   │   │   ├── icons
│   │   │   │   ├── profile.svg
│   │   │   │   ├── stress.svg
│   │   │   │   └── test.svg
│   │   │   └── test_resources.qrc
│   │   ├── Utility_test.cpp
│   │   └── Utility_test.hpp
│   ├── testLogHandler
│   │   ├── TestLogHandler.cpp
│   │   ├── TestLogHandler.hpp
│   │   └── test.qbs
│   ├── testTryToggle
│   │   ├── TestTryToggle.cpp
│   │   ├── TestTryToggle.hpp
│   │   └── test.qbs
│   └── tests.qbs
└── integration
   └── qbs
     └── imports
            ├── OctoMYApp.qbs
            ├── OctoMYLibProbe.qbs
            ├── OctoMYAutoLib.qbs
            ├── OctoMYTestProbe.qbs
            ├── OctoMYAutoTest.qbs
            ├── OctoMYFiles.qbs
            ├── OctoMYQtDepends.qbs
            └── utils.js

So there is a lot to cover here, and I promise that we will go over one part at the time so that it will all make sense in the end!

First, qbs consist of strategically placed text files with the .qbs file extension. Inside these qbs files is a declarative syntax borrowed from QML. Expect to see a bunch of json-like blocks of data with some javascript sprinkled on top.

The top-level file of the project is OctoMY.qbs This marks the entrypoint for qbs, and it is this file which qbs will read first, and this is the file you open in QtCreator to load the project.

Qbs project in QtCreator



As you can see, I have used color coding thoruhgt this article so that you may easily see the link between views. For example, the main project file has been marked red, and the sub-project files that it references yellow in this entire article, so you can readily find them in the file structure above.


OctoMY.qbs looks like this:


import qbs.FileInfo

Project {
    name: "OctoMY™"
    qbsSearchPaths: [ FileInfo.joinPaths(path, "integration", "qbs") + FileInfo.pathSeparator()]
    references: [
           "src/libs.qbs"
          , "src/apps.qbs"
          , "test/tests.qbs"
    ]
}


As you can see, it has some important lines:

  • import qbs.FileInfo: This is how we get access to other types and script libraries in qbs. In this case, we need the FileInfo service provided with qbs that allows us to do file operations.
  • Project: This indicates that we are defining a new qbs project. "Project" is a qbs type that represents a project. It is used to collect other items inside of it similar to a folder in a filesystem. It can contain other items directly inside of it, or it can refer to files which in turn will contain such items. In our case we only refer to other files through the references property (see below).
  • name: This is what shows up inside QtCreator as the name of your project. If you leave it out, Qbs will use the file as a fallback. In other words, this property is optional.
  • qbsSearchPaths: This points to where qbs will look for our user defined reusable components, of which there are several in the OctoMY™ project. You don't have to use this feature, but it will serve you well to use it once your Qbs project grows beyond the trivial "Hello World" examples. In this case we tell qbs that this project will look for our components in ./integrations/qbs folder, and you can see (in theproject tree at the top of this article) that it contains some components that we will use from sub projects, such as OctoMYTestProbe.qbs.
  • references: References is a list of files that Qbs should read and include in the project. Since we are currently defining a project, we want to include the actual sources and libraries and tests of the project, and that is done by referring to the qbs files which we use to find them. In this case we include 3 sub-projects via the libs.qbs, apps.qbs and test.qbs files.
As you can see, I have color coded some files such as the main project file in red, and the sub-project files that it references in yellow in this entire article series, and so you can readily find them in the file structure.

This concludes the first part in a series on using Qbs to our benefit and joy in a non-trivial project, namely OctoMY™

In the next part we will look at test/tests.qbs and how it relates to the main project file.


Illustrations borrowed from the amazing collection at https://ericjoyner.com/

OctoMY™ will migrate from qmake to Qbs

OctoMY™ has from this date forward decided to standardize on Qbs and migrate from qmake to Qbs. qmake project files will remain for some time until we see that the Qbs project files replace them perfectly before they will be removed. Thanks for all the fish qmake!


This might seem like a strange decision since Qt itself has selected CMake in favor of their own Qbs project, however the benefits Qbs will have to OctoMY™ is substancial compared to what CMake will bring. So here is the list of reasons why OctoMY™ will chose Qbs over CMake:

  • CMake is arguably hard to work with, inelegant, cargo-culty, full of legacy, slow, inflexible, archaic and challenging to like
  • The main selling point of CMake is that "everyone is using it" and so you will find that a lot of projects support CMake and consuming these projects as dependencies with CMake will be simpler. This is however not relevant for OctoMY™ because we have a strict policy of not depending on any external libraries except Qt framework itself. All the sources are in the tree.
  • CMake will allow us to export our project as a dependency to others. Again, this is a mute point as OctoMY™ will not be consumed as a source level dependency.
  • Qbs is well supported by Qt (they developed it after all)
  • Qbs is well integrated into QtCreator which is very important for OctoMY™ since we depend on that as our main development tool
  • Qbs is modern
  • Qbs is flexible
  • Qbs is fast
  • Qbs is multi-platform and has good support for all the platforms that OctoMY™ do and will need support for in the future
  • Qbs is open-source
  • Qbs is well written (if you peek inside the sources of Qbs, you will probably like what you see)
  • Qbs acts as a server that talks to QtCreator over a pipe. This means that QtCreator will better reflect the internal state of Qbs at any given time. For example, it would automatically update the project tree view in QtCreator whenever I saved changes to the project (*.qbs) files, and Qbs would act as expected in many cases where qmake just would not. This gave a surprisingly fluid experience that is addictive.
  • Qbs really maxes out your CPU at build time and spends almost zero time on house-keeping. The system is really fast at dependency checking and re-building only the necessary files making your developer experience quite pleasant.
  • It has not yet screwed up dependencies even once even in my very demanding project and with me as a demanding user which is quite impressive. qmake would require a full re-build on a regular basis.
  • While Qbs works out of the box with Qt, it is not limited to Qt. It has extensions to support a whole load of platforms, languages and frameworks and can integrate with a bunch of developer tools including IDEs, other build tools and more. Especially, Qbs aims to tackle the not-for-the-faint-of-heart builds that target embedded hardware, mobile platforms and other quirky and archaic targets full of demanding requirements such as cross compilation and advanced tool-chain management. Impressive! This means we have some wiggle-room for futer expansion of the OctoMY™ project
  • There is a Qbs Discord Server where devs and users of Qbs hang out. This has been one of the best discoveries so far. The amount of patience and wisdom the core team have showed me as a annoying beginner is truely amazing.
  • Qbs will hopefully grow and get a much deserved boost in adoption!

Qbs is not perfect, and here are a few of the things that I did NOT like so far:
  • The Qbs output is very sparse and debugging Qbs is very difficult for a beginner in the start before you start udnerstanding how it all works under the hood. The log output windows in QtCreator will all just be completely empty even if there definitely are some problems. There are options to add more verbose output, but it is well hidden and off by default. Also the "max verbose" option is not possible to swithc on  at all.
  • "printf debugging" is hard since Qbs is declarative. It is definitely possible but very quirky.
  • Some of the features in Qbs have severe pitfalls that you are expected to pick up on by yourself. Here are some examples:
    • Group prefix is a string, and NOT a file entity. In other words, "someFolder" and "someFolder/" means different things.
  • While the documentation is "complete" it is kind of sparse still, and leaves you wondering about implementation details in many cases. For example, does Group.excludeFiles support wildcards like Group.files or not? (The answer is yes, it does, but the documentation does not mention this).
All these downside however, are easy to fix, while arguably the downsides of CMake are not at all easy to fix.


2023-02-07

Old dog learns new tricks: Qbs edition

So I had a go at QML a few years ago and it seems like a cool technology and definitely has a lot of potential for creating good user interfaces.

Getting started with QML is easy enough thanks to the examples directly present in QtCreator. You can get up and running quickly and adapt an example to what you want to achieve.

After a while you will stumble upon a bunch of more or less fundamental questions like:

  • How can I interface QML with my existing C++ code?
  • How can I get the filename and path of the current module from code?
  • What is the meaning of life?
Most of these questions are sort of answered in the examples or in the documentation.

So I thought I had enough QML under my belt to tackle Qbs, the misunderstood, does everything right-yet-nobody-uses-it build system invented by Qt.

Qbs lives on it's own domain, has minimalist examples and documentation and seems to be completely neglected by the whole world. But it is using QML under the hood so that is a start.

Or so I thought.

It could very well be me whom is the problem here. After all I am an old fart that has been too long in the industry. I used to joke that "I learned to program before the internet" which is technically true.

But for some reason getting into Qbs has left me feeling extra old. It feels as if there is a great secret that everyone knows except me. But I won't shy away from a good challenge and so I have decided to conquer Qbs before I make my judgement. My hope is that all the promise it holds will be unleashed upon my projects once I unlock it's secrets.

The journey begins!


2023-02-02

Brief OctoMY™ update 2023

These are the changes scheduled for early to mid 2023 in the OctoMY™ project:

  • Good bye Qt5 hello Qt6
  • Refactoring of subsystems relating to media such as speech, video streaming etc
  • Once-over of whole project
  • New build system

Good bye Qt5 hello Qt6

Perhaps a little overdue, we finally support Qt6. Since we didn't really have a usable build in Qt5, we have just put that behind us and we are going all inn on Q6.

Media subsystem

This derives partly from the Qt6 transition as Qt6 introduced many changes in the underlying media related APIs and partly from the initial implementations in OctoMY™ showing it's age as many were the first parts of the codebase to be implemented and they have suffered from systemic neglect ever since.

Once-over

With a Qt version update and lots of refactoring there is always a risk of introducing lots of healthy little bugs, so this step means simply looking it over and adding tests where they are needed, running all the parts of the oprogram to see if any bugs show up.

New build system

With Qt6 came the astonishingly pragmatic (near-sighted?) decision by Qt project to move Qt away from qmake and on to CMake. The first part I completely understand, while I have learned to love qmake it still is a flawed little program and not really feasible for a large project such as Qt.

The second part of the decision; to transition to CMake feels like substituting pest for cholera. Time will tell if this was a great idea, especialkly since they had an alternative at their hands that shows a lot of rpomise as a proper next generation alternative, namely Qbs.

OctoMY™ project has some build needs that are difficult to express in either build system (being able to not maintain lists of files that are part of the project up front, and being able to dynamically chose wether a library should be linked separately or not) so we will look at adopting Qbs. So far it seems it will suit OctoMY™ quite nicely.