Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Composing Profiles

A target applies profiles from sources, and a profile builds on others and offers features, as a crate builds on crates and offers features. This is how a team builds on a shared collection instead of copying it: its own profiles require the collection’s, turn on what they need, and add what is the team’s.

Sources

.devset/config.toml names each source once, and applies profiles from them as layers, by source/profile:

[sources]
atxp  = { git = "https://github.com/atomix-labs/atxp", tag = "v0.20.0" }
house = { path = "../house-profiles" }

[[layers]]
profile  = "atxp/rust"
features = ["docs", "agents"]

[[layers]]
profile          = "house/deploy"
default-features = false

A source uses Cargo’s git-dependency fields. path alone is a directory, relative to the target; with git, it is a directory inside the repository that holds the profiles. tag, branch and rev go only with git, and at most one of them; with none, the repository’s default branch. A git URL may be a local path, as ../profiles.git, relative to the target too.

A source’s profiles are every profile.toml in it, found by their name wherever they are, so a collection may group its profiles as it likes, and move them, without breaking a target. Names are unique in a source, which devset checks when it reads one; a profile.toml inside another profile’s files/ is that profile’s payload. A source may describe itself in collection.toml:

[collection]
name        = "atxp"                                   # the name a target gives it unless it chooses another
description = "Profiles for devset, one concern each"

devset add names a source as it adds it: devset add atxp/rust --git <url> names it atxp; without a name, after its collection.toml, or else its directory. A profile named alone, as devset add book, is in the target’s only source. devset list shows each source’s profiles, their descriptions and their features.

Layers

Each active profile is a layer: the target’s own, and every profile they require. A layer is named by its profile’s name, which is what from, devset remove and devset explain take; so two active profiles may not share one, whichever sources they come from. Layers apply in order, each requirement before the profile that requires it.

Two layers that provide the same file collide, and devset refuses to guess which one wins:

$ devset add house/rust
error: fmt.toml is provided by base and rust
  |
  = help: choose one in .devset/config.toml:
              [files."fmt.toml"]
              from = "base"   # or "rust"

from is the target’s choice; Settings has the rest of what the target decides. Layers that own parts of one file share it without a choice, so long as no two own the same key, and one layer’s whole once file may start a file the others own parts of.

Requirements

[requires] names the profiles one builds on, by name:

[requires]
just      = {}                                         # in this profile's source
lychee    = { optional = true }                        # only when a feature activates it
github-ci = { features = ["pages"], default-features = false }
deny      = { git = "https://github.com/acme/profiles", tag = "v2.0.0" }

A requirement is in the requirer’s own source, at the same commit, unless it names another with git: one tag pins a whole collection. Each is a layer of its own, before the profile that requires it, so the requirer’s files come last and its choices stand. A profile required from two places is one layer. A profile with nothing but requirements is a bundle.

status folds each required layer, applied as it is, into a requires line under the layer that brings it in; devset status -v lists them all. A required layer is not the target’s to remove: devset remove takes the configured one that brings it in.

Features

[features] declares what a target, or a profile that requires this one, may turn on:

[features]
default = ["katex", "links"]
katex   = []                       # files gated on it: when = { features = ["katex"] }
api     = ["katex"]                # another feature of this profile
pages   = ["github-ci/pages"]      # a feature of a requirement, which activates it too
links   = ["dep:lychee"]           # an optional requirement, activated
agents  = ["mdbook?/agents"]       # a requirement's feature, only if the target has it anyway

Features follow Cargo’s rules:

  • Additive. A feature adds files, parts, requirements and features. It never removes or replaces: a choice between alternatives is a variable.
  • Unified. A profile is one layer, whoever asks for it: the features the target and every requirer turn on are unioned, and devset expands the graph until nothing more turns on.
  • Default. default lists the features on unless the layer, or a requirer, sets default-features = false. When a layer switches them off and a requirer turns them on, they are on, and devset warns and names the requirer.
  • Named. An unknown feature is an error, with a near match.

devset explain shows each layer’s features, and who turned each on; devset explain <layer> adds the ones it leaves off. The lock records them.

$ devset explain
atxp/ci  [pages]  (required by mdbook)
    pages  <- mdbook/pages
atxp/mdbook  [api, katex, pages]  (required by rust)
    api    <- rust
    katex  <- default, mdbook/api
    pages  <- rust/docs
atxp/rust  [docs]
    docs  <- target

devset add changes a layer’s features as cargo add changes a dependency’s: on a profile the target already applies, --features turns more on, and --no-default-features and --default-features switch its defaults. A profile active only because another requires it becomes a layer of its own when you add features to it.

devset remove <layer> --features turns off features the layer lists, and keeps the layer; a feature a requirer or the defaults turn on stays on, and devset says which.

devset add atxp/mdbook --features mermaid
devset remove mdbook --features mermaid

devset refuses a graph that cannot hold: a cycle of requirements, a profile named twice from two sources, or listed as two layers, one source at two refs, and two required profiles that provide the same file.