Project output
In order to let our cm-ui project know where to find the implementation of cm-lib, we used the LIBS variable in our project file. It was a pretty ugly folder name, but it’s only one line and everything has worked perfectly well, so it could be tempting to leave things as they are. However, look forward to when we are ready to produce our first build for testing or even production. We’ve written some really clever code, and everything is building and running beautifully. We switch the configuration from Debug to Release and...everything falls over. The problem is that we’ve hard-coded the library path in our project file to look in the Debug folder. Change to a different kit or another operating system and the problem is even worse, as you will have binary compatibility issues from using different compilers.
Let’s set a few goals:
- Get rid of the unwieldy build-cm… folders
- Aggregate all the compiled binary output into one common folder cm/binaries
- Hide all temporary build artifacts in their own folders cm/<project>/build
- Create separate build and binary folders for different compilers and architectures
- Detect those compilers and architectures automatically
So, where do these funny long folder names come from in the first place? In Qt Creator, click on the Projects mode icon in the navigation bar. Down the left-hand side in the Build & Run section, select Desktop Qt 5.9.0 MinGW 32 bit > Build. Here, you will see the Build Settings for the MinGW kit in this solution and under the Shadow build checkbox, you will recognize the long build directory.
We need to leave shadow builds enabled as this gives us the capability to perform builds to alternative locations for different kits. We will control the exact output of our builds in the .pro files, but we still need to specify a build directory here to keep Qt Creator happy. Enter <Qt Projects>/shadow-builds. Repeat this setting for each build configuration (Debug/Release/Profile) using the dropdown at the top of the pane, and for all the kits you are using:
In your filesystem, delete any of the old build-cm… folders. Right-click on the solution folder and Run qmake. After qmake has finished, you should see that shell cm-lib, cm-tests, and cm-ui folders have been created in <Qt Projects>/shadow-builds and that the long build-cm… folders have not reappeared.
The first step for dynamically setting any relative path is to know which path you are currently on. We’ve already seen that in action in qmake when we used $$PWD to get the project working directory. To help us visualize what is going on, let’s introduce our first qmake function—message().
Add the following line to cm.pro—it doesn’t matter where in the file it goes:
message(cm project dir: $${PWD})
Add the following line to cm-lib.pro:
message(cm-lib project dir: $${PWD})
The message() is a test function supported by qmake that outputs the supplied string parameter to the console. Note that you don’t need to surround the text with double quotes. When you save the changes, you will see the Project Working Directory (PWD) of both the solution project and the library project logged out to the General Messages console:
Project MESSAGE: cm project dir: C:/projects/qt/cm
Project MESSAGE: cm-lib project dir: C:/projects/qt/cm/cm-lib
If we look back at the default behavior of Qt Creator for shadow builds, we’ll see that the aim was to allow multiple builds to sit alongside each other. This is achieved by constructing distinct folder names containing the kit, platform, and build configuration:
build-cm-solution-Desktop_Qt_5_10_0_MinGW_32bit-Debug
You can see just by looking at the folder name that the contents are from a build of the cm project using the Qt 5.10.0 for Desktop MinGW 32bit kit in Debug mode. We’ll now reimplement this approach in a cleaner and more flexible way.
Rather than concatenating the information into one long folder name, we'll prefer a hierarchical structure consisting of the Operating System > Compiler > Processor Architecture > Build Configuration folders.
Let’s first hard-code this path and then move on to automating it. Edit cm-lib.pro and add this:
DESTDIR = $$PWD/../binaries/windows/gcc/x86/debug message(cm-lib output dir: $${DESTDIR})
This is to reflect that we are building with the MinGW 32bit kit on Windows in Debug mode. Replace Windows with osx or Linux if you are on a different OS. We’ve added another call to message() to output this destination directory in the General Messages console. Remember that $$PWD extracts the working directory of the .pro file being processed (cm-lib.pro in this case), so this gives us <Qt Projects>/cm/cm-lib.
Right-click on the cm-lib project, run qmake, and build. Ensure that you have the MinGW kit selected, along with Debug mode.
Navigate to <Qt Projects>/cm/binaries/<OS>/gcc/x86/debug in the filesystem, and you will see our library binaries without the associated clutter of build artifacts. This is a good first step, but if you now change the build configuration to Release or switch kits, the destination directory will remain the same, which is not what we want.
The technique we are about to implement will be used in all three of our projects, so rather than having to duplicate the configuration in all of our .pro files, let’s extract the configuration to a shared file and include it instead.
In the root cm folder, create two new empty text files called qmake-target-platform.pri and qmake-destination-path.pri. In cm-lib.pro, cm-tests.pro, and cm-ui.pro, add these lines:
include(../qmake-target-platform.pri) include(../qmake-destination-path.pri)
Add these lines somewhere near the top of the *.pro files. The exact order doesn’t matter too much as long as they are before the DESTDIR variable is set.
Edit qmake-target-platform.pri as follows:
win32 { CONFIG += PLATFORM_WIN message(PLATFORM_WIN) win32-g++ { CONFIG += COMPILER_GCC message(COMPILER_GCC) } win32-msvc2017 { CONFIG += COMPILER_MSVC2017 message(COMPILER_MSVC2017) win32-msvc2017:QMAKE_TARGET.arch = x86_64 } }
linux { CONFIG += PLATFORM_LINUX message(PLATFORM_LINUX) # Make QMAKE_TARGET arch available for Linux !contains(QT_ARCH, x86_64){ QMAKE_TARGET.arch = x86 } else { QMAKE_TARGET.arch = x86_64 } linux-g++{ CONFIG += COMPILER_GCC message(COMPILER_GCC) } } macx { CONFIG += PLATFORM_OSX message(PLATFORM_OSX) macx-clang { CONFIG += COMPILER_CLANG message(COMPILER_CLANG) QMAKE_TARGET.arch = x86_64 } macx-clang-32{ CONFIG += COMPILER_CLANG message(COMPILER_CLANG) QMAKE_TARGET.arch = x86 } } contains(QMAKE_TARGET.arch, x86_64) { CONFIG += PROCESSOR_x64 message(PROCESSOR_x64) } else { CONFIG += PROCESSOR_x86 message(PROCESSOR_x86) }
CONFIG(debug, release|debug) { CONFIG += BUILD_DEBUG message(BUILD_DEBUG) } else { CONFIG += BUILD_RELEASE message(BUILD_RELEASE) }
Here, we are leveraging the platform detection capabilities of qmake to inject personalized flags into the CONFIG variable. On each operating system, different platform variables become available. For example, on Windows, the win32 variable is present, Linux is represented by linux, and Mac OS X by macx. We can use these platform variables with curly braces to act like if statements:
win32 { # This block will execute on Windows only… }
We can consider different combinations of platform variables to figure out what compiler and processor architecture the currently selected kit is using, and then add developer-friendly flags to the CONFIG, which we can use later in our .pro files. Remember that we are trying to construct a build path—Operating System > Compiler > Processor Architecture > Build Configuration.
When you save these changes, you should see the flags similar to the following in the General Message console:
Project MESSAGE: PLATFORM_WIN Project MESSAGE: COMPILER_GCC Project MESSAGE: PROCESSOR_x86 Project MESSAGE: BUILD_DEBUG
Try switching kits or changing the build configuration, and you should see different output. When I switch my kit to Visual Studio 2017 64 bit in Release mode, I now get this:
Project MESSAGE: PLATFORM_WIN Project MESSAGE: COMPILER_MSVC2017 Project MESSAGE: PROCESSOR_x64 Project MESSAGE: BUILD_RELEASE
With the same project on a Linux machine with the MinGW 64 bit kit, I get this:
Project MESSAGE: PLATFORM_LINUX Project MESSAGE: COMPILER_GCC Project MESSAGE: PROCESSOR_x64 Project MESSAGE: BUILD_DEBUG
On a Mac using Clang 64 bit, I get the following:
Project MESSAGE: PLATFORM_OSX Project MESSAGE: COMPILER_CLANG Project MESSAGE: PROCESSOR_x64 Project MESSAGE: BUILD_DEBUG
Now that all the platform detection is done, we can construct the destination path dynamically. Edit qmake-destination-path.pri:
platform_path = unknown-platform compiler_path = unknown-compiler processor_path = unknown-processor build_path = unknown-build
PLATFORM_WIN { platform_path = windows } PLATFORM_OSX { platform_path = osx } PLATFORM_LINUX { platform_path = linux } COMPILER_GCC { compiler_path = gcc } COMPILER_MSVC2017 { compiler_path = msvc2017 } COMPILER_CLANG { compiler_path = clang } PROCESSOR_x64 { processor_path = x64 } PROCESSOR_x86 { processor_path = x86 } BUILD_DEBUG { build_path = debug } else { build_path = release } DESTINATION_PATH = $$platform_path/$$compiler_path/$$processor_path/$$build_path message(Dest path: $${DESTINATION_PATH})
Here, we create four new variables—platform_path, compiler_path, processor_path, and build_path—and assign default values to them all. We then use the CONFIG flags we created in the previous file and construct our folder hierarchy, storing it in a variable of our own, called DESTINATION_PATH. For example, if we detect Windows as the operating system, we add the PLATFORM_WIN flag to CONFIG and as a result of that, set platform_path to windows. Switching between kits and configurations on Windows, I now get these messages:
Dest path: windows/gcc/x86/debug
Alternatively, I get this:
Dest path: windows/msvc2017/x64/release
On Linux, I get the following:
Dest path: linux/gcc/x64/debug
On Mac OS, this is what I get:
Dest path: osx/clang/x64/debug
You can just combine these platform detection and destination path creation tricks in one file, but by keeping them separate, you can use the flags elsewhere in your project files. In any case, we are now dynamically creating a path based on our build environment and storing it in a variable for later use.
The next thing to do is to plug this DESTINATION_PATH variable into our project files. While we’re here, we can also structure our build artifacts using the same mechanism by adding a few more lines. Add the following to all three *.pro files, replacing the DESTDIR statement already in cm-lib.pro:
DESTDIR = $$PWD/../binaries/$$DESTINATION_PATH OBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.obj MOC_DIR = $$PWD/build/$$DESTINATION_PATH/.moc RCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrc UI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui
Temporary build artifacts will now be placed into discreet directories within the build folder.
Finally, we can fix the problem that brought us here in the first place. In cm-tests and cm-ui, we can now set the LIBS variable using our new dynamic destination path:
LIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lcm-lib
You can now right-click on the cm project, run qmake, and build to automatically build all three subprojects in one step. All the output will be sent to the correct place and the library binaries can be easily located by the other projects. You can switch kits and configurations and not have to worry about referencing the wrong libraries.