 Hi everyone, I'm a Xelik Lassence, software engineer at Colabora. Today I'm going to present a quick introduction to the Maison build system and how it can be used to shrink the size of GSTU Mobile. Let's start with a quick introduction to Maison, its syntax and how it's used. First what is Maison? It's a build system that runs on multiple platforms including Linux, macOS and Windows. It can also use Visual Studio. It supports multiple languages like C, C++, it is written in Python 3 and it supports cross-compilation so you can cross-compile your project for a dedicated device or Android device or anything. It's user-friendly, the syntax is really easy to understand and read. Last but not least, it's fast. One of the main goals is to have a fast build system. To do that, Maison generates a Ninja build instructions. Ninja is really fast to compile the project and only recompile the parts that need it. Let's start with a really simple example that defines a library. First of all, you put your build definition in a Maison.bit file at the top of your source tree. In that file, you start by declaring the project, giving its name, the language used in this example. It's a C library. You define the version of your project, the Maison version required and so forth. To build a library, you first need to declare the source files needed to compile the library. Maison does not support globbling, so start.c for example. That's for performance reasons. You have to list all the files you need for your library or any target. Then you can build the library, give it a name, give the source files, and install that library and install the header files of the library. To compile the library, the command is... First, you have to configure the project. To configure the project, you run Maison setup and give it a build directory. It's going to do an out of source tree build. That means that Maison is not going to create any file inside the source tree, but put everything inside the dedicated directory. By default, it's going to build a shared library, but we'll see later how we can build the static library or even both. For the compilation step, it uses Ninja. Maison only generates a Ninja file and then Ninja is going to execute the compilation. As I said, Maison supports building shared library, static library, and even both at the same time. To control that by default, it's going to build shared library, but you have dash-default library equal, and then you can give it both or just static. If you do both, Maison will compile as you can see here. The source files, both the C and C++ source files are written in once for both the static here and the shared library here. That's a trick that saves a lot of build time. It has some limitations, like, for example, that cannot be used on Windows. Let's see how Maison supports cross compilation next. To cross compile, you have to define a cross file. A cross file is basically a unify that defines the tool chain and the target machine. Any file supports some special syntax that helps you maintain a readable setting. For example, here you can see that we can define some constants. With the path to, for example, the Android and DK you installed on your system. You can use that constant and use the slash operator to concat a path. So in this case, we don't have to repeat the path to and DK. We can already, we can define another constant. We name it tool chain here and it's going to concat both paths. Those constants can be used inside the rest of your file here and here. So you don't have to repeat the full path to your tool chain. So as you can see here, we define a C-sroot where the compiler is going to find head of files and libraries. And here we define the C, C++ and a few binaries needed to compile. Now that we have written that file, we can pass it to Maison setup command line with dash, dash cross file. And as you can see, it's going to use the CLN compiler from the Android and DK to compile our small example project. As before, so Maison does the setup, the configuration part and then Ninja does the build. And as you can see, the example that the shared library is cross-built for ARM architecture. Now every C and C++ project needs external dependency. Let's see how Maison supports that. Maison has the dependency method. It's really straightforward. You pass it the name of your dependency and Maison is going to look up on your system if it can find it. To find the dependency, Maison uses mostly PKG config or CMake. As you can see, in this example, we look for the libxml dependency, external dependency and pass it to build our library. So now our library can depend and be linked on the libxml. Unfortunately, especially when we cross-compile, most of the time the dependencies are not found on the system. Because of course, if you cross-compile on Linux for Android, you cannot link on libxml from your system. You have to provide all the dependencies already built for Android. Most of the time people use a meta-build system for that, but Maison has a really spatial way of handling that for you. Maison has something called sub-projects. A sub-project is a dependency you can have on another project. And Maison will build it together with your project. So let's say you have an application that needs a dependency example to build the application. You can give to your application a file named sub-projects-example.wrap. And there you define where to download the source code of that dependency and which dependency names it provides. So here it provides an example dependency. That means that whenever Maison looks up for the example dependency and cannot find it on the system, it's automatically going to git clone the project from GitHub and configure it as part of your project and link it together. Here is an example. You can see the lib example we had previously, but we added a few things there to be used as a sub-project for the application. So we declare a dependency. So we say that to use our lib example, we need to link the lib example and we need to include directory to the current directory, so the top source tree of that sub-project. And we can even generate a pkgconfig file. You can see that's really simple because Maison knows all the flags, C flags, and the linker flags needed inside that pkgconfig file. So you can just ask to generate one and it's going to be done automatically. Now it's really, it's complicated to maintain a list of all your dependency wrap files like that. You need the location of every dependency that's complicated and Maison has something called WrapDB. You can go and look at it and wrapdb.maisonbuild.com. That's a collection of wrap files. That's basically just a collection of wrap files for all the, every project we know that supports Maison build system either as an upstream build system or some of them are supported by the Maison project itself. So we have a Maison port of, for example, here, libxml2. We have a Maison port maintained by the Maison team. And if you use, if you fetch that wrap file from wrapdb, you're going to, it's going to download and configure libxml for you on your project so you don't have to build it separately. That system can go wild. So Gstreamer in particular has many, many, many projects. You can see here the list. It's over 40 sub-project that can be used. And together, that means that Gstreamer can be built completely without any dependency from your system. When I say completely, some of the features are missing, so some dependencies are missing, but every half dependency are provided by a Maison sub-project and even more than, even lots of optional dependencies. Let's dive in now how we build the Gstreamer. In our presentation now, we are going to see how we can cross compile, for example, Gstreamer for the Android platform using the Android and DK and see how we can go from a build that takes, if you enable all the features of Gstreamer, it will take about 150 megabytes. And we'll see a few tricks that can strip down that size to something really small that can fit embedded devices. So if you build by default, you just provide Maison the cross file that is already provided in the Gstreamer project. You have the cross file already there. You just have to edit that file to change the path where you installed your Android and DK on your system. If you just a simple command like that and no option, it's going to configure over 40 sub-project by default. So you're going to have caro, free type 2, lip soup and so forth, and that's going to make a really huge build, as I said, over 150 megabytes. Fortunately, Gstreamer has a feature option for every single plugin it has. So how it works, a feature option. Here we see the example with the Opus plugin. So Gstreamer has plugins for every feature it has. So every plugin can be built separately and we can enable some plugins or not. The goal when you do a small build is to only enable the plugins you actually need in your applications and skip everything that you don't need. So yeah, as you can see, we can declare the Opus option, set it a type for features. A feature has three possible values. It's either auto enabled or disabled. Enable and disable are which is straightforward. We have to build with the Opus plugin. Disable means we don't build the Opus plugin. And auto means that we build the plugin only if the dependency is found on your system or if a subproject can provide that dependency. So you can see here, when we look up for the Opus dependency, we said it is required and we get the Opus option. That means that if the Opus option is set to disabled, for example, it's going to always return not found. And if it's not found, then the plugin is not built. If it's set to auto, Nezon is going to look up for a subproject that provides the Opus external dependency and build that subproject. So in our case, we want to disable all those features, features option for everything we don't need in an Android application. Disabling every features one by one is going to be really difficult because there are a lot of options like that and you have to know them all. But Nezon has a trick. By default, every option in Gstreamer is set to auto, but Nezon has a special option called auto features. That option can switch all the features that the value is set to auto. It can switch them all at once either to disabled or enabled. In our case, we want to disable everything. With that command line option, you can pass to Nezon Configure. It's going to disable every plugin in Gstreamer and you're going to have basically an empty build. Now, of course, we need to enable a few of them, those that we know that we need for our application and those can be enabled one by one after we disabled everything by default. Here is an example command line to make the smallest build possible for Android. I mean, the less plugins possible. As you can see, you first pass the crossfire to disable every features and then you enable the few that you actually need for a useful Android build area. We need Android media plugin. That one provides the Android codecs. It uses Android internal API for supporting other codecs. And itself, it also requires GL support in Gstreamer. So, those options here just says that we disable everything except for GL support and Android codecs. And when you do that, you can see that the subprojects enabled during your build are really small. Most of them are just disabled and only the app dependency or those that you enabled are actually built. So, you can see here it's going to build Glib because Glib is a hard dependency of Gstreamer. And also, you can see it's nothing else except PCR. That one is a dependency of Glib. So, when you do that build and install that inside this dear, you can see that it takes about 48 megabytes when I did the try on my laptop. It can change a bit depending on which version of Gstreamer you have. Of course, here I did the try on my Gstreamer main branch. So, let's see how we can train that 48 megabytes down as much as possible. But that's already a good start because, as I said, a full build is over 100 megabytes. So, that's already a good start. The first step, of course, is we can skip all the files we don't need at runtime. So, all the header files, pkgconfig files, static libraries, if there is any, all those are not needed when you are really running the application. So, to do that, Meson has a nice command line. You can pass to Meson, install, you can pass dash dash tag runtime. And that's going to install only the files, the targets that are tagged as a runtime target. So, runtime targets are most of the time they are shared libraries, executables. Yeah, that's basically the only thing we have in this context. So, when you do that, to see you're already down to 40 megabytes, so that eight megabytes already just for useless files that we remove. Next step, obviously, is to strip the binaries. Meson installs support the dash dash strip command line. And that's actually, that's divided by four, the size of your libraries, so you are down to 11 megabytes. That's starting to look good. The next step is doing a static build, because static builds allows the linker to do a lot of optimization. The linker is going to drop all the code that is not actually used for that build. To build, to make a static build with Meson, you have the option dash dash default library equals static. So, every library Meson is going to build, instead of building the shared library, it's going to be a static library instead. As you can see, when I did that build, the lib directory went from 11 megabytes down to 5 megabytes. So, it's half of the size, just because we static linked everything together. When I say static link, it's going to static link gstreamer, all the gstreamer libraries, all the gstreamer plugins, but also all the dependency that we built as subprojects. As you can see in this example, compared to the previous one, the gstreamer dash one zero directory is gone. That directory is where plugins are usually installed. Why is that, is that gone? Well, as I said, it's going to static link everything together. So, the plugins are not shared library anymore. They are static link inside what we call lib gstreamer full. So, that's a single shared library, specific to gstreamer that includes gstreamer, all the gstreamer libraries and their dependencies, but also all the plugins. Plugins are automatically initialized when you do GST in it, so you don't have to worry about any plugin. It's all done transparently. It's just like if gstreamer had no plugin system at all from there. So, that shared library actually exposed an ABI. In that ABI, you will have all the symbols from glib and gobject and lib gstreamer. So, with that ABI, you can now build an applications that use gstreamer and glib, because glib, of course, it's required to write any gstreamer application. Oh, yeah. So, as you can see, we were already at 5 megabytes. So, that shared library is about 5.2 megabytes. That's really good. We shrink gstreamer quite a lot already. So, a few exceptions that can shrink even more your builds. Obviously, you want to disable the debug. So, you have dash gd debug equal force. You want to optimize for size. The optimization, that's the flag pass to GCC or C-line. They can optimize the code for speed or size. In some specific cases, you want to optimize for size. For the case where every byte counts, if you want to fit gstreamer on a smartwatch, every byte counts. So, that's useful. And then you can also disable glib assert and checks. Those are macros that checks at runtime if the instance of the object are valid or not. And you can disable those checks. But give a little bit of runtime speedup and also shrink a little bit the binary size. I would not recommend that during the development at all. But for the production projects, once you have really stabilized your application, that's a little trick you can use as well. Currently, there is a small glitch between the gstreamer option and the glib option. As you can see, you have to duplicate them here. That's simply because gstreamer uses a dash where I glib user underscore. That difference means that meson does not recognize those options as being the same. Otherwise, the queue first would be enough. There is a merge request I made for gstreamer to uniformize the option name. But since it's not merged yet. As you can see now, we are down to a little bit over 4 megabytes. Now, let's see a few little experimental tricks that can help you shrink it even further. Those are more advanced and you don't always need them. One trick I did for an old project is to strip a part of the EBI we have from glib. For that, you can pass LD version scripts. We have an example here. The gstreamer full shared library is going to expose every public symbol from glib and gobject. That means that when a symbol is exposed, even if your application does not use that symbol, all the codes and the binary related to that symbol is included inside your shared library. If you use that script, it says that some of those symbols are actually local. That means they are private and they are not exposed. That means that the linker can then strip them out of the binary if they are not used internally by that library. A few of glib's API are not used by gstreamer itself. We can hide them and that saves a few bytes in your binary. This is just an example. Of course, you can tweak that with more symbols but make sure that your application is not using them otherwise that's going to be a linker error. The final trick we have, so glib has a Unicode support for UTF-8. That Unicode support includes huge Unicode tables that's over a megabyte of tables included inside the glib binary. Most of the time, the device is already heavily by CU. That's a really popular C++ library that does all the same Unicode operations and includes the same tables. You end up with a duplicated data there. If you are really constrained in binary size, you want to drop the tables from glib and use those from libICU. We did with a collaborator colleague did a merge request on glib to optionally use libICU in glib instead of glib's internal implementation for Unicode. If you use that merge request from glib and you have a new option where you can set to use the libICU Unicode implementation instead of glib, that's going to drop all the Unicode tables and related curves from the glib binary and save over a megabyte. Unfortunately, that merge request needs a little bit of work so it's not working anymore, so I don't have the exact numbers when I did my presentation. But if anyone is interested, help is welcome here. That's it for me today. Hope you learned something. By the way, I'm working at Collabora and we are in. If you are interested in working in open source and cool projects, please send us an email. Thank you.