Xmake: A great one-stop build system

  ·   8 min read

This week I started a new project, to evaluate Haxe and Heaps as an alternative game development framework for my 2D retro indie game. As the first required step, I would like to build my own Hashlink virtual machine and make sure it runs on all gaming clients I plan to support.

I had a rather painful experience with Cmake for two days, then I decided to use Xmake instead. It finally solved all my problem. I think I should write it down. And honestly I’m happy to be an advocate of Xmake as a C++ developer.

The build problem

The story started from the official Hashlink project repository on Github. It provides both Makefile and CMakeLists.txt. I use CMakeLists.txt because my plan covers both Linux and Windows. Not surprised, it broke at the first try. After some investigation I found the root cause. Hashlink requires an C library, MbedTLS, to build its ssl module. It requires an old version, MbedTLS 2.x. Unfortunately, my system, Manjaro Linux, has upgraded to MbedTLS 3.x. The 3.x version introduces breaking changes on a lot of APIs, which causes build break on Hashlink.

I continued my study and found, that Manjaro did provide a compatibility package, MbedTLS2, which had been installed to my system. However, Cmake build process still broke. This is because Manjaro provides a MbedTLSConfig.cmake configuration file only in the latest MbedTLS 3.x package, while compatibility package does not provide the configuration. That means, while a correct package is installed, CMake still can’t locate the binaries and headers. After struggling for a while, I finally decided to modify Cmake myself, that I manually pointed path of headers and binaries myself.

This is not the end of the task. Actually, building a Hashlink binary with system libraries is a bad idea. Manjaro prefers dynamic libraries (*.so files) on system packages. I can’t simply copy my build to different Linux machines because they may be using same version of dependencies. The build can crash if the installed libraries have different versions. And again, the existing CMake build tools does not help at all.

My own solution based on Xmake

After trying for two days, I finally decided to a new repository, HashBld. It’s an external set of build scripts. I expect the tools in this repository handle everything of building my code: from downloading dependencies, to build correct dependencies, then build my code, and at last, packages all needed binaries.

I picked Xmake instead of Cmake for my project. This is the first time I really use Xmake in my project. Surprisingly it does an unexpectedly perfect job in my case.

Xmake is not just a build manager. It has a built-in component called Xrepo. This is a package manager that maintains 1000+ packages from Internet. I can add a single line like add_requires("libsdl 2.28.5") to define a dependency in Xmake’s build script xmake.lua. When running xmake build, it automatically downloads a source code package from Internet with correct version, builds it as a static library, and save it as local cache. Then Xmake automatically build Hashlink code, with references automatically set to header and libraries of our libSDL2.a, instead of the system package.

There’s a second highlight in the process. If you have developed games with SDL, you probably know that SDL itself it built with CMake. In the process, Xmake understands it, and it can invoke Cmake to build SDL. Thus, I don’t need to take care of the problem that “what if my dependencies is not built with xmake”. Everything is built smoothly.

The third highlight, that makes me an “aha”, is Xmake provides more options than just versioning on add_requires(). An option I need is system=true|false. When this option is set, Xmake ignores all version spec, but tries to search and use package installed by system. This is useful in a scenario that when I develop a game on a non-general platform such as Steam Runtime or Nintendo Switch, that we could build the code against system-default libSDL2.so, which may lead to a better optimization. Besides, there are options for SDL library to build with only Wayland or Xorg, which allows me optimize build in detailed way.

So, all these good things leads to my xmake.lua I can share in public. So far it supports Linux only, but I’m working on it to make it cover more platforms.

A postmortem: CMake’s find_package() approach is a bad practice

I may have to say, that Cmake does a very bad job in my case. The biggest issue I learn from my case is, Cmake effectively does nothing on package management. Cmake provides an “API”, find_package(), which gives an illusion to inexperienced developers that it has ability to locate dependency. However, the “dirty works” is delegated to package managers, or let’s use Cmake terminology: dependency providers.

In my case, the true package manager is indeed my Operating System, Manjaro Linux. For sure the OS does not do a good job: it provides two packages but one MbedTLSConfig.cmake, pointing to version 3.x. Unfortunately, my code needs to build with 2.x. Cmake couldn’t help anything, until I manually wrote my own .cmake file.

People may argue that Cmake is not responsible for package locating, it’s useless to me. Yes, per protocol it should be the job of dependency providers. However, it does not really consider package managers’ point of view.

To be honest, it’s difficult to blame Manjaro Linux. Manjaro, as a rolling updating Linux distro, has a long tradition that it always provides only the build environment for latest version of software packages. This makes sense to them because Manjaro build system is responsible mostly for how to build a package for the OS, not how to build a portable binary. Manjaro team has made a decision that they always stick to latest version of every component, then they won’t pay much attention to old packages.

Thus, all the blames comes to app developers. that app developers are fully responsible to understand the roles and set up both correctly. Well, technically it makes sense. What makes me frustrated is, such attitude indicates, that more tools means more mind burdens. This is not a good design concept.

Given Cmake actually has no control on dependency provides, I really don’t like the idea that Cmake defines a long list of parameters for its find_package() API. If Cmake defines itself as a pure build system, it does not make sense to define a package management concept in find_pacakge() function, including but not limited to version, config, paths, etc. These concepts are purely for package managers, they should be located in package manager’s dependency settings. On the other hand, though Cmake defines find_package() with so many parameters, they do not take effect if a package manager does not full follow its protocol. My Manjaro case is a good example.

From practice point of view, I would say Cmake’s definition is not accepted by community either. Do you remember any project using find_package() with version specification? Not as far as I know. A true example from a real package manager like Conan: when it works with Cmake, Conan defines version spec in its own conanfile.txt, but only calls find_package() without any additional spec.

In one word, Cmake claims too many roles (define APIs with package management parameters), but does not take corresponding responsibilities (really drive package managers).

Xmake does a good job

Xmake does not follow Cmake’s approach. Instead, it takes responsibility of both build tool and package manager. Thus, when I write a line of add_requires() as a developer, I know the end-to-end build work is taken care by Xmake: it downloads source code archives from Internet, performs the build process, delivers static libraries and saves them in local cache. Then Xmake builds my source code, links to dependencies. All jobs done.

The biggest benefit of Xmake is it does not need any third-party dependency providers, even the Operating System. It’s fine that my Manjaro Linux provides only build configuration for MbedTLS 3.x. Xmake just builds a copy of MbedTLS 2.x from source code for me. It can do the same job for all dependencies I need. Thus, it maximize the ability that the built C/C++ binaries can be executed in a client environment without heavy dependency pre-installed.

The second benefit of Xmake is gives enough flexibility. With the system = true|false option and other APIs like is_config(), I can design my building policies in similar system but different environment. Like the previous Steam runtime example, it is basically a Linux, but I can use different link policies. Similar policies can be combined with build config options to give more control.

Xmake does a great job to minimize my mind overhead on platform. It allows app developers like me to focus on my gaming logic.

Summary

Though this is the first time I use Xmake in a real project, I found it already a promising tool. Unlike some discussions on Reddit talking about topics like simplified syntax, the features I use so far gives me a strong impression, that Xmake team really understand the pains of cross-platform C/C++ developers, and provides a solution with enough flexibility and capability.

Besides, Xmake has a good interoperability with existing systems like Cmake. Thus, there’s no chance for me to refuse it anymore.

I hope this note is useful for more C/C++ developers who faces similar challenges with me. If you have time, please kindly give chance to Xmake. I believe you will not regret it.