Skip to content

A GNU Makefile system for C-family and Java projects

License

Notifications You must be signed in to change notification settings

trflynn89/flymake

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flymake

Azure DevOps

flymake is a parallel, non-recursive GNU Makefile system for C-family and Java projects. The goal is to provide a system that is simple to use, requires minimal boilerplate, and produces fast builds.

Installation

Installing flymake may be done from source or from a stable release package.

To install from source:

make install

To install from a release package, download the latest release and extract the downloaded archive in the root system directory:

tar -C / -xjf flymake-[version].tar.bz2

By default, both of the above methods will install flymake into /usr/local/src/fly/. It will also install an uninstallation script into /usr/local/bin/uninstall_flymake. This location may be overridden when installing from source:

make INSTALL_ROOT=$HOME install

This will install flymake into your home directory (~/src/fly/ and ~/bin/uninstall_flymake).

Supported languages

The following languages and file extensions are supported by flymake:

Language Supported File Extensions
C .c
C++ .cc, .cpp
Objective-C .m
Objective-C++ .mm
Java .java

Note: header file extensions aren't important for compilation, but for some secondary make goals (see Make goals), the following header extensions are supported: .h, .hh, .hpp.

Usage

In general, using flymake is as simple as defining the targets you want to build and the paths to the source files for those targets.

The only required files are a Makefile, which may exist anywhere in your project, and individual files.mk files in the source directories included by the Makefile. The Makefile must define the following two variables:

  • SOURCE_ROOT - The path to the the root directory of the project. All targets should fall under this path. This path will be added to the include path for all targets.
  • VERSION - The current version of the project, using semantic versioning.

The Makefile should then import the flymake API for defining targets, api.mk. Use the ADD_TARGET function to define the targets for your project. Usage of ADD_TARGET is as follows:

$(eval $(call ADD_TARGET, [target name], [target path], [target type], [target dependencies]))
  • Target name - A unique identifier for the target. This is used to generate the names for the output files created during the build, depending on the target type.
  • Target path - The path, relative to SOURCE_ROOT, to the root directory containing the source files for this target.
  • Target type - One of BIN, LIB, JAR, or TEST:
    • BIN - The target is an executable binary compiled from C-family sources. The executable's name will be the target name.
    • LIB - The target is a library compiled from C-family sources. Both static and shared libraries will be created.
    • JAR - The target is an executable JAR file compiled from Java sources.
    • TEST - An alias for BIN for unit testing targets.
  • Target dependencies (optional) - If this target depends on other, previously defined targets in the Makefile, list those target names here (space-separated if multiple). Dependencies are currently only supported for BIN and LIB targets. For BIN targets, dependencies must all be LIB or SCRIPT targets. For LIB targets, dependencies must all be SCRIPT targets. Library dependencies will automatically be linked into the BIN target, and any sources generated by script dependencies will automatically be compiled into the BIN or LIB target (see Script targets).

With all targets defined, the last step in the Makefile is to import the flymake build system, build.mk. This will generate all of the Make goals required to build the defined targets, as well as goals to e.g. run unit tests, generate code coverage reports, etc. (see Make goals).

Each of the target paths provided to an ADD_TARGET invocation must contain a files.mk to define subdirectories to include and specific source files to build. The following variables may be used to define these:

  • SRC_DIRS_$(d) - The list of subdirectories (relative to d) to build.
  • SRC_$(d) - The list of source files (in this directory) to build.

Note the variable d used here. This is a special variable that is defined just before each files.mk file is included. It is the path, prefixed with SOURCE_ROOT, to the directory of the current files.mk. This variable exists because flymake is a non-recursive build system (meaning the entire build for all targets occurs in one Make process). Thus, this variable is used whenever a variable might be defined in more than one files.mk file to avoid naming conflicts.

Script targets

flymake supports execution of scripts at build time, which may optionally generate a set of source files when executed. Similar to ADD_TARGET, an ADD_SCRIPT function is provided by api.mk to define a script target. Usage of ADD_SCRIPT is as follows:

$(eval $(call ADD_SCRIPT, [target name], [script path], [script arguments], [generated sources], [target dependencies]))
  • Target name - A unique identifier for the target.
  • Script path - The path, relative to SOURCE_ROOT, to the script to be executed.
  • Script arguments (optional) - Space-separated arguments to pass to the script1.
  • Generated sources (optional) - The source files generated by the script relative to the generated source directory. All generated source files must be unique.
  • Target dependencies (optional) - If this target depends on other, previously defined targets in the Makefile, list those target names here (space-separated if multiple). Dependencies must all be other SCRIPT targets.

1 Every script will be provided two directories as positional arguments, both of which are under the Build artifact directory (see Build artifacts). The first will be the gen directory, a build-configuration-specific directory to place generated source files. The second will be the data directory, a configuration-independent location to place downloaded files. If additional arguments are specified, they will be provided as-is after those directories.

The script itself must be executable. No inference on the type of script (Bash, Python, etc.) is performed by flymake. Instead, a shebang directive should be added to the script.

Simple C-family example

For example, if the layout of your project is as follows:

├── build/
│   └── Makefile
├── lib/
│   ├── files.mk
│   ├── foo.hpp
│   ├── foo.cpp
│   ├── bar.hpp
│   └── bar.cpp
├── bin/
│   ├── files.mk
│   └── main.cpp
└── test/
    ├── files.mk
    ├── main.cpp
    ├── foo.cpp
    └── bar.cpp

The Makefile may be:

SOURCE_ROOT := $(CURDIR)/..
VERSION := 1.0.0

include /usr/local/src/fly/api.mk

$(eval $(call ADD_TARGET, main_library, lib, LIB))
$(eval $(call ADD_TARGET, main_executable, bin, BIN, main_library))
$(eval $(call ADD_TARGET, unit_tests, test, TEST, main_library))

include /usr/local/src/fly/build.mk

The files.mk should then simply define the source files in each directory:

lib/files.mk: (here, d = /path/to/lib)

SRC_$(d) := $(d)/foo.cpp $(d)/bar.cpp

bin/files.mk: (here, d = /path/to/bin)

SRC_$(d) := $(d)/main.cpp

test/files.mk: (here, d = /path/to/test)

SRC_$(d) := $(d)/main.cpp $(d)/foo.cpp $(d)/bar.cpp

That's it! You can run make -C build to build the entire project, or selectively build individual targets with e.g. make -C build main_library.

More realistic C-family example

The above example contains a rather flat directory structure - all targets are self-contained and do not have nested subdirectories. If your project is large, or has platform-specific files that shouldn't always be compiled, a bit more setup is required.

For example, if the layout of your project is as follows:

├── build/
│   └── Makefile
├── lib/
│   ├── files.mk
│   ├── lib.hpp
│   ├── lib.cpp
│   ├── foo/
│   │   ├── files.mk
│   │   ├── foo.hpp
│   │   └── foo.cpp
│   └── bar/
│       ├── files.mk
│       ├── bar.hpp
│       ├── bar_linux.cpp
│       └── bar_windows.cpp
├── bin/
│   ├── files.mk
│   └── main.cpp
└── test/
    ├── files.mk
    ├── main.cpp
    ├── foo.cpp
    └── bar.cpp

The main Makefile from the simple example does not change. However, this example adds subdirectories to lib with their own source files, some of which maybe should not be included in the build of main_library.

The files.mk files should be written as follows to handle these nuances:

lib/files.mk: (here, d = /path/to/lib)

SRC_DIRS_$(d) := $(d)/foo $(d)/bar
SRC_$(d) := $(d)/lib.cpp

lib/foo/files.mk: (here, d = /path/to/lib/foo)

SRC_$(d) := $(d)/foo.cpp

lib/bar/files.mk: (here, d = /path/to/lib/bar)

SRC_$(d) := $(d)/bar_linux.cpp

Java example

Warning: Java support with flymake is currently experimental and rudimentary. It is subject to change at any time. Currently, only executable JAR files may be created.

Just like with C-family targets, Java targets use files.mk files to define variables required to build an executable JAR. SRC_DIRS_$(d) and SRC_$(d) have the same meaning for Java targets. Additionally, a Java target's files.mk file may contain:

  • MAIN_CLASS_$(d) - (Required) The application entry point for the executable JAR.
  • CLASS_PATH_$(d) - The paths to any third-party JARs or packages to reference for compilation.
  • RESOURCES_$(d) - The paths to any runtime resources to include in the executable JAR.

For example, if the layout of your project is as follows:

├── build/
│   └── Makefile
├── lib/com/third_party/library/
│   └── library.jar
└── src/main/
    ├── java/
    │   ├── files.mk
    │   └── com/project/example/
    │       └── App.java
    └── resources/images/
        └── logo.png

Where library.jar is a third-party library used by the Java target, and src/resources/images/ is a directory of resources to bundle in the executable JAR. The Makefile may be:

SOURCE_ROOT := $(CURDIR)/..
VERSION := 1.0.0

include /usr/local/src/fly/api.mk

$(eval $(call ADD_TARGET, jar_example, src/main/java, JAR))

include /usr/local/src/fly/build.mk

There is a single files.mk file under src/main/java to define the variables required for the jar_example target. It may contain:

SRC_DIRS_$(d) := $(d)/com/project/example
MAIN_CLASS_$(d) := com.project.example.App
CLASS_PATH_$(d) := $(d)/../../../lib/com/third_party/library/library.jar
RESOURCES_$(d) := $(d)/../resources/images

Make goals

As noted above, running make without specifying any Make goals will build all defined targets. Each target name is also defined as a Make goal for convenience.

The following primary goals are defined by flymake:

  • all - (Default) Build all targets defined in the Makfile.
  • clean - Remove the artifact directory for the current build configuration (see Build configuration).
  • tests - Run all unit tests defined in the Makefile with the TEST target type.
  • docs - Run Doxygen to generate source code documentation. Path to the Doxygen configuration file may be set via the doxyfile command line option (defaults to $(SOURCE_ROOT)/Doxyfile).
  • install - Extract any target release package created during the build in the file system root directory (see Release packages).

The following secondary goals are defined by flymake to aid in development:

  • commands - Create a clangd compliation database for the current build configuration.
  • coverage - Generate a code coverage report of the last unit test execution.
  • profile - Run all unit tests and generate a profile report of the unit test execution (see gcc's -pg flag).
  • style - Run clang-format on all source files under SOURCE_ROOT. Run with check=1 to verify that all source files are style compliant. Use formatter to override the path to the clang-format binary.

Build configuration

The following options may be specified to configure the build:

Option Accepted Values Default Value Description
output Any directory CURDIR Build artifact directory (see Build artifacts).
toolchain clang, gcc, none clang Compilation toolchain for C-family targets1.
mode debug, release, profile debug Compilation mode2.
arch x86, x64 Defaults to host architecture Compilation architecture, 32-bit or 64-bit.
strict 0, 1, 2 2 Compiler warning level3.
symbols Any valid -g option 1 Debug symbol level, passed directly to -g.
cstandard Any valid -std option c2x The language standard to use for C files, passed directly to -std.
cxxstandard Any valid -std option c++2a The language standard to use for C++ files, passed directly to -std.
linker Any valid -fuse-ld option lld on Linux, ld on macOS The linker to use for creating executables, passed directly to -fuse-ld.
sanitize Any valid -fsanitize option None The sanitizers to enable, passed directly to -fsanitize4.
coverage 0, 1 0 Enable code coverage instrumentation.
cacher See description ccache, if available Enable use of a compilation cache5.
stylized 0, 1 1 Enable pretty build output.
verbose 0, 1 0 Enable verbose build output.

These options make be specified either via the command line (e.g. make mode=release), or by setting them in the main Makefile before importing build.mk. The latter allows for changing the defaults for the project.

1 flymake supports GCC and Clang toolchains and will use Clang by default. Other toolchains have not been tested, but may be used by setting toolchain=none. If this is set, you should also define the following:

  • CC - Compiler for C files.
  • CXX - Compiler for C++ files.
  • AR - Archive tool for creating static libraries.
  • STRIP - Strip tool for discarding symbols from build artifacts.

2 Compilation mode changes the build flags used to build source files:

  • debug - Debugging symbols are added to compiled sources.
  • release - Builds are optimized and all debugging information is removed.
  • profile - Builds are optimized and profiling symbols are added for generation of profile reports. Currently only supported if the toolchain is gcc.

3 By default, flymake enables a strict set of compiler warnings. This may not be desired for all projects, so the warning level may be globablly reduced or disabled. For locally amending warning flags, see Advanced build configuration. The warning levels are:

  • 0 - Disable all warnings.
  • 1 - Enable -Wall -Wextra -Werror.
  • 2 - Enable -pedantic and more. See flags.mk for all warnings that are set.

4 By default, flymake will not enable any sanitizers. If you would like to use a sanitizer, set sanitize to the relevant -fsanitize options (e.g. -fsanitize=address or -fsanitize=address,undefined).

5 By default, if ccache is installed and available on the system $PATH, flymake will use ccache as the compilation cache. If you would like to override this, set cacher to the cache binary to use, or an empty value to disable caching entirely.

Build artifacts

All build artifacts are created under a hierarchy of subdirectories under the directory specified by the output option (which defaults to the directory of the main Makefile). That hierarchy is controlled by other build configuration options.

  • C-family targets - The artifacts will appear in the path $(output)/$(mode)/$(toolchain)/$(arch); with the default options listed above, the default path will be $(CURDIR)/debug/clang/x64 on 64-bit hosts. The following subdirectories will be created as needed by the build:

    • bin - Contains executable files created for BIN and TEST targets.
    • lib - Contains static and shared library files created for LIB targets.
    • gen - Contains generated source files created by script targets.
    • obj - Contains intermediate object (.o) and dependency (.d) files compiled from source files.
    • etc - Contains any extra files created during the build or by one of the secondary Make goals, such as code coverage and profile reports. Also contains any release package created during the build (see Release packages).
  • Java targets - The artifacts will appear in the path $(output)/$(mode)/javac; with the default options listed above, the default path will be $(CURDIR)/debug/javac. The following subdirectories will be created as needed by the build:

    • bin - Contains executable JAR files created for JAR targets.
    • classes - Contains intermediate class (.class) files compiled from source files. Class files from any third-party JARs or packages specified via CLASS_PATH_$(d) are also extracted/copied here.

Further, the following subdirectories are defined regardless of the target type:

* `data` - Located at `$(output)/data`, this is provided for script targets to have a location
  to place downloaded / generated data files that are independent of build configuration.

Release packages

flymake supports creating a release package bundled as a .tar.bz2 containing any desired build artifacts or source files. By default, the package will install files under /usr/local/, but this is configurable by setting INSTALL_ROOT on the make command line.

A set of APIs is available to each target's files.mk file for creating a release package:

$(eval $(call ADD_REL_BIN))
$(eval $(call ADD_REL_LIB))
$(eval $(call ADD_REL_INC, [directory], [header file extension], [header file destination]))
$(eval $(call ADD_REL_SRC, [directory], [source file extension], [source file destination]))
  • ADD_REL_BIN - Add the executable file created for BIN targets to the release package. Executable files will be installed to $(INSTALL_ROOT)/bin/.
  • ADD_REL_LIB - Add the static and shared library files created for LIB targets to the release package. Library files will be installed to $(INSTALL_ROOT)/lib/.
  • ADD_REL_INC - Add header files from the project to the release package. Requires specifying the path to the directory containing the header files, the extension of the header files to bundle, and the subdirectory under the installation directory to place the header files. Header files will be installed to $(INSTALL_ROOT)/include/$(specified subdirectory).
  • ADD_REL_SRC - Add source files from the project to the release package. Requires specifying the path to the directory containing the source files, the extension of the source files to bundle, and the subdirectory under the installation directory to place the source files. Source files will be installed to $(INSTALL_ROOT)/src/$(specified subdirectory).

If any of the above APIs are used by a target, the release package is created after the target is built. It may be installed manually or by the install goal. There will also be an uninstallation script created into $(INSTALL_ROOT)/bin/uninstall_$(target name) to remove the installed files.

Advanced build configuration

Each files.mk file may specify compiler and linker flags to be applied to files in that directory. The following variables may be defined:

  • CFLAGS_$(d) - Compiler flags for C and Objective-C files.
  • CXXFLAGS_$(d) - Compiler flags for C++ and Objective-C++ files.
  • LDFLAGS_$(d) - Linker flags for C-family targets.
  • LDLIBS_$(d) - Libraries to link for C-family targets.

These variables are defaulted to the values of the directory which included this directory via SRC_DIRS_$(d). Thus, these variables should generally be treated as append-only (i.e. modified with +=). But this inheritance may be avoided by assigning instead (:=).

The reason for this inheritance is the target-level files.mk file may, for example, add a third-party library to the include path via the -I flag. Inheritance of these flags means that each subdirectory does not also need to update the include path.

The resulting flags used when compiling and linking files in a directory are the global flags defined in flags.mk followed by any of the per-directory variants listed above.