Autark is a self-contained build system that requires only the /bin/sh and a cc compiler to work!
The goal of this project is to provide the community with a portable, cross-platform build environment that has no external dependencies and can be distributed directly with the project’s source code. It eliminates version compatibility issues common to traditional build systems, making software truly open and self-contained.
The build.sh script automatically initializes the Autark build system, and then proceeds with your project build. Autark handles both internal and external project dependencies much more precisely and cleanly than is typically done with Makefiles by hands. Build rules are defined using a specialized DSL in Autark files, which is mostly declarative in nature.
Key features of Autark
- To initialize the project build system on the target system, nothing is required except a C99-compliant compiler.
- The build process does not modify the project's source tree.
- Build rules are described using a simple and clear declarative DSL, which is not a programming language.
- The system provides extensive capabilities for extending the build process with custom rules.
- Parallel compilation of C/C++ source files is supported.
Installation
To use Autark in your project, simply copy the latest version of Autark:
The best way to get started is to look at the Autark sample project or explore real-life projects that use Autark.
Built artifacts are placed in ./autark-cache dir by default.
Brief overview of Autark
Autark build artifacts, as well as rules dependency metadata, are stored in a separate directory called the autark-cache. By default, this is the ./autark-cache directory at the root of your project, but it can be changed using the -H or --cache option.
The directory structure within the autark-cache mirrors the structure of your project source tree. For all programs executed during the build process, the current working directory is set to a corresponding location inside the autark-cache. This behavior can be overridden using the in-source directive. This approach reduces the risk of modifying original source files and makes it easier to access intermediate build artifacts during the various stages of the build pipeline.
Autark script is a specialized DSL with modest capabilities, yet sufficient for writing concise and elegant build scripts. The syntax is simple and can be informally described as follows:
- A script consists of a set of rules and literals.
- A rule follows this syntax:
- Rules and literals are separated by whitespaces.
- Every rule has a body enclosed in curly braces {}.
- Rules form lists, similar to syntactic structures in Scheme or Lisp.
More formal syntax decription can be found here: https://github.com/Softmotions/autark/blob/master/scriptx.leg
Sample Autark script
Below is a demonstration of an Autark script from the demo project: https://github.com/Softmotions/autark-sample-project Take a look and try building it!
You can also explore the following real-life C projects that use Autark:
https://github.com/Softmotions/autark-sample-project
Autark concepts and build lifecycle
Autark executes rules defined in Autark script files. The build process goes through the following stages: init, setup, build, and post_build.
For each stage, the rules specified in the scripts are executed sequentially, from top to bottom. However, during the build stage, some rules may depend on the results of other rules. In such cases, the execution order is adjusted to ensure that dependencies are resolved correctly.
The project build process goes through the following phases:
init
Initialization of the build process.
During the init phase, the following steps occur:
- Autark build script files are parsed.
- Initialization logic for rules is executed in sequence so it makes sence order of set, check, if in build script.
- Based on the current state of variables, tree shaking is applied to the syntax tree of the overall Autark build script. As a result, all conditional if rules and certain helper rules such as in-source and foreach are removed from the syntax tree completely.
setup
At this stage, the hierarchical structure of rules (the syntax tree) is finalized and will no longer change. This phase prepares the rules for the main build stage of the process. You could think of this phase as a pre-build step of main build phase.
build
This is the main phase of the project build process, where the actual work of the build rules is performed. During this phase, dependencies between rules are tracked and established, as well as dependencies on filesystem objects, project source files, environment variables, and more.
The execution order of rules is determined by their dependencies. A rule typically will not perform its main function if all of its dependencies have already been satisfied and the rule has been previously executed.
post-build
This phase is executed after the build phase has completed successfully. It is primarily used for rules that install the built project artifacts.
- If the syntax of an Autark script is invalid, the resulting error message may be vague or imprecise. This is due to the use of the leg PEG parser generator, which can produce generic error reports for malformed input.
- meta
- option
- check
- set
- env
- ${}
- @{}
- ^{}
- if
- echo
- configure
- path helpers
- run
- in-sources
- run-on-install
- foreach
- cc/cxx
- library
- install
Sets script variables using a simple convention: each variable name is automatically prefixed with META_. These variables are evaluated during the init phase of the build.
The meta rule is recommended for defining global variables such as project version, name, description, maintainer contact information, etc.
These meta-variables are especially convenient to use in configure rules.
If variable is defined using the let clause inside meta, the META_ prefix is not added to its name.
option {...}
This rule declares a build option. Build options are variables set at the start of the build to configure the desired output. These variables can be defined either as environment variables:
or as command-line arguments:
If you don't use the variable values in shell scripts during the build, the second method is preferred - it avoids polluting the environment and prevents these variables from leaking into subprocesses where they might be unnecessary or accidentally override something.
To list all documented build options, use:
check {...}
This construct is the workhorse for checking system configuration and capabilities before compiling the project. Checks are performed using dash shell scripts.
The main purpose of a check script is to set variables used in Autark scripts. Additionally, check scripts can declare dependencies on files and environment variables, and are automatically re-executed when some of dependencies are changed. (This is handled by invoking the autark command from within the check script.)
You can find a collection of useful check scripts at: https://github.com/Softmotions/autark/tree/master/check_scripts Feel free to copy any scripts that are relevant to your project.
Check scripts must be located in the ./autark directory relative to the script that references them, or in similar directories of parent scripts.
For small projects, it is convenient to place all check scripts in the .autark directory at the root of your project.
Within the context of a check script, the autark command is available. It allows the script to define variables or register dependencies - so that if any of those dependencies change, the check script will be automatically re-executed.
Check script example:
https://github.com/Softmotions/autark/blob/master/check_scripts/test_symbol.sh
The results of check script execution can also be used not only in variables but in project header templates, such as config.h.in. This is typically done via configure rules in the Autark script:
Autark will replace //autarkdef lines with the appropriate #define or comment them out, depending on whether the variable is defined or not.
config.h.in:
Or you may use variable directly in CFLAGS
set {...}
The set rule assigns a value to a variable in the build script.
set is a lazily evaluated rule - the value of set is only computed if the variable or expression is actually used.
The most common form is set { NAME [VALUES]... }
Note that in Autark, all variables are either strings or lists. A list is just a string internally, encoded in a special form: \1VAL_ONE\1VAL_TWO\1..., where list values are separated by the \1 character.
If there is only one value, e.g. set { foo bar }, then the variable foo is evaluated as "bar" string. If you write set { foo bar baz }, the value of foo becomes a list, encoded as \1bar\1baz.
Set is an expression actually and can be used inline in other constructs. For example:
Here, CFLAGS is extended with an additional flag -I./libhello directly in-place using an anonymous set where _ used in place of variable name.
By default, variables defined with set are visible only in the current Autark script and any child scripts included by include directive.
To define or modify a variable in the parent's script context, use:
To define a variable in the root script context:
env {...}
Similar to set, but sets an environment variable for the build process using setenv(3) at the operating system level.
Unlike set, the value of this rule is evaluated at the time it is executed, which happens during the setup phase of the build.
${...} Variable evaluation
The expression ${VARIABLE [DEFAULT]} is used when the value of a variable needs to be passed as an argument to another rule. If the variable is not defined in the current script context, the DEFAULT value will be used if provided.
@{...} Program output evaluation
Invokes the specified program PROGRAM and returns its standard output as a string.
Example:
In this example, the output of pkgconf --libs --static libcurl is appended to the LDFLAGS list. Note what .. in pkgconf instruction means what space separated output of pkgconf programm will be converted to list and merged with LDFLAGS list.
^{...} Expressions concatenation
Concatenates expressions output. Here we encounter an important syntactic rule in Autark scripts: all rules must be separated by whitespace. Because of this, the following code is invalid:
In this case, the Autark interpreter treats -DBUILD_TYPE=$ as a rule name, which is not what we intend.
To correctly construct the string -DBUILD_TYPE=${BUILD_TYPE} using the variable value from the script context, you must use the string concatenation rule:
Note: the space between -DBUILD_TYPE= and ${BUILD_TYPE} is required.
if {...} condition
Conditional directive.
Examples:
If the condition CONDITION_RULE evaluates to a truthy value, the entire if expression is replaced by EXPR1 in the Autark script’s instruction tree. Otherwise, if an else block is provided, it will be replaced by EXPR2.
Conditions
Exclamation mark ! means expression result negation, when trufly evaluated expressions became false.
[!]${EXPR}
Evaluates as truthy in any of the
following cases:
- The result of EXPR is a non-empty string and not equal to "0".
- The result is a list with a single element that is not empty and not "0".
- The result is a list with more than one element, regardless of content.
[!]defined { VARIABLE [VARIABLE2 ...] }
Truthy if
any of specified variables is defined in the current script context.
[!]eq { EXPR1 EXPR2 }
Truthy if the string value of
EXPR1 is equal to EXPR2.
[!]prefix { EXPR1 EXPR2 }
Truthy if the string
value of EXPR2 is a prefix of EXPR1.
[!]contains { EXPR1 EXPR2 }
Truthy if EXPR1
contains EXPR2 as a substring.
[!]or { EXPR1 ... EXPRN }
Truthy if any of the
expressions inside the directive is truthy (according to the rules
defined above).
[!]and { EXPR1 ... EXPRN }
Truthy if all
expressions inside the directive are truthy.
Please Note: Autark syntax does not support else if constructs.
error {...} Abort build and report error
Typical example error directive usage:
echo {...}
Prints arbitrary information to the console (standard output, stdout).
You can optionally specify the build phase (init, setup, or build) during which the output of the echo directive should appear. By default, it is printed during the build phase.
Please note: Variables in Autark defined by set rule are lazy evaluated, so by using echo you may indirectly change behaviour and effectiveness of your build process
Example:
This will print the current values of the CFLAGS and LDFLAGS variables.
configure {...}
The configure rule acts as a preprocessor for text files and C/C++ source templates, replacing specific sections with values of variables from the current script context.
Autark tracks all variables used during the substitution process. If any of those variables change, the entire configure rule will be re-executed. This is why it's recommended to configure each file in a separate configure rule whenever possible.
Incorrect:
Correct:
@...@ Substitutions
Text fragments enclosed in @ symbols are interpreted as variable names and replaced with their corresponding values. If a variable is not defined, the expression is replaced with an empty string.
Example libejdb2.pc.in:
exec_prefix=@INSTALL_PREFIX@/@INSTALL_BIN_DIR@ libdir=@INSTALL_PREFIX@/@INSTALL_LIB_DIR@ includedir=@INSTALL_PREFIX@/@EJDB_PUBLIC_HEADERS_DESTINATION@ artifact=@META_NAME@ Name: @META_NAME@ Description: @META_DESCRIPTION@ URL: @META_WEBSITE@ Version: @META_VERSION@ Libs: -L${libdir} -l${artifact} Requires: libiwnet Libs.private: @LDFLAGS_PKGCONF@ Cflags: -I@INSTALL_PREFIX@/@INSTALL_INCLUDE_DIR@ -I${includedir} Cflags.private: -DIW_STATICFor C/C++ header files, the //autarkdef directive is a convenient way to conditionally generate #define statements based on variable presence.
Basic form:
If VAR_NAME is defined, this will be replaced with:
With string value:
If VAR_NAME is defined, this becomes:
With num value:
If VAR_NAME is defined, this becomes:
S / SS / C / CC / % Path Helpers
These small helper rules are useful for computing file paths in build scripts. They help avoid hardcoding paths that may depend on the current location of the project's source code.
S {...}
Computes the absolute path of the given argument(s) relative to the project root. If multiple arguments are provided, they are concatenated into a single path string.
SS {...}
Same as S, but relative to the directory where the current script is located.
C {...}
Computes the absolute path to the argument(s) relative to the project-wide autark-cache dir.
CC {...}
Computes the absolute path relative to the local cache directory of the current script. This is the default working directory for tools and commands that operate on build artifacts.
% {...}
Returns the basename of the given filename changing its extension optionally.
run {...}
The run rule is the most powerful construct in the Autark system. It allows you to define custom build actions and their dependency chains.
In C/C++ projects, run is typically used to build static and dynamic libraries, executables, and other artifacts - but it can support virtually any custom pipeline.
The consumes section defines the input dependencies that must be satisfied before the run rule is executed. If any of the consumed files or variables change, the rule will be re-executed.
The produces section declares the artifacts that this run rule generates. Other rules can then declare dependencies on these outputs.
You can include any number of exec or shell subsections inside a single run rule:
- exec executes a command directly using execve() (no shell interpretation).
- shell executes the command via the /bin/sh shell.
Autark also implicitly tracks dependencies on the evaluated arguments passed to exec and shell commands.
If the special keyword always is present inside the run rule, the command will be executed unconditionally, regardless of input state. This is useful, for example, when running test cases or side-effectful operations.
in-sources {...}
By default, most Autark rules are executed inside the autark-cache dir corresponding to the current build script. This is convenient because generated artifacts, logs, and temporary files stay in the cache and don't clutter the project source tree.
However, in some cases, it's necessary to run a command in the context of the actual project source directory. For this purpose, Autark provides the in-sources rule.
Example: Appending glob-matched source Files
In the example below, the SOURCES variable is extended with the output of a glob-matching command executed in the source directory. The result is assigned merged with SOURCES variable in the parent script:
Example: Finding Java Source Files
This example populates JAVA_SOURCES with the full paths of all .java files under src/main/java, resolved from the script's source directory.
run-on-install {...}
run-on-install is a special form of the run rule that is executed during the post-build phase.
A typical use case for this rule is to install artifacts from dependent projects that the current project relies on.
In this example, the project invokes Autark to install the dependent iowow project with the proper build flags and installation prefix.
Reference: https://github.com/Softmotions/iwnet/blob/master/Autark
foreach {...}
When you need to perform many similar actions for a list of files or objects, foreach provides a concise and powerful solution.
Currently, foreach supports only the run rule as its body.
foreach iterates over all elements in LIST_EXPR, and for each element, it sets the variable VAR_NAME to the current item and evaluates the run rule.
A common use case for foreach is generating and running per-file executables, such as test cases. Here's an example:
In this example:
- foreach is used to build a separate binary for each object file in ${CC_OBJS}.
- Then, conditionally (if ${RUN_TESTS} is truthy), each test binary is executed.
- %{${OBJ}} is used to extract the base name from the object file path (e.g., main.o => main).
This approach enables clean, scalable test orchestration without hardcoding individual test names.
cc { ... } / cxx { ... }
This rule compiles C or C++ source files into object files, while automatically tracking all required dependencies including header files. Compilation is parallelized to speed up the build process.
SOURCES
The first argument must be an expression that returns a list of source files. It can be a single string (for one file) or a list defined by a set rule.
Paths to source files can be:
- Absolute paths
- Relative to the source directory of the current script
- Paths relative to the cache directory corresponding to the script (this is useful when compiling generated sources from other rules)
COMPILER_FLAGS
A list of compiler flags, typically defined using a set rule.
Note: the -I. flag is automatically added to the compiler flags, which includes the current cache directory in the include path for header files.
COMPILER_CMD
The compiler to use.
If not explicitly specified:
- Rule cc uses the CC variable or cc if variable is not defined.
- Rule cxx uses the CXX variable or c++ if variable is not defined.
Note: The compiler must support dependency generation using the -MMD flag to allow Autark to correctly track header file dependencies.
objects { NAME }
By default, the cc rule sets a variable named CC_OBJS containing the list of object files produced during compilation.
If you use multiple cc rules within the same build script, you may want to assign different output variable names using the objects { NAME } directive.
This allows you to manage multiple sets of object files independently.
consumes { ... }
This section declares additional dependencies for the cc compilation rule.
For example, if your code depends on a file generated by a configure rule (such as a generated header file), you must list it here. This ensures that the configure step is executed before compilation starts.
Source files compilation is performed in parallel. By default, the parallelism level is set to 4. You can change this using the -J option on the command line. For example:
This will run up to 8 compilation jobs in parallel.
library {...}
Search for a library file by name.
This simple rule looks for the specified LIB_FILES (e.g., libm.a, libfoo.so) in standard library locations commonly used across Unix-like systems:
- $HOME/.local/<lib>
- /usr/local/<lib>
- /usr/<lib>
- /<lib>
Here, <lib> refers to platform-specific subdirectories where shared or static libraries may be located (such as lib, lib64, etc., depending on the system).
- The resulting absolute path to the first matched file will be stored in the specified variable.
- You can store it in the current scope (VAR_NAME), parent script (parent { VAR_NAME }), or root script (root { VAR_NAME }).
Example:
This will search for libm.a, and if found, store its absolute path in LIB_M and print it. If the library is not found, the script will terminate with an error.
install {..}
If build.sh is run with the -I or --install option (to install all built artifacts), or if an install prefix is explicitly specified using -R or --prefix=<dir>, then all install rules will be executed after the build phase completes.
Available variables when install is enabled
- INSTALL_PREFIX – Absolute path to the installation prefix. Default: $HOME/.local
- INSTALL_BIN_DIR – Path relative to INSTALL_PREFIX for installing executables. Default: bin
- INSTALL_LIB_DIR – Path relative to INSTALL_PREFIX for installing libraries. Default: platform-dependent (e.g., lib or lib64)
- INSTALL_DATA_DIR – Path relative to INSTALL_PREFIX for shared data. Default: share
- INSTALL_INCLUDE_DIR – Path relative to INSTALL_PREFIX for headers. Default: include
- INSTALL_PKGCONFIG_DIR – Path relative to INSTALL_PREFIX for .pc files. Default: platform-dependent (e.g., lib/pkgconfig)
- INSTALL_MAN_DIR – Path relative to INSTALL_PREFIX for man pages. Default: share/man
The install rule copies the specified files into the target directory ${INSTALL_PREFIX}/${INSTALL_DIR_EXPR} (creating it if necessary). File permissions from the original files are preserved during the copy.
Example:
This will install libiwnet.pc into the appropriate pkgconfig directory, and public headers into the include directory under the chosen prefix.
MIT License Copyright (c) 2012-2025 Softmotions Ltd <[email protected]> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE..png)
