Skip to content

Latest commit

 

History

History
1213 lines (933 loc) · 41.2 KB

catalog-format-spec.md

File metadata and controls

1213 lines (933 loc) · 41.2 KB

Catalog format specification

Big picture

Each release belonging to a crate is described as a TOML file. This file has minor differences depending on its location: a local manifest is found at the top-level directory of the sources of a project, in which case its named alire.toml, whereas a manifest found in an index (e.g., the community index), is named <name>-<version>.toml.

Other than that, contents follow the same conventions and there are only slight differences (some fields are intended only for an index manifest, and cannot appear, or are optional, in a local manifest). These differences are highlighted in the following descriptions, where necessary.

Each TOML description file contains exactly one release, except for the special external definitions that are described in their own section.

Information encoding

This section describes the various encodings used in this format to encode information.

First, there are two kinds of data: atomic and composite.

Atomic data designates values that cannot be decomposed. There are only two atomic data types:

  • mere strings ("Hello, world!");
  • booleans (true, false);

When a string denotes a relative path intended to be portable across operating systems, it must use forward slashes as directory separator: "path/to/my/resource".

We can then split composite data in two kinds: lists (TOML's arrays) and mappings (JSON's tables). Lists are just sequences of other values, for instance a list of strings:

["A", "B"]

Mappings are the traditional sets of associations from keys (here, always strings) to other values. For instance, the following represents a set of dependencies, with version constraints:

libfoo = "^1.2"
libbar = "^2.0 & /=2.1.3" # Excluding a known bad version

In some contexts, information can be dynamic: special encodings can be used to make data vary depending on the environment (OS, architecture, ...). The environment is represented as a set of specific variables which can have a specific set of values: see the Parameters section below for a comprehensive list.

All properties that support dynamic expressions follow the same structure, in which the expression (case-like) is inserted between the key and its value. For example, given a static expression:

key = "value"

one of its cases would be expressed by the following inline TOML table:

key.'case(var)'.var_value = "value"

Several expressions can be inserted between a property key and its value, leading to a combinatorial explosion if all cases have specific values. The equivalent to an others Ada clause in this format is a '...' entry.

Here is an example of a conditional boolean value.

{'case(distribution)' = {
    'debian|ubuntu': true,
    '...': false
}}

# Or in a more idiomatic TOML syntax
['case(distribution)']
'debian|ubuntu' = true
'...' = false

Depending on the value of the distribution environment variable, this will return true (its value is debian or ubuntu) or false (for other values). Note that these and subsequent examples are not showing the left-hand-side property to which such a value would be assigned.

A little variation allows building environment-dependent composite data. For instance, to make the dependency on libbar above dynamic:

{
    "libfoo": "^1.2",
    "case(os)": {
        "linux": {"libbar": "^2.0"},
        "windows": {"libwinbar": "^3.0"},
        "...": {}
    }
}

# Or in a more idiomatic TOML syntax
libfoo = "^1.2"

['case(os)'.linux]
libbar = "^2.0"

['case(os)'.windows]
libwinbar = "^3.0"

['case(os)'.'...']

The case(os) part selects dependencies depending on the value of the os environment variable.

If the os environment variable contains linux, this will create the following dependencies:

libfoo = "^1.2"
libbar = "^2.0"

If the os environment variable contains windows, this will create the following dependencies:

libfoo = "^1.2"
libwinbar = "^3.0"

And finally for other os values:

libfoo = "^1.2"

Release Information

This section describes the actual properties that must or can appear in a manifest file to describe a release. Unless specified, all the entries must be static, i.e. they cannot depend on the context.

  • name: mandatory string. The name of the crate this release belongs to. Use alr help identifiers to see the rules such names must follow.

  • version: mandatory string. The semantic version of the release.

  • description: mandatory string. One-line description about the package. For instance:

    description = "Library to handle foobars"
  • long-description: optional free-form string to provide information about this package, in addition to description, without length restrictions.

  • authors: optional array of strings. Flat list of human-readable names for the authors, i.e. the people that wrote the software that is packaged. For instance:

    authors = ["Alice Example",
               "Bob For Instance <bob@example.com>"]
  • maintainers: mandatory (for indexing) array of strings. Flat list of human-readable names (optional) for the maintainers, with a contact email (mandatory); i.e. the people that maintain the crate metadata in Alire. For instance:

    maintainers = ["alice@example.com",
                   "Bob For Instance <bob@athome.com>"]
  • maintainers-logins: mandatory (for indexing) array of strings. Flat list of github login usernames used by the maintainers of the crate. This information is used to authorize crate modifications. For instance:

    maintainers-logins = ["alicehacks", "bobcoder"]
  • licenses: mandatory (for indexing) string. A valid SPDX expression. Custom license identifiers are accepted with the format: custom-[0-9a-zA-Z.-]+

    licenses = "MIT"

    For a double license:

    licenses = "GPL-3.0-only OR MIT"

    For a custom license:

    licenses = "custom-my-license-1.2"
  • website: optional string. URL to the original project's website. For instance:

    website = "https://myproject.example.org/"
  • tags: optional array of strings. Flat list of topics covered by the crate. Tags will help users find crates related to their interests:

    tags = ["spark", "security"]
  • available: optional dynamic boolean expression. Determines whether the package is available for the current platform (true) or not (false). For instance:

    [available.'case(distribution)']
    'debian|ubuntu' = true
    '...' = false
  • depends-on: optional array of dynamic dependencies expressions. For instance:

    [[depends-on]]  # A static dependency
    libfoo = "^1.2"
    
    [[depends-on]]  # A dynamic dependency
    [depends-on.'case(os)'.linux]
    libbar = "^2.0"
    
    [depends-on.'case(os)'.windows]
    libwinbar = "^3.0"

    Available constraint operators are the usual Ada relationals (=, /=, >, >=, <, <=) plus caret (^, any upwards version within the same major point) and tilde (~, any upwards version within the same minor point).

    Note that caret and tilde do not have any special behavior for pre-1 versions. This means, for example, that ^0.2 will still mean any release below 1.0. The Semver specification does not make any promises about the compatibility of pre-1 versions, and there are differing interpretations of these operators out there for such versions. Bear in mind this when expressing your restrictions; for pre-1 versions you most likely want to use ~0.x constraints (compatibility within a minor version).

    Logical operators for and (&), or (|) are accepted; see the Semantic_Versioning project documentation on extended version sets.

    See also the section on compiler dependencies for more details on how to use the depends-on property for cross-compiling or compiler version selection.

  • project-files: optional list of strings. Each is a path, relative to the root of the source directory, to a .gpr project file to be made available. Expressions are accepted. For instance:

    project-files = ["my_project.gpr", "utils/utils_for_my_project.gpr"]
    
    [project-files.'case(word-size)']
    bits-64 = ["my_project.gpr"]
    bits-32 = ["my_project32.gpr"]
  • gpr-externals: optional table, giving a mapping from the name of external variables in the .gpr project files to sets of possible values (as array of strings), or an empty string if this set is infinite. For instance:

    [gpr-externals]
    BUILD_MODE = ["debug", "profile", "release"]
    TAG = ""
  • gpr-set-externals: optional dynamic table, setting values of project external variables when building the project. This should not be used to specify default values, the default values must be specified in the .gpr project file. Expressions are accepted before the mapping. For instance:

    [gpr-set-externals]
    BUILD_MODE = "release"
    
    [gpr-set-externals.'case(os)']
    linux   = { OS = "gnu-linux" } # Compact table syntax is convenient in this case
    windows = { OS = "ms-linux" }  # to see all enumeration values, one per row.
  • environment: optional dynamic table used to modify environment variables that will apply at build time. Variables and values are specified with the form VARIABLE.<action> = "value", where <action> is one of append, prepend, or set. For instance:

    [environment]
    C_INCLUDE_PATH.append = "/usr/include/something"
    MY_PROJECT_ASSETS.set= "${CRATE_ROOT}/assets"
    PATH.append = "${DISTRIB_ROOT}/usr/bin"

    Path fragments in this table must use portable format, that is, '/' for path separation. Alire will take care of using the native separator when setting these variables.

    Predefined variables are provided by Alire and will be replaced in the value:

    • ${CRATE_ROOT} absolute path to the deployment directory of the crate.
    • ${DISTRIB_ROOT} absolute path to the root directory of the system distribution. On UNIX systems it will be /, on Windows msys2 it will be the msys2 installation directory (e.g. C:\Users\user_name\.cache\alire\msys2).

    The escaping "\$" can be used to prevent the expansion of a dollar-bracketed expression.

    Environment entries can use dynamic expressions:

    [environment.'case(distribution)']
    msys2 = { C_INCLUDE_PATH.append = "${DISTRIB_ROOT}/mingw64/include/SDL2" }
  • executables: optional dynamic list of strings. Each one is the simple name of an executable provided by the package. Executables are looked for by alr in the build tree and must not include a path. If only one executable is given, it is considered the default for alr run. For instance:

    executables = ["my_main"]
  • actions: optional dynamic list of actions to perform when certain events take place in a workspace. Actions are executed in the order they are defined in the manifest. The general action syntax is:

    [[actions]]
    type = <kind>
    command = <command>
    directory = <relative path>  # Optional

    <command> is an array of strings for a shell command to run in the source directory.

    For events that cause a workspace-wide triggering of actions (all pre_/post_ actions described next), the actions are invoked in a dependency-safe order, starting at the leaves of the dependency graph (releases with no dependencies) and moving up to the root release (the working release, or the release being obtained with alr get). In this context, the root release is considered part of the dependency solution, and so its actions are executed too, always in the last place.

    directory is an optional portable relative path (forward-slashed) from the crate root, in which the action will be executed. This directory must exist or the action will error. Actions are executed by default in the crate root.

    <kind> can be either:

    • post-fetch: the command is to be run whenever there are new sources deployed in the workspace, in any release in the solution. All releases post-fetch actions are run after the new deployment is complete. Initial retrieval, subsequent modification of dependencies, pinning a directory or repository is considered a deployment of new sources. A manual alr update, even if it results in no changes, will also trigger this action in every release in the solution.

    • pre-build: the command is to be run right before the build of the workspace starts. This kind of action is run for all releases in the solution.

    • post-build: the command is to be run right after a build has successfully completed. This kind of action is run for all releases in the solution.

    • test: the command is run on demand for crate testing within the Alire ecosystem (using alr test). This kind of action is run only for the root crate being tested. The crate is not built beforehand when a test action is defined so, if a build is necessary, it should be explicitly given as part of the action sequence.

    Since actions may end being run more than once they should take this into account and allow multiple runs with the expected results intended by the packager.

    Actions accept dynamic expressions. For example:

    [[actions.'case(os)'.linux]]
    type = "post-fetch"
    command = ["make"]
    
    [[actions.'case(os)'.windows]]
    type = "post-fetch"
    command = ["cmd", "build"]
    
    [[actions.'case(os)'.'...']]
    # An explicit empty case alternative, which is not mandatory

    The aforementioned TOML syntax is valid when there is only one conditional action. For multiple conditional actions, one can write:

    [[actions]]
    [actions.'case(os)'.linux]
    # Regular contents of an action, applying to the Linux case
    [actions.'case(os)'.macos]
    # macOS case
    
    [[actions]]
    # Another action, that needs not be also conditional (but could be).
  • auto-gpr-with: optional Boolean value that specifies if the project (gpr) files of a crate can be automatically depended upon ('withed') directly by the root project file. (The default is true.) This feature is meant to simplify the process of using dependencies in Alire. However, not all project files are supposed to be direct dependencies. Some are intended to be extended, for example, and in that case a crate can disable the feature by setting auto-gpr-with=false.

  • origin: dynamic table. Mandatory for index manifests and forbidden in workspace manifests. This table describes how sources are obtained, using the following fields:

    • url: mandatory string which points to a source file or repository.

    • hashes: mandatory string array for source archives. An array of "kind:digest" fields that specify a hash kind and its value. Kinds accepted are: sha512.

    • archive-name: optional string. If url points to a source archive, this can specify the name of the file to download, which is needed in order to properly extract the sources, in case the URL does not identify it.

    • commit: mandatory string for VCS origins that describes the VCS-specific revision to be checked out (a git/hg hash, a svn revision).

    • subdir: optional relative path, only valid for repository origins, that when provided indicates that the crate is not located at the repository root. This option enables the possibility of publishing several crates from the same repository (sometimes referred to as a monorepo).

    • binary: optional (defauts to false) boolean used to design the origin as binary. Binary origins are not compiled and can optionally use dynamic expressions to narrow down the platform to which they apply. An origin using a dynamic expression must be tagged as binary; see the example below.

    Examples of origin tables:

    # Clone a git repository with a crate at its root
    [origin]
    url = "git+https://github.com/example-user/example-project"
    commit = "ec8b267bb8b777c6887059059924d823e9443439"
    # Download and extract a source archive
    origin = "https://example.org/0123456789"
    archive-name = "archive.tar.gz"
    hashes = ["sha512:bf6082573dc537836ea8506a2c9a75dc7837440c35c5b02a52add52e38293640d99e90a9706690591f8899b8b4935824b195f230b3aa1c4da10911e3caf954c04ac"]
    # Clone a git repository with the crate in a subdirectory
    [origin]
    url = "git+https://github.com/example-user/example-project"
    commit = "ec8b267bb8b777c6887059059924d823e9443439"
    subdir = "examples"
    # A binary origin denoting a compiler
    [origin."case(os)".linux."case(host-arch)".x86-64]
    url = "https://github.com/alire-project/GNAT-FSF-builds/releases/download/gnat-12.1.0-1/gnat-x86_64-linux-12.1.0-1.tar.gz"
    hashes = ["sha256:df1f36b306359d528799b1de8629a793523347a90c9d4b72efd23c62a7279555"]
    binary = true
  • available: optional dynamic boolean expression. If it evaluates to false, the package is not available for the current platform.

  • notes: optional string. Provides miscellaneous information about this release. For instance:

    notes = "Experimental version"
  • configuration: optional table to control crate configuration code generators:

    For more information on crate configuration, see Using crate configuration.

    • disabled: Completely disable configuration code generation for the crate (default: false)

    • output_dir: Path to the directory where the configuration code will be generated, relative to the crate root (default: config).

    • generate_ada: Enable generation of Ada configuration (default: true).

    • generate_gpr: Enable generation of GPR file configuration (default: true).

    • generate_c: Enable generation of C configuration (default: true).

    • auto_gpr_with: Enabled generation of list of withed project in the GPR file configuration (default: true).

  • configuration.variables: optional table of crate configuration variable definitions.

    For more information on crate configuration, see Using crate configuration.

    The keys of the table are names of the variables. Variable definitions themselves are tables with the following entries:

    • type: mandatory string which defines the type of the variable, it can be:

      • String: any string

      • Boolean: either True or False

      • Enum: enumeration type

      • Integer: an integer value that can be encoded in 64-bit

      • Real: a real value that can be encoded in IEEE 754 binary64

    • default: optional default value for the variable. Will be used if no crates explicitly set a value for this variable. Must be a valid value for the type.

    • first: (optional) for Real and Integer types only. Defines the lower bound of valid values for the type (inclusive).

    • last: (optional) for Real and Integer types only. Defines the upper bound of valid values for the type (inclusive).

    • values: mandatory for Enum types. An array of strings containing all the possible values for the enumeration.

    Example:

    [configuration.variables]
    Device_Name = {type = "String", default = "no device name"}
    Print_Debug = {type = "Boolean", default = false}
    Debug_Level = {type = "Enum", values = ["Info", "Debug", "Warn", "Error"], default = "Warn"}
    Buffer_Size = {type = "Integer", first = 0, last = 1024, default = 256}
    Max_Power   = {type = "Real", first = 0.0, last = 100.0, default = 50.0}
  • configuration.values optional table of variables assignment:

    The keys of the table are crate names, and entries are sub-tables of variable_name and value. The type of the value has to match the definition of the variable type.

    Example:

    [configuration.values]
    crate_1.var1 = 42
    crate_1.var2 = true
    crate_2.var1 = "Debug"
  • build-profiles: optional table of strings that sets the build profile of crates in the solution.

    For more information on build profiles and switches, see Build profiles and switches.

    There are 3 build profiles available in Alire:

    • Development
    • Release
    • Validation

    Example:

    [build-profiles]
    depend1 = "validation" # Set depend1 build profile to validation
    depend2 = "development" # Set depend2 build profile to development
    my_crate = "release" # Set my_crate build profile to release

    A wildcard key can be used to set the build profile of all crates that are not otherwise specified:

    [build-profiles]
    "*" = "validation" # Set all build profiles to validation
  • build-switches: optional table of build profile switches definitions.

    For more information on build profiles and switches, see Build profiles and switches.

    The keys of the table are either build profiles or the wildcard "*":

    • Development
    • Release
    • Validation

    The values are profile definitions, themselves tables with switch categories as keys and switches selection as values. This list of switch categories and their corresponding selection is as follow:

    • Optimization
      • Performance
      • Size
      • Debug
    • Debug_Info
      • No
      • Yes
    • Runtime_Checks
      • None
      • Default
      • Overflow
      • Everything
    • Compile_Checks
      • None
      • Warnings
      • Errors
    • Contracts
      • No
      • Yes
    • Style_Checks
      • No
      • Yes
    • Ada_Version
      • Compiler_Default
      • Ada83
      • Ada95
      • Ada05
      • Ada12
      • Ada2022
      • GNAT_Extensions
    • Source_Encoding
      • Compiler_Default
      • UTF-8

    For example, to enable all run-time checks in the release profile:

    [build-switches]
    release.runtime_checks = "Everything"

    To disable style checks for all profiles:

    [build-switches]
    "*".style_checks = "No"

    All switch categories also accept a custom list of switches, for instance:

    [build-switches]
    release.optimization = ["-O1"]
    validation.style_checks = ["-gnatyg"]
  • provides: specifies a list of releases of another crate for which the current release is a drop-in replacement. I.e., the crate is either API-compatible or call-compatible, depending on how it is to be used (as a source library, or providing some command-line tool).

    Example:

    name = "foo"
    provides = ["bar=1.1"]
    # A crate depending on `bar^1` might find this `foo` release in its solution instead.
  • forbids: an array of tables containing dependency specifications, just as the depends-on property. Releases matching one of the forbidden dependencies are prevented from appearing in a solution with the release doing the forbidding.

    There are two use cases for this property:

    1. To codify known conflicts between releases for some reason (for example, sources with the same name).
    2. To provide drop-in replacements for another crate, in conjunction with a provides field. In this case the release must both provide and forbid the crate for which it is a replacement.

    Example:

    name = "bar"
    version = "1.0"
    provides = [ "foo=1.0" ]
    [[forbids]]
    baz = "*" # This crate cannot coexist with ours for some reason
    foo = "*" # No other crate that provides foo is needed/allowed at the same time

Work-in-progress dependency overrides

It is usual to develop several interdependent crates at the same time. In this scenario, it is often impractical to rely on indexed releases which are not intended to be modified. Instead, one would prefer to use a work-in-progress version of a crate to fulfill some dependency.

Alire provides pins to support this use case. Pins override dependencies, they are intended to be used locally, and to be fulfilled by proper dependencies once a crate is ready to be published. The use of pins is based on two ideas:

  • Dependencies are given, as normally, in the depends-on array of the manifest, even for those dependencies to be pinned. This way, once the release is ready, pins are simply removed and the actual dependencies are used in their place.
  • Dependency overrides, aka pins, are given under the [[pins]] array of the manifest.

Three kinds of pins are available, all of them with the syntax:

crate_name = { pin_attributes }

The specific pin kinds and their attributes are:

  • Pins to versions: used to force the use of a particular version of an indexed crate.

    • version: a string containing a single version to be used.
    • crate_name = { version = "1.2+hotfix-1" }
  • Pins to local crates: a local directory will fulfill the crate dependency, no matter what version is given in its local manifest. "Raw" Ada projects without an Alire manifest can be used too, as long as their project file matches the crate name and it is located in the directory given as override.

    • path: an absolute or relative path to the crate directory.
    • crate_name = { path = "../my/wip/crate" }

    For the common case of directories containing an Alire manifest, dependencies and pins will be included recursively in the build context.

  • Pins to git repositories: the repository will be cloned locally and its directory will be used as in the previous case. This pin may optionally include a commit to fix the checkout to be used, or a branch to track. Otherwise, the default branch will be used. Running alr update will refresh the checkout.

    • url: the URL of a git repository
    • commit (optional): a complete git commit hash.
    • crate_name = { url = "https://my/repo.git" } # Updatable pin to default branch
    • crate_name = { url = "https://my/repo.git", branch="feature" } # Updatable pin
    • crate_name = { url = "https://my/repo.git", commit="abcdef..." } # Fixed pin

Using pins for crate testing

Pins are also useful to have a separate test project that depends on your main crate. The recommended setup is as follows:

/path/to/my_crate
├── alire.toml
└── tests
    └── alire.toml

I.e., a tests crate is initialized within the main my_crate. In tests manifest, you have a dependency and local relative path pin for my_crate:

# tests/alire.toml
[[depends-on]]
my_crate = "*"              # Any version of the main crate
aunit = "*"                 # We can have dependencies for testing only
[[pins]]
my_crate = { path = ".." }  # Overridden by the latest sources

Then, my_crate is published normally, and tests can be used locally for any kind of testing needed on my_crate without polluting my_crate manifest with test specifics (like extra dependencies used by the test setup).

External releases

The above information applies to regular releases distributed from sources (that is, the Ada projects whose distribution is the main Alire goal). Some special supporting releases also exist that are described differently.

A release is considered "external" when it is not built from sources and, furthermore, its semantic version cannot be known until run time. Hence, the availability and version of these releases is detected by alr.

Several definitions for these external releases may exist so they are defined in a manifest as a vector with key external:

[[external]]
# Common entries to all externals
kind = "hint" # One of several predefined external kinds
hint = "Please install SDL in your platform from source or system packages"
# Specific external kind parameters might follow

All external kinds can define these regular properties:

  • available: when defined, it restricts the external detection to the given environment conditions.

  • hint: optional dynamic string containing an explanation for the user on how to make the external entity available. This explanation is shown on request with alr show --external, or after alr get, for any external dependency that could not be detected.

External kinds: hints

A plain undetectable external kind intended to simply serve as a hint. For crates that are known to be unavailable through Alire, it serves to provide a generic or customized hint to the user. It has no specific fields, other than the common ones just described. Its key is "hint":

[[external]]
kind = "hint" # Identifies this external kind
# Bare minimum external. Optionally, the hint/available fields can be used.

External kinds: command-line tools

This external kind is used to describe commands that can be run in the system, and that are able to provide their own version via some particular invocation. Their specific fields are (all mandatory):

kind = "version-output" # Identifies this external kind

version-command = ["gnat", "--version"]
# Invocation that will provide the version when the tool is available

version-regexp  = "^GNAT ([\\d\\.]+).*|^GNAT Community ([\\d]{4}).*"
# TOML-escaped GNAT.Regpat-compatible regular expression. Parenthesized
# matches will cause the matched expression to be parsed as the Semantic
# Version of the tool.

provides = "another_crate_name"
# This crate will be equivalent to `another_crate_name` for the solver. The
# version will be the same as detected for the current external. For example,
# all GNAT compilers provide the "gnat" crate, and so there cannot be two
# compilers in the same solution.

External kinds: system packages

Systems that have their own package manager (e.g. Linux) can readily provide many complex dependencies still unpackaged as source code in Alire. Alire can use these on supported platforms during resolution. At this time, the supported platforms are Arch, CentOS, Debian, Fedora, Homebrew, MacPorts, MSYS2, RHEL, SUSE/openSUSE, and Ubuntu; do not hesitate to contact us if you would like to maintain other distributions.

A system external gives a list of platform package names that supply the dependency natively. The platform package manager will be used to detect their availability and version. To that effect, the origin field is used (which can accept dynamic expressions in this context):

kind = "system" # Identifies this external kind
origin = ["libncursesada3", "libncursesada5"]
# As versions appear this list will grow. To speed up detection, dynamic
# expressions may become recommended for certain system packages.

For Ada pre-compiled system libraries that require the platform compiler for linking (e.g., in Debian/Ubuntu), and that cannot be used with other GNAT compilers, this should be expressed with the available property, e.g.:

available.'case(toolchain)'.user = false
# `available` defaults to true, so it is enough to flag the user toolchains

Parameters

  • os: name of the OS. Currently supported values are: freebsd, linux, macos, windows, and os-unknown.

  • distribution: name of the Linux distribution or name of the software distribution platform if running on a different OS. Currently supported values are: arch, centos, debian, fedora, homebrew, macports, msys2, rhel, suse, ubuntu, and distribution-unknown.

  • toolchain: takes system value in distributions with the system Ada compiler first in PATH (GNAT FSF in Debian/Ubuntu), user otherwise (GNAT Community editions, other cross-target toolchains).

  • word-size: architecture word size. Currently supported values are: bits-32, bits-64, bits-unknown

Using crate configuration

Alire provides a mechanism for crates to expose a list of variables that can be set by other crates depending on them. The configuration variables will then be converted to Ada, C and GPR source files that can be used to change the behavior or feature set of the code.

Let's start with a simple example. A crate named test can print debug log on the console. However printing on the console has a performance impact, for an embedded project it can even have a significant code size impact. Therefore it would be best if this logging can be disabled/enabled at compile time.

To achieve this, a crate maintainer can define a configuration variable in the crate manifest alire.toml. The definition will be like so:

[configuration.variables]
Enable_Logs = {type = "Boolean", default = false}

A single variable of type Boolean with a default value of false.

From this definition, Alire will generate various source files, including an Ada package specification:

package Test_Config is
   Enable_Logs : constant Boolean := False;
end Test_Config;

In the crate source code, this configuration package can be used like so:

   if Test_Config.Enable_Logs then
      Print_Log ("This is a log message.");
   end if;

If one of the crates depending on test sets the configuration variable to true, e.g.:

[configuration.values]
test.Enable_Logs = true

The constant value will change in the generated configuration package:

package Test_Config is
   Enable_Logs : constant Boolean := True;
end Test_Config;

Which will enable logging in the test crate.

It is possible for multiple depending crates to set test.Enable_Logs to the same value, however if two depending crates set the variable to a different value then the configuration is invalid and Alire will print an error. If no depending crates set the test.Enable_Logs variable, then its default value is used.

When to use crate configuration?

Usually when something has to be static or known at compiler-time, either for performance or memory usage.

When not to use crate configuration?

When the Ada languages provides a better alternative. There are many ways to provide an Ada API that will result in compile time optimization or static memory usage.

For instance, discriminants are an effective way to let the user define the size of a buffer:

   type Buffered_Thing (Size : Positive) is private;
private
   type Buffer_Array is array (Positive range <>) of Unsigned_8;
   type Buffered_Thing (Size : Positive) is record
      Buf : Buffer_Array (1 .. Size);
   end record;

With this definition, users are then able to allocate either statically, on the stack or on the heap depending on their project.

   Thing : Buffered_Thing (Size => 256);

Use cases

Log levels

Enumerations variables in crate configuration can be used to set a level of log verbosity:

[configuration.variables]
Log_Level = {type = "Enum", values = ["Info", "Debug", "Warn", "Error"], default = "Warn"}

Buffer size

Integer variables can be used the define the size of a static buffer:

[configuration.variables]
Buffer_Size = {type = "Integer", first = 0, last = 1024, default = 256}

This is useful in particular for embedded projects where compile time memory usage is preferred over dynamic allocation.

Server URL

String variables can be used to define the URL of a website or service:

[configuration.variables]
URL_Name = {type = "String", default = "example.com"}

PID coefficients

Real variables can be used for PID coefficients:

[configuration.variables]
Proportional = {type = "Real"}
Integral = {type = "Real"}
Derivative = {type = "Real"}

Worst case allocation

Integer variable can be used to define The maximum length of file names in a file-system:

[configuration.variables]
Max_Filename_Length = {type = "Integer", first = 5, last = 128}

Select algorithm in GPR project file

Crate configuration also generates a GPR project file, therefore it can be used to control which units are compiled in the project.

[configuration.variables]
Sort_Algorithm = {type = "Enum", values = ["bubble", "quick", "merge"]}

The generated GPR will look something like this:

project Test_Config is
   type Sort_Algorith_Kind is ("bubble", "quick", "merge");
   Sort_Algorith : Sort_Algorith_Kind := "quick";
end Test_Config;

It can be used in the main GPR file like so:

   package Naming is
      for Body ("Test.Sort") use "test-sort__" & Test_Config.Sort_Algorith;
   end Naming;

With the files test-sort__bubble.adb, test-sort__quick.adb and test-sort__merge.adb each implementing a different algorithm.

Platform Specific Code

In the crate configuration Alire also generates a few built-in values to identify the host platform:

  • Alire_Host_OS
  • Alire_Host_Arch
  • Alire_Host_Distro

They can be used in the main GPR file to add a different source directory based on the OS. For instance:

   for Source_Dirs use ("src",
                        "src/" & Test_Config.Alire_Host_OS);

with the following directory tree:

+-- src
    +-- host_specific.ads
    +-- linux
    |   +-- host_specific.adb
    +-- macos
    |   +-- host_specific.adb
    +-- windows
        +-- host_specific.adb

Build Profiles and Switches

As part of crate configuration, Alire will generate a list of compiler switches in the configuration GPR file. The list of switches for a given crate is controlled from two features:

  • build-profiles
  • build-switches

There are 3 build profiles available in Alire:

  • Development
  • Release
  • Validation

By default, the root crate is in Development and the dependencies are in Release. The defaults can be overridden in two ways:

  • The build profile of the root crate can be changed with a switch to the alr build command:

    • $ alr build --release
    • $ alr build --validation
    • $ alr build --development (default)
  • In the root crate manifest, the build profile of each crate in the solution can be changed with the [build-profiles] table.

    This can be used, for instance, in a unit test crate to set the crate under test in validation profile, or to debug one of the dependencies.

    Example:

    [build-profiles]
    lib_under_test  = "validation"
    lib_to_debug    = "development"

Each crate can customize the compiler switches corresponding to its profiles using the [build-switches] table. In general, this should be avoided to preserve consistency in the ecosystem. However, there are cases where it makes sense for a crates to change its build switches. For instance, a SPARK crate that is proved to be safe from errors can disable run-time checks in all profiles:

[build-switches]
"*".runtime_checks = "none"

It is also possible to specify a custom list of compiler switches for a category:

[build-switches]
release.optimization = ["-O1", "-gnatn"]

Using switches in GPR file

Alire will generate a list of switches in the crate configuration GPR file. It will look something like this:

abstract project my_crate_Config is
   [...]
   Ada_Compiler_Switches := External_As_List ("ADAFLAGS", " ") &
          (
            "-Os" -- Optimize for code size
           ,"-gnatn" -- Enable inlining
          );
   [...]

In the main GPR file, "with" the crate config GPR and use the Ada_Compiler_Switches variable to define compiler switches:

with "config/my_crate_config.gpr";
project My_Crate is

   [...]

   package Compiler is
      for Default_Switches ("Ada") use My_Crate_Config.Ada_Compiler_Switches;
   end Compiler;

Compiler versions and cross-compilers

Dependencies in Alire are used also to deal with compiler versions and cross-compilers. Also related is the information on toolchains available in the Toolchain management document or via alr help toolchains.

Excluding compiler versions

One may know that a particular compiler version has a problem with some code. This may be expressed with dependencies on the generic gnat crate, which although is not found in the catalog, is a crate that all GNAT compilers provide. (Such a crate without actual releases, but provided by other crates, is called an abstract crate.) For example:

gnat = ">=7"   # We require a minimum compiler version
gnat = "/=7.3" # We know a precise version is incompatible

Since only one dependency on a same crate may appear, the relational operators & (and), | (or) can be used instead:

[[depends-on]]
gnat = "/=7.3 & >=7"

Requesting a compiler for a concrete target

The other use of compiler dependencies is to specify that a compiler for a particular target is needed. (Note that the project file also has to specify the proper target and runtime.) This way Alire can configure the appropriate environment for the build. For example:

gnat_arm_elf = "*" # Any compiler targeting ARM

Dependencies on cross-compilers should only be used in crates that actually require a concrete target (e.g., final binaries) to avoid preventing their use as general libraries with any compiler.

Further reading

You can inspect index files to get an idea of how projects are included into the catalog.