xmake v2.5.9 release, improves C++20 modules, and supports Nim, Keil MDK, and Unity Build

Keywords: C++ cmake Makefile nim

xmake Lua-based lightweight cross-platform construction tool, built with xmake.lua maintenance project, has a simpler and more intuitive configuration syntax than makefile/CMakeLists.txt, is very friendly to novices, can get started quickly in a short time, and enables users to focus more on the actual project development.

In this release, we have added a number of new and heavy features, such as Nim language project building support, Keil MDK, Circle and Wasi tool chain support.

In addition, we have greatly improved the C++20 Modules to support not only the latest gcc-11, clang, and msvc compilers, but also automatic analysis of intermodule dependencies to maximize parallel compilation support.

Finally, another useful feature is Unity Build support, which allows us to greatly speed up the compilation of C++ code.

Introduction to new features

Nimlang Project Construction

Recently, we have added support for building Nimlang projects. For related issues, see: #1756

Create an empty project

We can use the xmake create command to create an empty project.

xmake create -l nim -t console test
xmake create -l nim -t static test
xmake create -l nim -t shared test

Console Application

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

target("test")
    set_kind("binary")
    add_files("src/main.nim")
$ xmake -v
[ 33%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache -o:b
uild/macosx/x86_64/release/test src/main.nim
[100%]: build ok!

Static library program

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

target("foo")
    set_kind("static")
    add_files("src/foo.nim")

target("test")
    set_kind("binary")
    add_deps("foo")
    add_files("src/main.nim")
$ xmake -v
[ 33%]: linking.release libfoo.a
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
:staticlib --noMain --passC:-DNimMain=NimMain_B6D5BD02 --passC:-DNimMainInner=NimMainInner_B6D5B
D02 --passC:-DNimMainModule=NimMainModule_B6D5BD02 --passC:-DPreMain=PreMain_B6D5BD02 --passC:-D
PreMainInner=PreMainInner_B6D5BD02 -o:build/macosx/x86_64/release/libfoo.a src/foo.nim
[ 66%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
[100%]: build ok!

Dynamic Library Program

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

target("foo")
    set_kind("shared")
    add_files("src/foo.nim")

target("test")
    set_kind("binary")
    add_deps("foo")
    add_files("src/main.nim")
$ xmake -rv
[ 33%]: linking.release libfoo.dylib
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
:lib --noMain -o:build/macosx/x86_64/release/libfoo.dylib src/foo.nim
[ 66%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
[100%]: build ok!

Mixed compilation of C code

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

target("foo")
    set_kind("static")
    add_files("src/*.c")

target("test")
    set_kind("binary")
    add_deps("foo")
    add_files("src/main.nim")

Nimble Dependent Package Integration

For a complete example: Nimble Package Example

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

add_requires("nimble::zip >0.3")

target("test")
    set_kind("binary")
    add_files("src/main.nim")
    add_packages("nimble::zip")

main.nim

import zip/zlib

echo zlibVersion()

Native Dependent Package Integration

For a complete example: Native Package Example

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

add_requires("zlib")

target("test")
    set_kind("binary")
    add_files("src/main.nim")
    add_packages("zlib")

main.nim

proc zlibVersion(): cstring {.cdecl, importc}

echo zlibVersion()

Unity Build Acceleration

We know that C++ code is usually slow to compile because each code file needs to parse the introduced header file.

With Unity Build, we speed up the compilation of a project by combining multiple cpp files into one. The main benefit is to reduce the duplication of parsing and compiling the header content contained in multiple source files, which usually accounts for most of the code in the pre-processed source files.

Unity building also reduces overhead due to having a large number of small source files by reducing the number of target files created and processed in the compilation chain, and allows for inter-process analysis and optimization (similar to optimization when linking effects) across files that form a unified build task.

It can greatly increase the compilation speed of C/C++ code, usually up to 30%, but depending on the complexity of the project, the benefits will depend on your project.

xmake already supports this build pattern in v2.5.9. See related issues #1019.

How do I enable it?

We provide two built-in rules for Unity Build for C and C++ code, respectively.

add_rules("c.unity_build")
add_rules("c++.unity_build")

Batch mode

By default, Unity Build in Batch mode is enabled whenever the above rules are set, that is, xmake automatically organizes merges based on project code files.

target("test")
    set_kind("binary")
    add_includedirs("src")
    add_rules("c++.unity_build", {batchsize = 2})
    add_files("src/*.c", "src/*.cpp")

Additionally, we can specify the size of each merged Batch by setting the {batchsize = 2} parameter to the rule, where every two C++ files are automatically merged and compiled.

The compilation effect is approximately as follows:

$ xmake -r
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_642A245F.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_bar.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_73161A20.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_F905F036.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_foo.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/main.c
[ 77%]: linking.release test
[100%]: build ok

Since we only enabled Unity Build in C++, the C code is still compiled one by one. In addition, in Unity Build mode, we can achieve as much parallel compilation speed as possible, without conflict with each other.

If the batchsize parameter is not set, all files are merged into one file for compilation by default.

Group mode

If the above Batch mode does not work well for automatic merging, we can also use custom grouping to manually configure which files are merged together to participate in compilation, which gives users more flexibility and control.

target("test")
    set_kind("binary")
    add_rules("c++.unity_build", {batchsize = 0}) -- disable batch mode
    add_files("src/*.c", "src/*.cpp")
    add_files("src/foo/*.c", {unity_group = "foo"})
    add_files("src/bar/*.c", {unity_group = "bar"})

We use {unity_group = "foo"} to specify the name of each group and which files are included, each grouped file being merged into a single code file.

In addition, batchsize = 0 also forcibly disables Batch mode, that is, unity_is not set Grouped code files, which we will compile separately, will not automatically turn on automatic merging.

Batch and Group Mixed Mode

By simply changing the above batchsize = 0 to a non-zero value, you can let the remaining code files continue to open Batch mode for automatic merge compilation.

target("test")
    set_kind("binary")
    add_includedirs("src")
    add_rules("c++.unity_build", {batchsize = 2})
    add_files("src/*.c", "src/*.cpp")
    add_files("src/foo/*.c", {unity_group = "foo"})
    add_files("src/bar/*.c", {unity_group = "bar"})

Ignore specified file

In Batch mode, all files are merged by default because they are automatic merge operations, but if some code files do not want them to participate in the merge, we can also ignore them by {unity_ignore = true}.

target("test")
    set_kind("binary")
    add_includedirs("src")
    add_rules("c++.unity_build", {batchsize = 2})
    add_files("src/*.c", "src/*.cpp")
    add_files("src/test/*.c", {unity_ignored = true}) -- ignore these files

Unique ID

Although Unity Build brings good results, we still encounter some unexpected situations, such as the existence of global variables and functions with the same name in both of our code files and in the global namespace.

Merge compilation then causes compilation conflicts, and compilers often report global variable redefinition errors.

To solve this problem, we need to make some modifications to the user code and then work with the build tools to solve it.

For example, our foo.cpp and bar.cpp both have global variable i.

foo.cpp

namespace {
    int i = 42;
}

int foo()
{
    return i;
}

bar.cpp

namespace {
    int i = 42;
}

int bar()
{
    return i;
}

Then, if we merge and compile, there will be a conflict, and we can introduce a Unique ID to isolate the global anonymous space.

foo.cpp

namespace MY_UNITY_ID {
    int i = 42;
}

int foo()
{
    return MY_UNITY_ID::i;
}

bar.cpp

namespace MY_UNITY_ID {
    int i = 42;
}

int bar()
{
    return MY_UNITY_ID::i;
}

Next, we need to make sure that after the code is merged, MY_ UNITY_ The definition of ID in foo and bar is completely different. A unique ID value can be calculated by file name, which does not conflict with each other, i.e. achieves the following merging effect:

#define MY_UNITY_ID <hash(foo.cpp)>
#include "foo.c"
#undef MY_UNITY_ID
#define MY_UNITY_ID <hash(bar.cpp)>
#include "bar.c"
#undef MY_UNITY_ID

This may seem like a hassle, but users don't need to care about it. xmake handles it automatically when merging. Users just need to specify the name of this Unique ID, for example:

target("test")
    set_kind("binary")
    add_includedirs("src")
    add_rules("c++.unity_build", {batchsize = 2, uniqueid = "MY_UNITY_ID"})
    add_files("src/*.c", "src/*.cpp")

This can be used to avoid conflicts when dealing with global variables, global duplicate macro definitions, functions or anything.

C++20 Modules

xmake uses.mpp as the default module extension, but also supports extensions such as.Ixx,.Cppm,.Mxx.

In the early days, xmake experimentally supported C++ Modules TS, but at that time, gcc was not very well supported and intermodule dependencies were not supported.

Recently, we have made a number of improvements to xmake, fully supporting C++20 Modules build support for gcc-11/clang/msvc, and automatically analyzing dependencies between modules to maximize parallel compilation.

The new version of clang/msvc is also better handled.

set_languages("c++20")
target("test")
    set_kind("binary")
    add_files("src/*.cpp", "src/*.mpp")

For more examples: C++ Modules

Lua5.4 Runtime Support

Last version, we added support for the Lua5.3 runtime, and in this version, we further upgraded the Lua runtime to 5.4, which offers significant performance and memory utilization improvements over 5.3.

However, the default runtime for xmake is still luajit, and it is expected that version 2.6.1 (i.e., the next version) will officially be cut to Lua5.4 as the default runtime.

Despite switching the Lua runtime, it is completely insensitive to the client and fully compatible with existing project configurations, as xmake originally provides a layer of encapsulation for exposed api s.
Interfaces that have compatibility issues with the lua version, such as setfenv, ffi, etc., are hidden internally and are not exposed to users.

Keil MDK Tool Chain Support

In this release, we also added support for Keil/MDK embedded compilation tool chains, related example projects: Example

xmake automatically detects compilers installed by Keil/MDK, related issues #1753.

Compile with armcc

$ xmake f -p cross -a cortex-m3 --toolchain=armcc -c
$ xmake

Compile with armclang

$ xmake f -p cross -a cortex-m3 --toolchain=armclang -c
$ xmake

Console Application

target("hello")
    add_deps("foo")
    add_rules("mdk.console")
    add_files("src/*.c", "src/*.s")
    add_defines("__EVAL", "__MICROLIB")
    add_includedirs("src/lib/cmsis")

Static library program

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

target("foo")
    add_rules("mdk.static")
    add_files("src/foo/*.c")

Wasi Tool Chain Support

Previously, we supported the wasm platform's EMCC tool chain to build wasm programs, and here we added another WASI-enabled Wasm tool chain to replace emcc.

$ xmake f -p wasm --toolchain=wasi
$ xmake

Circle Tool Chain Support

We have also added support for the circle compiler, a new C++20 compiler with some interesting compile-time metaprogramming features that interested students can view on the official website: https://www.circle-lang.org/

$ xmake f --toolchain=circle
$ xmake

gcc-8/9/10/11 specific version support

If the user has additional gcc-11, gcc-10 and other specific versions of the GCC tool chain installed, the local GCC program name may be/usr/bin/gcc-11.

One way to do this is to switch through the specified configurations one by one, xmake f--cc=g cc-11--cxx=g cc-11--ld=g++++ 11, which is cumbersome.

So xmake also provides a quicker way to switch:

$ xmake f --toolchain=gcc-11 -c
$ xmake

You can quickly switch the entire GCC tool chain by specifying the corresponding version name for gcc-11.

C++17/20 Compiler Feature Detection

xmake provides check_ Featuretures auxiliary interface to detect compiler characteristics.

includes("check_features.lua")

target("test")
    set_kind("binary")
    add_files("*.c")
    add_configfiles("config.h.in")
    configvar_check_features("HAS_CONSTEXPR", "cxx_constexpr")
    configvar_check_features("HAS_CONSEXPR_AND_STATIC_ASSERT", {"cxx_constexpr", "c_static_assert"}, {languages = "c++11"})

config.h.in

${define HAS_CONSTEXPR}
${define HAS_CONSEXPR_AND_STATIC_ASSERT}

config.h

/* #undef HAS_CONSTEXPR */
#define HAS_CONSEXPR_AND_STATIC_ASSERT 1

In version 2.5.9, we added c++17 feature detection:

Attribute Name
cxx_aggregate_bases
cxx_aligned_new
cxx_capture_star_this
cxx_constexpr
cxx_deduction_guides
cxx_enumerator_attributes
cxx_fold_expressions
cxx_guaranteed_copy_elision
cxx_hex_float
cxx_if_constexpr
cxx_inheriting_constructors
cxx_inline_variables
cxx_namespace_attributes
cxx_noexcept_function_type
cxx_nontype_template_args
cxx_nontype_template_parameter_auto
cxx_range_based_for
cxx_static_assert
cxx_structured_bindings
cxx_template_template_args
cxx_variadic_using

c++20 feature detection has also been added:

Attribute Name
cxx_aggregate_paren_init
cxx_char8_t
cxx_concepts
cxx_conditional_explicit
cxx_consteval
cxx_constexpr
cxx_constexpr_dynamic_alloc
cxx_constexpr_in_decltype
cxx_constinit
cxx_deduction_guides
cxx_designated_initializers
cxx_generic_lambdas
cxx_impl_coroutine
cxx_impl_destroying_delete
cxx_impl_three_way_comparison
cxx_init_captures
cxx_modules
cxx_nontype_template_args
cxx_using_enum

Xrepo Package Virtual Environment Management

Enter virtual environment

xmake comes with xrepo package management tools that now support package virtual machine environment management very well, similar to nixos nixpkgs.

We can customize some package configurations by adding the xmake.lua file to the current directory and then enter a specific package virtual environment.

add_requires("zlib 1.2.11")
add_requires("python 3.x", "luajit")
$ xrepo env shell
> python --version
> luajit --version

We can also configure xmake.lua to load corresponding tool chain environments, such as the compilation environment for loading vs.

set_toolchains("msvc")

Manage virtual environments

We can use the following command to register the specified virtual environment configuration globally to the system for fast switching.

$ xrepo env --add /tmp/base.lua

At this point, we have saved a global virtual environment called base, which we can view through the list command.

$ xrepo env --list
/Users/ruki/.xmake/envs:
  - base
envs(1) found!

We can also delete it.

$ xrepo env --remove base

Switching Global Virtual Environments

If we register multiple virtual environments, we can also switch them quickly.

$ xrepo env -b base shell
> python --version

Or load the specified virtual environment directly to run specific commands

$ xrepo env -b base python --version

Xrepo env-b/--bind is the binding of the specified virtual environment. For more details, see: #1762

Header Only Target Type

For target, we added the headeronly target type, which is a target program that we will not actually compile because it has no source files to compile.

However, it contains a list of header files, which are typically used for installation of headeronly library projects, generation of file lists for IDE projects, and generation of cmake/pkgconfig import files during installation.

For example:

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

target("foo")
    set_kind("headeronly")
    add_headerfiles("src/foo.h")
    add_rules("utils.install.cmake_importfiles")
    add_rules("utils.install.pkgconfig_importfiles")

For more details see: #1747

Find packages from CMake

Now cmake is the de facto standard, so find_provided by CMake Package has been able to find a large number of libraries and modules, and we have fully reused this part of cmake's ecosystem to extend xmake's package integration.

We can use find_package("cmake::xxx") to find some packages with cmake, xmake will automatically generate a cmake script to call cmake find_package to find some packages, get package information inside.

For example:

$ xmake l find_package cmake::ZLIB
{
  links = {
    "z"
  },
  includedirs = {
    "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
15.sdk/usr/include"
  },
  linkdirs = {
    "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
15.sdk/usr/lib"
  }
}
$ xmake l find_package cmake::LibXml2
{
  links = {
    "xml2"
  },
  includedirs = {
    "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/libxml2"
  },
  linkdirs = {
    "/usr/lib"
  }
}

Specified version

find_package("cmake::OpenCV", {required_version = "4.1.1"})

assignment component

find_package("cmake::Boost", {components = {"regex", "system"}})

Preset switch

find_package("cmake::Boost", {components = {"regex", "system"}, presets = {Boost_USE_STATIC_LIB = true}})
set(Boost_USE_STATIC_LIB ON) -- will be used in FindBoost.cmake
find_package(Boost REQUIRED COMPONENTS regex system)

Setting environment variables

find_package("cmake::OpenCV", {envs = {CMAKE_PREFIX_PATH = "xxx"}})

Specify custom FindFoo.cmake module script directory

mydir/cmake_modules/FindFoo.cmake

find_package("cmake::Foo", {moduledirs = "mydir/cmake_modules"})

Package Dependent Integration

package("xxx")
    on_fetch(function (package, opt)
         return package:find_package("cmake::xxx", opt)
    end)
package_end()

add_requires("xxx")

Package dependent integration (optional components)

package("boost")
    add_configs("regex",   { description = "Enable regex.", default = false, type = "boolean"})
    on_fetch(function (package, opt)
         opt.components = {}
         if package:config("regex") then
             table.insert(opt.components, "regex")
         end
         return package:find_package("cmake::Boost", opt)
    end)
package_end()

add_requires("boost", {configs = {regex = true}})

Related issues: #1632

Add custom commands to CMakelists.txt

We have further improved the cmake generator, and now we can serialize the custom scripts inside the rule into a list of commands, which are generated together into CMakelists.txt

At present, however, only batchcmds series scripts can be serialized.

rule("foo")
    after_buildcmd(function (target, batchcmds, opt)
        batchcmds:show("hello xmake!")
        batchcmds:cp("xmake.lua", "/tmp/")
        -- batchcmds:execv("echo", {"hello", "world!"})
        -- batchcmds:runv("echo", {"hello", "world!"})
    end)

target("test")
    set_kind("binary")
    add_rules("foo")
    add_files("src/*.c")

It will generate Makelists.txt similar to the following

# ...
add_custom_command(TARGET test
    POST_BUILD
    COMMAND echo hello xmake!
    VERBATIM
)
add_custom_command(TARGET test
    POST_BUILD
    COMMAND cp xmake.lua /tmp/
    VERBATIM
)
target_sources(test PRIVATE
    src/main.c
)

But cmake's ADD_ CUSTOM_ COMMAND PRE_ The actual effect of BUILD on different generators is quite different and cannot meet our needs, so we have done a lot to support it.

Related issues: #1735

Improved installation support for NixOS

We have also improved the get.sh installation script to better support nixOS.

Update Content

New features

  • #1736 : Support wasi-sdk tool chain
  • Support for Lua 5.4 Runtime
  • Add gcc-8, gcc-9, gcc-10, gcc-11 tool chains
  • #1623 : Supports find_package Finds Packages from cmake
  • #1747 : Add set_kind("headeronly") better handles installation of headeronly Libraries
  • #1019 : Support Unity build
  • #1438 : Add xmake l cli.amalgamate command to support code merge
  • #1765 : nim language support
  • #1762 : Manage and switch specified environment configurations for xrepo env
  • #1767 : Supports the Circle compiler
  • #1753 : armcc/armclang tool chain supporting Keil/MDK
  • #1774 : Add table.contains api
  • #1735 : Add custom commands to the cmake generator
  • #1781 : Improve get.sh installation script support for nixos

Improvement

  • #1528 : Detect c++17/20 features
  • #1729 Improving C++20 modules support for clang/gcc/msvc, supporting intermodule dependency compilation and parallel optimization
  • #1779 : Improve ml.exe/x86 to remove the built-in-Gd option

Posted by levidyllan on Sun, 31 Oct 2021 12:54:13 -0700