Escaping the Garden (Qt through premake)
I generally am not a fan of frameworks. Instead, I tend to prefer tools that maximize my control, which results in a love of things like C++, Arch Linux, and toolkits. For a recent side project at work, I needed to create a GUI interface. In the past, I had used wxWidgets but I decided it was time to expand my horizons and try something else, so I tried Qt, and I have to say its been amazing. Everything seems to flow so easily when writing GUIs in Qt. Unfortunately, Qt is very much a framework. Abandon your freedom of choice and never venture outside the walled garden and you will be happy.
I, however, prefer to take the red pill. One of the first things you'll notice when moving to Qt is that they have their own makefile system called qmake. This build system will ensure you have your Qt dependencies and will invoke Qt's Meta Object Compiler (moc). (Thats right, Qt has its own preprocessor). My target in sight, I decided to claim a small victory against the garden and continue to use my build system of choice premake.
Premake is a build system built on top of Lua. This allows for great flexibility, since its a full scripting language rather than a DSL like CMake. In order to accomplish my task, I needed to override some functions in premake's gmake generator. It is worth noting that I am doing this in premake4, premake5 is currently in alpha and has standardized facilities for overriding functions.
The Qt preprocessor parses through C header files and generates C source code files that need to be compiled into the final binary. For my purposes, the preparser was handling a C macro
Q_OBJECT. This means that first I must identify files needing to be preprocessed. I decided my general structure would be:
- Original source code
- Folder filled with generated code from the moc output
So first I have a utility function to simply print a lua table out to the console that helps with debugging:
function print_table(t) for key,value in pairs(t) do print(key,value) end end
and a function that will translate the path of a file from src/*.h to moc/*moc.cpp (moc postfix to prevent name collisions)
function translate_file(path) return string.gsub(string.gsub(path, "src/", "moc/"), ".h", "moc.cpp") end
Now I'll need to identify the files that need to be preprocessed, so I iterate over the files line-by-line looking for
Q_OBJECT as a substring. Since I am not actually parsing the C code there is a change that I could have a false positive in the form of comments or blocked removed by the real C Preprocessor but for my purposes I haven't run into the issue:
function needs_preprocessing(path) for line in io.lines(path) do if string.find(line, "Q_OBJECT") ~= nil then return true end end return false end
Now, due to the structure of premake and the order it executes functions I will need to do the actualy transformation in two steps:
- Generate prebuild commands that run the header files through
- Add the generated files to the list of source files for compilation
It would be much cleaner if I could have done this all in one step, but that would involve editing premake.
For the first step, I will overload
gmake_cpp_config(). First I will need to save the current
gmake_cpp_config() function, then I will replace it with my own function that will invoke the saved function.
old_gmake_cpp_config = premake.gmake_cpp_config -- Modify prebuild commands premake.gmake_cpp_config = function(proj, cc) for i,k in pairs(os.matchfiles("src/**.h")) do if needs_preprocessing(k) then processed_file = translate_file(k) table.insert(proj.prebuildcommands, "mkdir -p $$(dirname " .. processed_file .. ") && moc -o " .. processed_file .. " " .. k) end end old_gmake_cpp_config(proj, cc) end
As you can see, I am iterating over the
src/ directory looking for header files and then executing
moc on them if they need preprocessing. Now that we have the output files, we need to add them to the list of files to be compiled. This involves overriding
make_cpp() in much the same way:
old_make_cpp = premake.make_cpp -- Modify files premake.make_cpp = function(proj) for i,k in pairs(os.matchfiles("src/**.h")) do if needs_preprocessing(k) then processed_file = translate_file(k) table.insert(proj.files, processed_file) end end old_make_cpp(proj) end
After all that, I just need to make sure I link the proper Qt libraries to my project, and build! Yay freedom.