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.