Makd is a GNU Make library/framework based on Makeit, adapted to D. It combines the power of Make and rdmd to provide a lot of free functionality, like implicit rules to compile binaries (only when necessary), tracking if any of the source files changed, it improves considerably Make's output, it provides a default test target that runs unittests and arbitrary integration tests, it detects if you change the compilation flags and recompile if necessary, etc.
MakD complies with Neptune for versioning.
- Major branch development period: 3 months
- Maintained minor versions: most recent
Major | Initial release date | Supported until |
---|---|---|
v2.x.x | v2.0.0: 2017-09-08 | 2020-12-31 |
v3.x.x | v3.0.0: 2020-11-02 | TBD |
Contents
First of all, is important to clarify that Makd do some assumptions on your
project's layout. All sources files should be located in src/
on the root
of the project (you can override this by overriding the $(SRC)
variable
though). This is the bare minimum you have to know, but there are a few more
conventions (for example, integration tests should go to
$(INTEGRATIONTEST)/
, integrationtest/
by default), they will be
explained when explaining the features that rely on them.
To get started you need to have makd as a submodule (or copy it to your project) and create a top-level makefile for your project (or convert the old one).
A typical Top-level Makefile
should look like this:
# Include the top-level makefile
include submodules/makd/Makd.mak
Assuming your makd installation is in submodules/makd
. By default, the
default target when typing just make
is all
, and you can add targets to
it, which will be explained later.
You can change this default target by explicitly overriding the
.DEFAULT_GOAL
variable, which tells GNU Make which target should be built
when you just run make
without arguments. If you set it, make sure you
define it after including Makd.mak
, order is important in this case:
# Default goal for building this directory
.DEFAULT_GOAL := some-target
This Makefile
file should be written only once and never touched again (most
likely). But in your project you might have more than one Makefile, for example
you could have one in your src
directory and another one in your test
directory, so you can do make
in src
without specifying -C ..
. Also,
probably your .DEFAULT_GOAL
in the src/Makefile
will be all
while
the one in test/Makefile
can be test
instead.
This is the file where you define what your Makefile
will actually do. Makd
does a lot for you, so this file is usually very terse. To define a binary to
compile, all you need to write in your Build.mak
is this:
$B/someapp: $C/src/main/someapp.d
That's it, this is the bare minimum you need. With this you can now write
make $PWD/build/devel/bin/someapp
and you should get your binary there (why
build/devel/bin
will be explained later in the next section). $B
is
a special variable holding the path where your binaries will be stored, and
$C
is a special variable storing the current path (the path where the
current Build.mak
is, not the directory where make
was invoked). Both
are absolute paths, to enable Makd to support building the project from
different locations (to make this work you should refer to all the project
files using this $C/
prefix when you refer to the current directory of
your Build.mak
).
Usually you want a shortcut to type less, so you might want to add:
.PHONY: someapp
someapp: $B/someapp
Now you can simply write make someapp
to build it. Simple.
But maybe you want to type just make
. Since the .DEFAULT_GOAL
defined in
your Makefile
is all
, you can use the special all
variable to add
targets to build when is called:
all += someapp
Now you can simply write make
and you'll get your program built.
Putting it all together, your file should look like:
.PHONY: someapp
someapp: $B/someapp
$B/someapp: $C/src/main/someapp.d
all += someapp
Makd has a lot of configuration variables available. This file lives in the
top-level directory of the project and serves as a global configuration point.
There is only one Config.mak
per project, so the configuration defined here
should make sense for all the Makefile
s defined across the project. For
example you could redefine the colors used here, or the default DMD binary to
use. This is why this file, when present, should be always added to the version
control system. But normally you shouldn't need to create this file.
This file (and Config.local.mak) should only define variables, as it's parsed
before any other variables or functions are defined. All the predefined variable
and functions available in Build.mak are not available here, except for
$F
, $T
and $R
, so use with care (see Predefined variables for
details).
This is a local (personal) version of the Config.mak, so users can customize the build system to their taste. Here is where you usually should define which Flavors to compile by default, or which colors to use, or the path to a non-conventional compiler location. This file should never be added to the version control system.
This file is loaded after Config.mak so it overrides its values.
Everything built by Makd is left in the build
directory (or the directory
specified in BUILD_DIR_NAME
variable if you defined it). In the build
directory you can find these other directories and files:
<flavor>
- Makd support Flavors (also called variants), by default flags are
provided for the devel and the prod flavors. All the symbols
produced by the devel variant (the default) for example, will live in
the
devel
subdirectory in the build directory. last
- This is a symbolic link to the latest flavor that has been built. Is
useful to use by script, where you do
make
but you don't know the name of the default flavor. Then you can just access tobuild/last
. doc
- Generated documentation is put in this directory. Flavors shouldn't
affect how the documentation is built, so there is only one
doc
directory.
Each flavor directory have a set of files and directories of its own:
bin
- This is where the generated binaries are left.
tmp
- This is where object files, dependencies files and any other temporary
file is left. Usually after a build all the contents of this directory
is trash and only works as a cache. If you remove this directory a new
build will be triggered next time you run make though, even if nothing
changed. The project directory structure is replicated inside this
directory, except for the directories specified by the
BUILD_DIR_EXCLUDE
variable (by default the build directory itself, the.git
directory and the submodule directories). pkg
- Generated packages are built in this directory. You can change this via
the
P
variable. build-d-flags
- A signature file to keep track of building flags changes.
Once you have the basic setup done, you can already enjoy a lot of small cool features. For example you get a nice, terse and colorful output, for example:
mkversion src/Version.d rdmd build/devel/bin/someapp
If there are any errors, messages will appear in red so they are easier to spot.
If you like the good old make verbose output, just use make V=1
and you'll
get everything. If you don't like colors, just use make COLOR=
. Makd also
honours Make options --silent
, --quiet
and -s
. So if you want to
avoid all output, just use make -s
as usual.
All these variables can be configured in your Config.local.mak if you want to always have it verbose or whatever.
If you want to force a build there is also the not-so-known make -B
, there
is no need to use the built-in make clean
target and destroy all your cache
(with all the other Flavors you compiled in the past).
By default the devel
flavor is compiled, but you can compile the
prod
flavor by using make F=prod
.
Also, if you have several cores, use make -j2
and enjoy of Make's
parallelism for free! (this will use 2 cores, you can use -j3
for 3 and so
on).
If you want to build as much as possible without stopping, you can also use
make -k
(for --keep-going
) so Make doesn't stop on the first error.
This is particularly useful for Testing, if you want to find out how many tests
are broken without fixing everything first.
Finally, if you want to speed things up a little bit, you can use make -r
,
which suppress the many Make predefined rules, which we don't use and sometime
makes Make evaluate more options than needed.
Of course you can combine many Makd and Make options, and specify more than one target, for example:
make -Brj4 F=prod V=1 COLOR= all test
So, we already shown you can use a couple of built-in predefined targets. The whole set of predefined targets are:
all
clean
test
fasttest
unittest
allunittest
fastunittest
integrationtest
example
example-run
doc
pkg
graph-deps
Not all of them will be useful out of the box, you need to assign other targets
to them to be useful. In this category are: all
and doc
. For all
we
already saw how to feed it, just add targets to the predefined variable with
the same name (all += sometarget
). All those special target behaves the
same.
The built-in *unittest
target will compile and run the unittests in every
.d
file found in the $(SRC)
directory. The integrationtest
target
will compile and run every test program in $(INTEGRATIONTEST)/
. The
test
target includes the allunittest
and integrationtest
targets by
default, but you can add more by using the test
special variable (test +=
mytest
). The fasttest
target will only run the fastunittest
target by
default, but you can add more too by using the fasttest
special variable.
See the Testing section for more details.
The example
target will compile example programs found in
$(EXAMPLE)/*/*.d
(example/
by default) in a similar fashion to what
integrationtest
does. An example can be skipped by adding the file to the
$(EXAMPLE_FILTER_OUT)
variable. Check Skipping tests section for
a similar example of filtering out files. Examples are built as part of the
test
target by default, but they are not ran, as they could expect user
input or have other limitations that might not be suitable for general testing.
An example-run
target is provided, though, in case you want to run all
examples manually. You can use the EXAMPLEFLAGS
variable to pass custom
arguments to the example programs when running them, look at the Adding
specific flags section for details on how to use EXAMPLEFLAGS
as you
would do with UTFLAGS
.
The pkg
target builds all packages defined in $P
, see Packaging
section for more details.
The clean
target simply removes The build directory recursively. Just
remember to put all your generated files there and the clean target will always
work ;). If you can't do that (because you generated a source file for example),
you can use the special variable clean
too (clean += src/trash.d
src/garbage.d
for example).
The doc
target will, by default, call harbored-mod tool to generate the documentation
for the project from DDOC comments inside source files. Harbored-mod is
choosen because it also allows Markdown syntax which makes the documentation
easier to read in the source files, as it doesn't require as much DDOC macros
as the dmd.
The graph-deps
target is used to generate a dependencies graph. To generate
this graph the dot
tool from the graphviz
visualization software is used (the location of the tool can be specified via
the DOT
variable). By default only cyclic dependencies are generated in the
graph, but other kind of dependencies graphs can be generated (please take
a look at the ./graph-deps --help
ouput for details, you can override the
options to pass to graph-deps
using the GRAPH_DEPS_FLAGS
variables).
There are a lot of predefined variables provided by Makd, we've already seen
quite a few important ones (F
, COLOR
, V
for example).
Some of these variables are meant to be overridden and some are mean to be just
used (read-only), otherwise the library could break. Here we list a lot of them,
but always check the source Makd.mak
if you want to know them all!
The standard Make variable LDFLAGS
have a special treatment when used with
dmd
/rdmd
: the -L
is automatically prepended, so if you need to
specify libraries to link to, just use -lname
, not -L-lname
(same with
any other linker flag).
- The special target variables
all
,test
,doc
. - Color handling variables (
COLOR
* variables, please look at the Makd.mak source for details). F
to change the default Flavor to build.V
to change the default verboseness.BUILD_DIR_NAME
andBUILD_DIR_EXCLUDE
, but usually you shouldn't.P
is where built packages will be created. Defaults to$G/pkg
.- Program location variables:
DC
is the D compiler to use, you can build your project with a different DMD by usingmake DC=/usr/bin/experimental-dmd
for example. Same forRDMD
, andFPM
. - Less likely you might want to override the
DFLAGS
,RDMDFLAGS
orFPMFLAGS
, but usually there are better methods to do that instead. TEST_FILTER_OUT
to exclude some files from the unit tests or integration tests.EXAMPLE_FILTER_OUT
to exclude some examples from being built with theexample
andtest
targets.TEST_RUNNER_MODULE
andTEST_RUNNER_STRING
are used to override the module or string to inject in the unittest file that runs all the unit tests. See Testing for details.INTEGRATIONTEST
to change the default location of integration tests (integrationtest
by default).EXAMPLE
to change the default location of the example programs (example/
by default).SRC
is where all the source files of your project is expected to be. By default issrc
but you can override it with.
if you keep the source file in the top-level. The path must be relative to the project's top-level directory. It's using mainly to search for unittests.PKG
is where package definitions are searched. When building packages, each*.pkg
file in that directory will be built. By default$T/pkg
.PKG_DEFAULTS
contains the default options passed tomkpkg
.PKG_FILES
contains the list of packages definitions.PKG_PREBUILD
hold commands to run previous to build packages.PROJECT_NAME
contains the name of the project, used in documentation generatation. It defaults to the name of the top directory.VERSION_FILE
is the location where to write a D module storing detailed information on the Git version and build information (like person who did the build, date, etc.). If this file shouldn't be generated at all, you can set this variable to be empty. By default it$(GS)/Version.d
.VERSION
is the version to be used when creating documentation. It's obtained via themkversion.sh
by default.PKGVERSION
is the version to be used when creating packages. It's obtained via theVERSION
variable by default.PKGITERATION
optionally allows setting the package iteration (forwarded to fpm as --iteration, known as the debian_revision [in Debian](https://www.debian.org/doc/debian-policy/#version)).PRE_BUILD_D
andPOST_BUILD_D
hold scripts executed before and after running the command to build D targets (when using thebuild_d
function). By default they are used to generate theVersion.d
file, but users can override it not to generate the file or do something else on top of that.COV
will compile and run tests with coverage support if is set to1
. Please see Coverage for details.COVDIR
specifies the directory where to store coverage reports (by default$O/cov
. Please see Coverage for details.COVMERGE
indicates if coverage reports should be merged (1
will merge,0
will not). Please see Coverage for details.
Some of this variables are typically overridden in the Config.mak file, others in the Build.mak file, others in the Config.local.mak or directly in the command line (like the style stuff).
Probably the most important read-only variables are the ones related to generated objects locations:
T
is the project's top-level directory (retrieved from git).R
is the current directory relatively to$T
.C
is the directory where the current Build.mak is (which might not be the same as the Make predefined variableCURDIR
). You should always use this variable to refer to local project files.G
is the base generated files directory, taking into account the flavor (for examplebuild/devel
).O
is the objects/temporary directory (for examplebuild/devel/tmp
).B
is the generated binaries directory (for examplebuild/devel/bin
).D
is the generated documentation directory (for examplebuild/doc
).GS
is the temporary where generated sources are stored, so that-I$(GC)
is added to the compiler (for examplebuild/devel/include
).
All these variables except for R
are absolute paths. This is to work
properly when run in different directories. You should take that into account.
Sometimes is good to be able to have some information about the environment provided by Makd. For this purpose, the following variables are exported:
MAKD_TOPDIR
: project's top directory as seen by Makd.MAKD_PATH
: directory where theMakd.mak
file lives.MAKD_TMPDIR
: temporary directory inside the build directory that can be used for temporary stuff.MAKD_BINDIR
: directory where build binaries are stored.MAKD_FLAVOR
: flavor currently being built (usually eitherdevel
orprod
).MAKD_DVER
: D version used (usually either1
or2
).MAKD_VERBOSE
: indicates if Makd is running in verbose mode (V=1
). This is only considered false when empty, any other value means true.MAKD_COLOR
: indicates if Makd is running in color mode (COLOR=1
). This is only considered false when empty, any other value means true.
There are a few useful predefined functions you might want to know about. Only the most important (the ones you are most likely to use) are mentioned here, once again, please refer to the Makd.mak source if you want to see them all.
Probably the most important is exec
. This function takes care of the pretty
output and verboseness. Each time you write a custom rule (hopefully you won't
need to do this often), you should probably use it. Here is the function
signature:
$(call exec,command[,pretty_target[,pretty_command]])
command
is the command to execute, pretty_target
is the name that will
be printed as the target that's being build (by default is $@
, i.e. the
actual target being built), and pretty_command
is the string that will be
print as the command (by default the first word in command
).
Here is an example rule:
touch-file:
$(call exec,touch -m $@)
This will print:
touch touch-file
When built. And will print touch -m touch-file
if V=1
is used, as
expected.
When COLOR
is enabled, exec
will colorize the output based on the
COLOR_OUT
variable. For commands that use colors in the output themselves,
having MakD coloring the output will just make the output weird. Because of
this, there is this flavour of exec
that will not colorize the output (but
will still use colors, when enabled, for the fancy progress indication).
This is a convenient shortcut to write rules to build D programs. It will run
the PRE_BUILD_D
and POST_BUILD_D
and rdmd
for the actual build.
It takes 3 optional arguments:
- arguments to be passed to
BUILD.d
(usuallyrdmd
) - arguments to be passed to the
PRE_BUILD_D
script - arguments to be passed to the
POST_BUILD_D
script
This is a very simple function that just checks a certain Debian package is installed. The signature is:
$(call check_deb,package_name,required_version[,compare_op])
package_name
is, of course, the name of the package to check.
required_version
is the version number we require to build the project and
compare_op
is the comparison operator it should be used by the check (by
default is >=, but it can be any of <,<=,=,>=,>).
You can use this as the first command to run for a target action, for example:
myprogram: some-source.d
$(call check_deb,dstep,0.0.1)
rdmd --build --whatever.
If you need to share it for multiple targets you can just make a simple alias with a lazy variable:
check_dstep = $(call check_deb,dstep,0.0.1)
myprogram: some-source.d
$(check_dstep)
rdmd --build --whatever.
Wrapper around the find
command to avoid errors when the directory to
search doesn't exist at all. Use this to avoid spurious find errors.
It takes the directory/ies where to search as first arguments and conditions and other find options as the second argument.
Example:
files := $(call find $C/$(SRC),-name '*.di')
Find files and get the their file names relative to another directory.
Arguments are:
- The files suffix (
.h
or.cpp
for example). - A directory rewrite, the matched files will be rewriten to be in the
directory specified in this argument (it defaults to
$3
if omitted). - Where to search for the files (
$C
if omitted). - A
filter-out
pattern applied over the original file list (previous to the rewrite). It can be empty, which has no effect (nothing is filtered).
Example:
UNITTEST_FILES := $(call find_files,.d,,$C/$(SRC),$(TEST_FILTER_OUT))
This function converts a file path to a D module. It takes as first argument
a file path to convert and as optional second argument the base path of the
sources (path that is not part of the fully qualified module name), by defaul
$C/$(SRC)
. This function takes into account the special pkg/package.d
module name, converting it to just pkg
.
For example:
$(call file2module,project/x.d,project/) # -> x
$(call file2module,project/y/package.d,project/) # -> y
$(call file2module,./some/longer/pkg/package.d,./) # -> some.longer.pkg
$(call file2module,./some/longer/pkg/mod.d,./) # -> some.longer.pkg.mod
OK, this is not really a function, but you might use it in a way that can be
closer to a function than a variable. When we are in verbose mode, V
is
empty and when we are not in verbose mode is set to @
. The effect is you
only get some Make output if we are not in verbose mode.
For example, this:
test:
$Vecho test
If called via make test
will produce:
test
While if called via make V=1 test
, it will produce:
echo test test
This is only useful for commands you normally don't want to print, but you want
to be friendly to the user and show the command if verbose mode is used.
Normally you should always use $V
instead of @
.
Yes, is a bit confusing that $V
internally becomes empty when you use
V=1
, but when you use it is very natural :)
Flavors are just different ways to compile one project using different flags. By
default the devel
and prod
flavors are defined. The The build
directory stores one subdirectory for each flavor so you can compile one after
the other without mixing objects compiled for one with the other and your cache
doesn't get destroyed by a make clean
.
To change variables based on the flavor (or define new flavors), usually the Config.mak is the place, and you can use normal Make constructs, for example:
ifeq ($F,devel)
override DFLAGS += -debug=ProjectDebug
endif
ifeq ($F,prod)
override DFLAGS += -version=SuperOptimized
endif
Usually the override
option is needed, if you want to still add these
special flags even if the user passes a DFLAGS=-flag
to Make.
To compile the project using a particular flavor, just pass the F
variable
to make, for example:
make F=prod
If you need to define more flavors, you can do so by defining the
$(VALID_FLAVORS)
variable in your Config.mak
, for example:
VALID_FLAVORS := devel prod profiling
There is a not-so-known Make feature that makes it very easy to override variables for a particular target, and usually that's the best way to pass specific variables to a particular target.
For example, you need to link one binary to a particular library but not the others, then just do:
$B/prog-with-lib: override LDFLAGS += -lthelib
$B/prog-with-lib: $C/src/progwithlibs.d
$B/prog: $C/src/prog.d
Then LDFLAGS
will only include -lthelib
when the target
$B/prog-with-lib
is made, but not others. One catch about this is this
variable override is propagated, so if your target needs to build a prerequisite
first, the building of the prerequisite will also see the modified variable. If
you want to avoid this, Makd also expands the special variable
$([email protected]_FLAGS)
. That is $(<name of the target>.EXTRA_FLAGS)
(yes,
Make support recursive expansion of variables :D), for example:
$B/prog-with-lib.EXTRA_FLAGS := -lthelib
$B/prog: $C/src/prog.d
Will have a similar effect, but the variable expansion will only work for this particular target. This is a corner case and hopefully you won't need to use it.
Makd supports a simple facility to make packages based on fpm. A simple
wrapper program mkpkg
is provided to ease the creation of scripts that use
fpm to create packages.
The predefined pkg
target depends on the special variable $(pkg)
,
where you can add any extra target that must be built for pkg
.
By default every package file specified in $(PKG_FILES) will be
added to $(pkg)
, and by default $(PKG_FILES)
holds all
*.pkg
files in the $(PKG)
directory (by default $T/pkg
).
mkpkg
is invoked for every file specified in $(PKG_FILES)
.
These files are expected to be Python scripts. They have some pre-defined built-in variables, some of which the user is expected to fill and some of which are tools for the user to define packages.
These are the built-in variables that the user should fill:
OPTS
- a
dict()
(associative array) where each item will be mapped to a fpm command-line option. If the key is only one character (for examplec
), it will be passed as-<key><value>
and if it's more, it will be passed as--<key>=<value>
(_
characters in the key will be replaced by-
for convenience). The<value>
can beTrue
, a string or an array of strings. If it'sTrue
, just-<key>
or--<key>
is passed (without an actual value), if it's an array of strings, the key is used as fpm flag for each item in<value>
. No validation is performed over the keys or values, they are just passed blindly to fpm. ARGS
- a
list()
(array) to pass to fpm as positional arguments (usually the list of files to include in the package).
These variables should never be rebound (never assign to them like OPTS
= dict(...)
), you always need to update them instead (normally using
OPTS.update(...)
and ARGS.extend([...])
).
An extra built-in variable will be available, VAR
, containing variables
passed to the mkpkg
util. By default Makd passes the following variables:
shortname
- name of the package as calculated from the
.pkg
file. suffix
- a suffix to add to the package name to support installing multiple versions simultaneously (see Package suffix for details).
fullname
shortname
with thesuffix
appended to it for convenience.version
- package version number as defined by
PKGVERSION
. iteration
- package iteration as defined by
PKGITERATION
. builddir
- base build directory (
$G
). bindir
- directory where the built binaries are stored.
lsb_release
- Debian
lsb_release -uc
content (distribution name).
mkpkg
also defines the following built-in functions in the special built-in
variable FUN
:
autodeps(bin[, ...][, path=''])
- returns a sorted
list()
of packagesbin
depends on based on the outcome of running theldd
utility and searching to which packages the libraries is linked belong to usingdpkg
. You can specify multiple binaries to get a list of dependencies for all of them. This function is tightly coupled to Debian packages for now. If apath
is given, then all thebin
passed will be prepended with thispath
.bin
s can be passed as multiple arguments or as one list. mapfiles(src, dst, file[, ...][, append_suffix=True])
- A very simple function that just returns a list with
{src}/{file}={dst}/{file}{VAR.suffix}
for eachfile
passed.file
s can be passed as multiple arguments or as one list. A named argumentappend_suffix
can be passed at the end to control whetherVAR.suffix
is appended to each destination file.append_suffix
defaults toTrue
if not given. desc([type[, prolog[, epilog]]])
A simple function to customize
OPTS['description']
. It can add an optionaltype
of package (will append `` (<type>)`` to the first line (short description),prolog
(inserted before the long description) and anepilog
(appended at the end of the long description. To use only one of them, you can use Python's keyword arguments syntax. Examples:FUN.desc('common files', 'These are just config files', 'Part of whatever') # All specified FUN.desc(epilog='Just an epilog') FUN.desc('a type', epilog='And an epilog') FUN.desc(prolog='A prolog', epilog='And an epilog, but no type')
Note that
OPTS['description']
must be defined and hold a non-empty string.
One can exclude packages from being built under certain conditions
(e.g. based on compilation flags) by filtering PKG_FILES
:
ifeq ($(IS_RELEASE_MODE),1)
PKG_FILES := $(filter-out $(PKG)/NonReleaseApp.pkg,$(PKG_FILES))
endif
Generated packages will be stored in the $P
directory (by default
$G/pkg
. Since each package usually have a different name, as the version
usually changes with each change, all old packages are removed before making
new ones with the pkg
target and also generates a Debian changelog from
the git history (you can override this by re-defining the PKG_PREBUILD
variable).
The options to pass by default to mkpkg
are defined by the variable
PKG_DEFAULTS
, you can override it if the defaults are not suitable for you
projects. By default it builds Debian packages from files, a Debian changelog
is provided, and a version and iteration (using the Debian version).
Bear in mind that you should use lazy variables when overriding
PKG_DEFAULTS
and PKG_PREBUILD
if you want to use variables defined in
the pkg
target.
Please run mkpkg --help
if you want to know more about that utility.
For more details on how to create packages using fpm (thus, to know which
options you can define in OPTS
and what to pass as ARGS
) please refer
to the fpm wiki.
Since the package version is included in the file, is very complicated to have
the target really based on the package file name, because of this Makd uses
a stamp approach. The building of the package will be tracked via the special
file $O/pkg-%.stamp
file.
So when specifying dependencies (this target should depends on all files used to build the package), you should use this special file instead.
To make it easy to build test packages that can be installed in parallel with
the current packages, the variable PKG_SUFFIX
can be passed to make
when building the package (for example make pkg PKG_SUFFIX=-test
). This
will produce a package with name name-test
. Bear in mind the files will
conflict if the regular name
package and a suffixed package have the
same files. To avoid this problem, the {SUFFIX}
variable will be replaced
by the contents of the PKG_SUFFIX
variable. So the most common pattern is
to add the suffix to any non-configuration file in the package.
For convenience, here is a simple example:
$P/defaults.py
# This is a normal python module defining some defaults
OPTS.update(
description = '''\
Test package packing some daemon
This is an extended package description with multiple lines
This is a longer paragraph in the package description that
can span multiple lines.''',
url = 'https://github.com/sociomantic/makd',
maintainer = 'dunnhumby Germany GmbH <[email protected]>',
vendor = 'dunnhumby Germany GmbH',
)
$P/daemon.pkg
:
import defaults
bins = 'daemon admtool util1'
OPTS.update(
name = VAR.fullname,
category = 'net',
depends = FUN.autodeps(bins, path=VAR.bindir) + [
'bash',
'libnew' if VAR.lsb_release == 'trusty' else 'libold',
],
)
ARGS.extend(FUN.mapfiles(VAR.bindir, '/usr/sbin', bins) + [
'README.rst=/usr/share/doc/' + VAR.fullname '/',
])
$P/client.pkg
:
import defaults
bins = 'client clitool'
OPTS.update(
name = VAR.fullname,
description = FUN.desc('tools', epilog='These are just ' +
'utilities for the daemon package'),
category = 'net',
depends = FUN.autodeps(bins, path=VAR.bindir),
)
ARGS.extend(FUN.mapfiles(VAR.bindir, '/usr/bin', bins))
ARGS.extend(FUN.mapfiles('.', '/etc', 'util.conf', append_suffix=False))
Suppose that the targets daemon
and client
build the binaries
daemon
, admtool
, util1
and client
, clitool
respectively,
then you probably want to make sure you build those before making the package,
so in the Build.mak
file you should put something like:
$O/pkg-daemon.stamp: daemon
$O/pkg-client.stamp: util
With this configuration, a call to make pkg
will leave the built packages
in the $P
directory.
Makd supports testing generally by the special variables $(test)
and
$(fasttest)
. You can add any custom target to this variables to be executed
when you use the corresponding test
and fasttest
targets.
Automatic unittest and integration tests support is added on top of that.
If you have a test script, you can easily add the target to run that script to
$(test)
too (or $(fasttest))
and $(test)
if it's really fast).
For example:
.PHONY: supertest
supertest:
./super-test.sh
test += supertest
Then when you run make test
all the unittests, integration tests and your
test will run.
Only unittest that live in the directory specified by the $(SRC)
variable
and the $(INTEGRATIONTEST)
directory (see Integration tests) are built
and run automatically, the unittest
target will scan for all the files with
the .d
suffix there.
All the unit tests are built using these extra options:
-unittest -debug=UnitTest -version=UnitTest
There are two different categories of unittest though: fast and slow. Tests
are assumed to be fast unless they are separated to a different file, with the
suffix _slowtest.d
. Usually all the slow tests for module m
should be
moved to m_slowtest.d
, but this is just a convention.
The general unittest
target is just an alias for the more specific target
allunittest
and it will run all the unit tests (fast and slow). This target
is automatically added to the $(test)
special variable, so they will be run
when using the test
target too. On the other hand, the fastunittest
target will only run the fast unit tests, leaving the slow out, and is added to
the fasttest
target.
Unit tests are compiled in a separate binary that imports all modules in the
project. By default, this binary will just have an empty main()
function
and will let the D runtime to execute the tests by passing -unittest
.
If Ocean is present as
a submodule, then ocean.core.UnitTestRunner
will be imported instead.
If you want to import a custom module to run the unit tests, you can do so by
specifying the module via the TEST_RUNNER_MODULE
variable. If you do this,
no main()
function will be generated, so the module you are importing
should define it.
If you want to define a custom main()
function, or put any other content
into the file generated to run the unit tests (importing all modules), you can
define TEST_RUNNER_MODULE
as an empty variable and then put the contents
you want to add to the file in the TEST_RUNNER_STRING
variable.
Bear in mind that unless you exclude files with a main()
function (see
Skipping tests), you'll get link errors about having multiple definitions
for main()
. To avoid this issue you should version
out all the
main()
functions in your project (both to produce project binaries or to
produce Integration tests):
version (UnitTest) {} else
void main()
{
// stuff
}
Integration tests are expected to live in the $(INTEGRATIONTEST)
directory
(integrationtest
by default), and it is expected that each subdirectory
stores a separate test program with a main.d
file as the entry point. So
the typical layout for the $(INTEGRATIONTEST)/
directory is:
$(INTEGRATIONTEST)/ test_1/ main.d onemodule.d test_2/ main.d othermodule.d
The integrationtest
target scan for those individual programs (specifically
for files with the pattern: $(INTEGRATIONTEST)/*/main.d
) and builds them
and runs them.
It is also expected that the integration tests are slow, so by default they are
only added to the test
target, but you can manually add them (all or just
a few) to the fasttest
target too (fasttest += integrationtest
should be
enough to add them all).
The $(TEST_FILTER_OUT)
variable is used to exclude some tests. The contents
of this variable will always be applied to the list of files to use in the tests
through the Make $(filter-out)
function. This means you can use a single
%
as a wildcard. You should always use absolute paths (which can be easily
done by applying the prefix $C/
to files). Adding files to the
$(TEST_FILTER_OUT)
variable should be done in the Build.mak file. Always
use +=
, there might be other predefined modules to skip.
For Unit tests, you just have to add the individual files you want to exclude
from the tests. You can use a single %
as a wildcard to exclude a whole
package for example:
TEST_FILTER_OUT += \
$C/src/brokenmodule.d \
$C/src/brokenpackage/%
For Integration tests, you can only skip a full test program, to do that just
exclude the main.d
for that program. For example:
TEST_FILTER_OUT += $C/integrationtest/brokenprog/main.d
Some tests might need special flags for the unittest to compile, like when you need to link to external libraries.
For Unit tests you can add unittest specific flags by using the following syntax:
$O/%unittests: override LDFLAGS += -lglib-2.0
This will link all the unittests to the glib-2.0 library, both fastunittest
and allunittest
. To apply flags to an individual test use a more specific
target, for example:
$O/allunittests: override LDFLAGS += -lextra
This will link the extra library only to the full unit tests, but not to the fast ones.
If you want to run the tests using some special options of the unit test runner
(see build/last/*unittests -h
for a list of supported options), you can use
the special variable UTFLAGS
, for example:
make allunittest UTFLAGS="-v -s"
This will print all the executed tests and a summary at the end with the number of passed tests, failed tests, etc.
Some special options are passed automatically, for example if make -k
is
used, the -k
option will be passed to the unit test runner too, and if
make V=1
is used, the options -v -s
will be passed to the unit test
runner.
For Integration tests the way to pass special flags is similar, but not the same. Use the following syntax:
$O/test-feature: override LDFLAGS += -lglib-2.0
The targets for individual integration test programs are defined following this
pattern: $O/test-%
. The previous example will link the program at
$(INTEGRATIONTEST)/feature/main.d
against glib-2.0 as expected.
To pass flags to the test program execution, you can use the special variable
$(ITFLAGS)
. Unfortunately, unless you are running a specific integration
test, the only way to do this for individual suites is to write it in the makefile,
otherwise the same flags will be used to run all the integration tests.
To run the feature integration test with the flag --verbose
, for example,
you can do this (pay attention to the .stamp
suffix, it is necessary):
$O/test-feature.stamp: override ITFLAGS += --verbose
If you want to run all the integration test programs with the same flags, you can still use:
make integrationtest ITFLAGS=--verbose
Once you built and ran the unittests once, if you want, for some reason, repeat
the tests, you can just run the generated *unittests
and test-*
programs. All the programs are built in the build/last/tmp
directory ($O
more specifically).
A reason to run it again could be to use different command-line options (the
unit tests runner accepts a few, try build/last/tmp/allunittests -h
for
help). For example, if you want to re-run the tests, but without stopping on the
first failure, use:
build/last/tmp/allunittests -k
This option is used automatically if you run make -k
.
Remember to re-run make
if you change any sources, the test programs need to
be re-compiled in that case!
Compiling using code coverage can be done by passing COV=1
to make
. If
the D runtime supports the DRT_COV*
environment variables (see list of
version below), the coverage reports will be put in the $(COVDIR)
directory
($O/cov
by default), otherwise they will be written in the top-level
directory.
Also by default the the coverage reports will be merged. To change this you can
override the $(COVMERGE)
environment variable (1
to merge,
0
to overwrite). This also only works for the versions specified
below, previous versions will always overwrite the reports on each
run.
You should be careful about report merging, as unless you clean the reports
manually, they will be accumulated ad infinitum as there is no obvious point
where reports can be cleaned automatically (except for make clean
of
course). There is a convenience target to just clean coverage reports:
clean-cov
.
Merging and overrideable directory versions:
dmd
: v2.078.0+