>-
xmake is a lightweight and modern c/c++ project building tool based on Lua. It's 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 maintain and generate multiple target files in a project, and how to set dependencies between them.
What is target?
In xmake's concept definition, an independent project may have multiple sub-projects organized together. Each sub-project corresponds to only one unique target file that can be generated, such as: executable programs, static libraries, or dynamic libraries, etc.
Each sub-project mentioned here is what xmake calls a target, literally meaning target sub-project.
Therefore, for each sub-project, we can maintain it in xmake.lua by adding a new target, for example:
target("test1")
set_kind("binary")
add_files("src/test1/*.c")
target("test2")
set_kind("binary")
add_files("src/test2/*.c")Above we have defined two independent sub-project targets, which will generate two independent executable files when compiling.
Inheriting Global Settings from Root Scope
Let's not talk about dependencies between targets for now. If we have many common settings, and each target has to set them, it would be very redundant and difficult to maintain.
Therefore, we can move these configurations outside the target scope, that is, set them in the root scope, so that they will take effect for all targets in the current xmake.lua and all sub-xmake.lua files, for example:
add_links("tbox")
add_linkdirs("lib")
add_includedirs("include")
target("test1")
set_kind("binary")
add_files("src/test1/*.c")
target("test2")
set_kind("binary")
add_files("src/test2/*.c")For example, if both targets need to link the tbox library, placing it in the outer root scope setting, both test1 and test2 can add the corresponding links.
Dependency Settings Between Targets
So if a target needs to use a static library generated by another target, how should it be configured?
One way is to manually specify the directory where the library generated by the corresponding target is located through add_linkdirs and add_links, and then add the link.
target("foo")
set_kind("static")
add_files("foo/*.c")
add_defines("FOO")
target("test1")
set_kind("binary")
add_includedirs("foo/inc")
add_links("foo")
add_linkdirs("$(buildir)")
add_files("test1/*.c")
add_defines("FOO")
target("test2")
set_kind("binary")
add_includedirs("foo/inc")
add_links("foo")
add_linkdirs("$(buildir)")
add_files("test2/*.c")
add_defines("FOO")In the above configuration, both test1 and test2 will use the libfoo library, and need to obtain the header file path, library path, and link of the libfoo library. In addition, they need to set the -DFOO macro definition switch during use.
It doesn't look like much, but there are actually two problems with writing it this way:
- There is a compilation order dependency between the test targets and the other two library targets. If test compiles first, it will prompt that the link library cannot be found.
- The configuration is too cumbersome and difficult to maintain. test1 and test2 have a lot of redundant configuration.
Is there a simpler and more reliable configuration method? Actually, we just need to use add_deps to configure dependencies between targets.
target("foo")
set_kind("static")
add_files("*.c")
add_defines("FOO", {public = true})
add_includedirs("foo/inc", {public = true})
target("test1")
set_kind("binary")
add_deps("foo")
add_files("test1/*.c")
target("test2")
set_kind("binary")
add_deps("foo")
add_files("test2/*.c")Comparing the configurations of test1 and test2, isn't it much more concise? Just through add_deps("foo"), it inherits all the exported settings of libfoo: linkdirs, links, includedirs, and defines.
The library generated by the target itself will automatically export link settings by default, and by setting the public attribute for includedirs and defines, we also mark them as exported, so they can be inherited by the test target.
And now, with the dependency relationship, xmake will automatically handle the compilation order between these targets during compilation, ensuring that the libfoo library has been generated when linking.
Further Analysis of Dependency Inheritance
Cascading Dependency Inheritance
According to what was said above, targets will automatically inherit configurations and properties from dependent targets, and there is no need to call interfaces like add_links, add_linkdirs, and add_rpathdirs to associate dependent targets.
And the inheritance relationship supports cascading, for example:
target("library1")
set_kind("static")
add_files("*.c")
add_includedirs("inc") -- Default private header file directory will not be inherited
add_includedirs("inc1", {public = true}) -- The header file related directory here will also be inherited
target("library2")
set_kind("static")
add_deps("library1")
add_files("*.c")
target("test")
set_kind("binary")
add_deps("library2")In the above configuration, test depends on library2, and library2 depends on library1. Then by adding only the library2 dependency through add_deps, test can completely inherit all exported settings on the entire dependency chain.
Disabling Default Inheritance Behavior
What if we don't want to inherit any configuration from dependent targets? How do we do it?
add_deps("dep1", "dep2", {inherit = false})By explicitly setting the inherit configuration, we tell xmake whether the configurations of these two dependencies need to be inherited. If not set, inheritance is enabled by default.
Detailed Explanation of Inheritable Export Properties
Above, we also set public to true through add_includedirs("inc1", {public = true}), making the includedirs setting public for other dependent sub-targets to inherit.
Currently, for target compilation and link flags related interface settings, inheritance properties are supported, and you can manually control whether they need to be exported to other targets for dependency inheritance. The currently supported properties are:
| Property | Description |
|---|---|
| private | Default setting, as the current target's private configuration, will not be inherited by other dependent targets |
| public | Public configuration, both the current target and dependent sub-targets will be set |
| interface | Interface setting, only dependent sub-targets will inherit the setting, the current target does not participate |
This actually references cmake's design. Currently in xmake, all compilation and link setting interfaces related to targets support visibility export, such as: add_includedirs, add_defines, add_cflags, etc.
For more detailed information about this, please see: https://github.com/xmake-io/xmake/issues/368