Xmake is a lightweight and modern C/C++ project build tool based on Lua. Its main features are: easy to use syntax, easy to use project maintenance, and a consistent build experience across platforms.

This article mainly explains in detail how to write some commonly used basic xmake.lua description configurations to achieve some simple C/C++ project build management. For most small projects, these configurations are completely sufficient. In the later advanced tutorials in this series, I will explain in detail how to use some advanced features to configure the project more flexibly and customized.

A simplest example

One line of description compiles all c source files in the src directory, and then generates an executable file named demo.

target ( "demo" , { kind = "binary" , files = "src/*.c" })

The above is a condensed version. Generally, we recommend the following expansion method:

target ( "demo" ) set_kind ( "binary" ) add_files ( "src/*.c" )

The two are completely equivalent. If the configuration is short, it can be completely reduced to one line, and splitting into multiple lines is more convenient and flexible.

If there is no special purpose, we will use the second paragraph.

Configure project target type

There are three main types of object files generated by common C/C++ projects: executable programs, static libraries, and dynamic libraries.

We can set it through set_kind() configuration, corresponding to: binary, static, shared

For example, if we want to compile the dynamic library, we just need to modify the kind:

target ( "demo" ) set_kind ( "shared" ) add_files ( "src/*.c" )

Add macro definition

The compilation macro settings are used by most c/c++ projects. Generally, if we set compilation flags to be passed to gcc/clang, we need to configure: -DXXX

In xmake, the add_defines() built-in interface is provided for configuration:

target ( "demo" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "XXX" )

Conditional configuration

What if we want to set different macro switches on different compilation platforms? We can easily implement it using lua’s built-in if statement:

target ( "demo" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "XXX" ) if is_plat ( "linux" , "macosx" ) then add_defines ( "YYY" ) end

We judge by is_plat() . If the current compilation target platform is linux or macosx, then the target will additionally add the -DYYY macro definition.

Global configuration

All the configurations under target("demo") belong to the target subdomain of demo, not global, so you will see that the indentation is usually added to the configuration to highlight the scope of influence. .

Generally, if multiple targets are defined consecutively, the next target definition will automatically end the scope of the previous target. The configuration of each target is completely independent and does not interfere with each other:

target ( "test1" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST1" ) target ( "test2" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST2" )

For example, the above configuration has two targets, each with its own independent macro definition: TEST1 and TEST2 .

So, we need to set a common macro definition for these two targets. How should we configure it?

add_defines("TEST") is configured under each target? Of course it can, but this is a bit redundant, and it will be difficult to maintain if you configure more. In fact, we only need to place it in the global root scope:

-- global settings add_defines ( "TEST" ) if is_arch ( "arm64" , "armv7" ) then add_defines ( "ARM" ) end target ( "test1" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST1" ) target ( "test2" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST2" )

All configurations outside the target belong to the global configuration. We can also call target_end() to forcibly end the target subdomain and switch back to the global scope:

target ( "test1" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST1" ) target_end () -- global settings add_defines ( "TEST" ) if is_arch ( "arm64" , "armv7" ) then add_defines ( "ARM" ) end target ( "test2" ) set_kind ( "shared" ) add_files ( "src/*.c" ) add_defines ( "TEST2" ) target_end ()

Add compilation options

If there are some compilation options, xmake does not provide built-in api settings, then we can degenerate to add_cflags , add_cxflags , add_cxxflags to set, However, this requires the user to determine the compilation platform, because not all compilation flags are supported on every platform.

such as:

add_cflags ( "-g" , "-O2" , "-DDEBUG" ) if is_plat ( "windows" ) then add_cflags ( "/MT" ) end

All option values are based on the definition of gcc as standard. If other compilers are not compatible (for example: vc), xmake will automatically convert them internally to option values supported by the corresponding compiler. The user does not need to worry about its compatibility. If other compilers do not have corresponding matching values, xmake will automatically ignore the compiler settings.

We can also force the automatic detection of flags through the force parameter, and pass it directly to the compiler, even if the compiler may not support it, it will be set:

add_cflags ( "-g" , "-O2" , { force = true })

So how do you know which flags failed to be ignored, you can see with -v compilation, such as:

$ xmake -v checking for the /usr/bin/xcrun -sdk macosx clang ... ok checking for the flags ( -Oz ) ... ok checking for the flags ( -Wno-error = deprecated-declarations ) ... ok checking for the flags ( -fno-strict-aliasing ) ... ok checking for the flags ( -Wno-error = expansion-to-defined ) ... no

Finally, note the differences between these three APIs:

add_cflags : add only C code-related compilation flags

: add only C code-related compilation flags add_cxflags : add C/c++ code related flags

: add C/c++ code related flags add_cxxflags : add only c++ code-related compilation flags

Add library related settings

For the integrated use of a C/c++ library, you usually need to set the header file search directory, link library name, and library search directory, such as:

target ( "test" ) set_kind ( "binary" ) add_links ( "pthread" ) add_includedirs ( "/usr/local/include" ) add_linkdirs ( "/usr/local/lib" )

Generally, in order to ensure the dependency order of the linked libraries, the system library links are usually backward. We use add_syslinks() to set the system library links specifically, and add_links() is usually used for non-system library links:

target ( "test" ) set_kind ( "binary" ) add_links ( "A" , "B" ) add_syslinks ( "pthread" )

In the above configuration, we added two third-party link libraries: A, B, and the system library pthread. The complete link sequence is: -lA -lB -lpthread , and syslinks will be placed at the end.

If you are unsure of the actual linking order, we can execute xmake -v compilation to see the complete link parameter command line.

Setting the language standard

The c standard and c++ standard can be set at the same time, for example:

-- set the c code standard: c99, c++ code standard: c++ 11 set_languages ( "c99" , "c++11" )

Note: The specified standard is not set, and the compiler will compile according to this standard. After all, each compiler supports different strengths, but xmake will try its best to adapt to the current standard of the compilation tool.

For example: the compiler for windows vs does not support compiling c code according to the c99 standard, only c89 is supported, but xmake supports it as much as possible, so after setting the c99 standard, xmake will compile c code according to c++ code mode, which solves the problem of c code compiling c99 under windows.

Set compilation optimization

xmake provides several built-in compilation optimization configurations: none, fast, faster, fastest, smallest, aggressive, to achieve various levels of compilation optimization.

set_optimize ( "fastest" )

If you set it through flags, you also need to consider the different compilation options of different compilers. Xmake has an internal mapping process for it, which greatly facilitates users to provide cross-platform.

If you want to view detailed mapping rules, you can go to the official documentation of xmake to check it: Compile Optimization Settings

Debug and Release Mode

Even though xmake provides set_optimize to simplify the complicated configuration of different compilers, for different compilation modes: debug/release, you still have to make some tedious judgments and configurations yourself:

if is_mode ( "debug" ) then set_symbols ( "debug" ) set_optimize ( "none" ) end if is_mode ( "release" ) then set_symbols ( "hidden" ) set_strip ( "all" ) if is_plat ( "iphoneos" , "android" ) then set_optimize ( "smallest" ) else set_optimize ( "fastest" ) end end

These seemingly commonly used settings, if each project is repeated, it is also very tedious, resulting in xmake.lua not concise and readable, so xmake provides some commonly used built-in rules to simplify the setting:

add_rules ( "mode.release" , "mode.debug" )

Only this line is needed, the effect is completely the same, and the user can also do some additional custom configurations to rewrite based on this:

add_rules ( "mode.release" , "mode.debug" ) if is_mode ( "release" ) then set_optimize ( "fastest" ) end

For example, I want to force fastest compilation optimization in release mode. Now that we have a mode configuration, how do we switch to debug mode compilation? (Default is release compilation)

answer:

xmake f - m debug ; xmake

Add source files

Finally, we introduce one of the most common and powerful settings of xmake, which is the configuration management of compiled source files: add_files() .

We can use this interface to add various source files supported by xmake, such as: c/c++, asm, objc, swift, go, dlang and other source files, and even: .obj , .a/.lib , etc. Binary objects and library files.

E.g:

add_files ( "src/test_*.c" ) add_files ( "src/xxx/**.cpp" ) add_files ( "src/asm/*.S" , "src/objc/**/hello.m" )

The wildcard * matches files in the current directory, while ** matches files in multiple directories.

The use of add_files is actually quite flexible and convenient. Its matching pattern borrows the style of premake, but it is improved and enhanced.

This makes it possible not only to match files, but also to filter out a batch of files with a specified pattern while adding files.

E.g:

-- Recursively add all c files under src, but not all c files under src/impl/ add_files ( "src/**.c|impl/*.c" ) -- Add all cpp files under src, but exclude src/test.cpp, src/hello.cpp and all cpp files with xx_ prefix under src add_files ( "src/*.cpp|test.cpp|hello.cpp|xx_*.cpp" )

The files after the delimiter | are all files that need to be excluded. These files also support matching patterns, and multiple filtering patterns can be added at the same time, as long as they are separated by | . .

One of the benefits of supporting filtering some files when adding files is that it can provide a basis for subsequent addition of files based on different switch logic.

Note: In order to make the description more concise, the filtering description after | is based on a pattern: the directory before * in src/*.cpp . So the above example filters the files under src, which should be noted.

After 2.1.6, add_files has been improved to support more fine-grained compilation option control based on files, for example:

target ( "test" ) add_defines ( "TEST1" ) add_files ( "src/*.c" ) add_files ( "test/*.c" , "test2/test2.c" , { defines = "TEST2" , languages = "c99" , includedirs = "." , cflags = "-O0" })

You can pass a configuration table in the last parameter of add_files to control the compilation options of the specified files. The configuration parameters are the same as those of the target, and these files will inherit the target’s general configuration -DTEST1 .

After version 2.1.9, it supports adding unknown code files. By setting rule customization rules, these files can be custom built, for example:

target ( "test" ) -- ... add_files ( "src/test/*. md" , { rule = "markdown" })

And after version 2.1.9, you can use the force parameter to forcibly disable the automatic detection of cxflags, cflags and other compilation options, and pass it directly to the compiler, even if the compiler may not support it, it will be set:

add_files ( "src/*.c" , { force = { cxflags = "-DTEST" , mflags = "-framework xxx" }})

Delete the specified source file

Now that we ’ve talked about adding source files, how to delete them, let ’s just drop in. We only need to use the del_files() interface to delete the specified files from the list of files added by the add_files interface. :

target ( "test" ) add_files ( "src/*.c" ) del_files ( "src/test.c" )

In the above example, you can add all files except test.c from the src directory. Of course, this can also be achieved by add_files("src/*.c|test.c") . But this approach is more flexible.

For example, we can use conditional judgment to control which files are deleted, and this interface also supports add_files matching mode, filtering mode, and batch removal.

target ( "test" ) add_files ( "src/**.c" ) del_files ( "src/test * .c" ) del_files ( "src/subdir/*.c|xxx.c" ) if is_plat ( "iphoneos" ) then add_files ( "xxx.m" ) end