Project description
A project is a named set of components and sub-projects. It carries no data of its own, because the data belongs to the components that produce and consume it; what it carries is the answer to the only question a component cannot answer alone - which components are supposed to fit together. That question has no single answer in a code base where several images are built out of an overlapping set of components, which is why the membership lives in a file of its own rather than in the components, and why a component never names the project it belongs to.
The project file is what a build script points DDD at, and it is the file whose name ends up
on the generated a2l. examples/demo/demo.ddd.json is a complete one:
{
"project": {
"name": "DemoDevice",
"description": "Demonstration project showing every DDD feature",
"includes": [
"components/*.ddd.json",
"subsystems/logging/logging.ddd.json"
]
}
}
$ ddd check demo.ddd.json
ok: 20 variables in 4 components are consistent
The keys
key |
default |
meaning |
|---|---|---|
|
required |
Identifier of the project. It is not decoration: the generated a2l file is called
|
|
|
Free text. It becomes the long identifier of the a2l |
|
|
Paths to the component, types, units, sections, constants and sub-project files that make up the project, relative to this file. Wildcards are expanded. |
The identifier rule is tighter than what a c compiler would accept, and deliberately so: ASAP2 1.6.1 limits an identifier to 128 characters, and a name DDD cannot write into the a2l is of no use in a project that generates one. The tighter of the two rules is the one that is enforced everywhere, so a name that passes the check cannot fail later in a backend.
An includes list may be left out entirely. {"project": {"name": "Bare"}} is a valid
project with no members, and DDD says so rather than treating it as a mistake:
$ ddd check bare.ddd.json
ok: 0 variables in 0 components are consistent
includes
Each entry of includes is a path, and every path in a description file is relative to
the file that contains it rather than to the current directory. That is what makes a
project relocatable: a subsystem can be moved, vendored or included as a git submodule
without a single path being rewritten, and two people running the tool from different
directories read the same set of files. An absolute path is taken as it stands, for the case
where a component genuinely lives outside the project tree.
Wildcards
Shell style wildcards are expanded: * for any part of a name, ? for a single
character, and ** for any number of directory levels. With components/*.ddd.json,
adding a component to the project is a matter of adding its file - nobody has to remember to
edit the project as well, and nobody can forget to.
{
"project": {
"name": "Wildcards",
"includes": ["**/*.ddd.json", "c/comp?.ddd.json"]
}
}
$ ddd check top.ddd.json
ok: 2 variables in 2 components are consistent
One file is deliberately left out of a pattern’s matches: the project file that contains the
pattern. It sits exactly where a
*.ddd.json pattern next to the project would sweep it up, and it is not an include - a
project including itself would be a cycle. Excluding it is what lets the shortest possible
project file work at all.
A pattern that matches nothing is reported, because the usual cause is a directory that was renamed or a component that never arrived:
$ ddd check empty.ddd.json
empty.ddd.json#project.includes[0]: error[include-empty]: pattern 'variants/*.ddd.json' matches no file
1 error
include-empty is an error by default but can be relaxed, because a pattern that is
legitimately empty in one variant of a project is a normal thing to allow - a project built
for several targets may have an optional/*.ddd.json that only some of them fill.
Components and sub-projects in the same list
includes does not distinguish between components and other projects. The kind of every
included file is detected from its top level key, so a subsystem developed by a separate team
can be delivered as a project of its own and pulled in as a single entry. The demo does
exactly that: subsystems/logging/logging.ddd.json is a project, and it brings its own
component with it.
{
"project": {
"name": "Logging",
"description": "Sub project of its own, developed by a separate team",
"includes": [
"event_logger.ddd.json"
]
}
}
The nesting has no depth limit and no effect on the result: the components of a sub-project become components of the including project, and are checked against all the others exactly as if they had been listed directly. What the sub-project keeps is its own file layout and its own relative paths, which is the point - it can be checked and delivered on its own, and it does not need to know who includes it.
A file reached twice is loaded once
Include graphs in a real project are rarely trees. Two subsystems commonly share a component, and both list it; a wildcard in one project and an explicit path in another commonly land on the same file. DDD resolves every path to a canonical absolute form before using it, so the two spellings are recognised as the same file and it is loaded once:
{ "project": { "name": "Top", "includes": ["left.ddd.json", "right.ddd.json"] } }
{ "project": { "name": "Left", "includes": ["shared/*.ddd.json"] } }
{ "project": { "name": "Right", "includes": ["shared/common.ddd.json"] } }
$ ddd check top.ddd.json
ok: 1 variable in 1 component are consistent
Without that rule the component would be loaded twice under the same name and the run would
fail with duplicate-component - a finding about the shape of the include graph rather than
about anything wrong with the project. Diamond shaped graphs are therefore not merely
tolerated but expected.
The check that does fire is the one that matters: two different files declaring the same component name. That is a real collision, since a component name decides the name of the generated header, and the second file would silently overwrite the first:
$ ddd check project.ddd.json
second.ddd.json#component: error[duplicate-component]: component 'Sensing' is already declared
note: first.ddd.json#component: first declared here
1 error
Cycles are reported, not followed
A project that includes a project that includes the first one is a mistake that is easy to make when subsystems are refactored, and it is one a naive loader would answer with a hang or a stack overflow. DDD carries the chain of projects it is currently inside, and reports the cycle as an ordinary finding, naming the whole chain so that the entry to remove is obvious:
$ ddd check a.ddd.json
b.ddd.json#project.includes[0]: error[include-cycle]: include cycle: a.ddd.json -> b.ddd.json -> a.ddd.json
1 error
include-cycle is one of the checks whose severity cannot be relaxed. The others are
relaxable because a project may reasonably decide it can live with the finding; there is no
way to live with a cycle, since following it is the only alternative and it does not
terminate.
What the project is built out of
The set of files that were actually read is more than the project file, and a build system
needs all of them to know when the generated artefacts are out of date. ddd sources prints
them, one absolute path per line, sorted and without duplicates - the root file, every
sub-project and every component:
$ ddd sources demo.ddd.json
/home/you/ddd/examples/demo/components/controller.ddd.json
/home/you/ddd/examples/demo/components/sensor_hub.ddd.json
/home/you/ddd/examples/demo/components/user_interface.ddd.json
/home/you/ddd/examples/demo/demo.ddd.json
/home/you/ddd/examples/demo/subsystems/logging/event_logger.ddd.json
/home/you/ddd/examples/demo/subsystems/logging/logging.ddd.json
This is the list that has to become the dependency list of the generation step. Depending on the project file alone would be wrong in the direction that hurts: editing a component would not regenerate anything, and the build would carry on with headers describing a project that no longer exists. The cmake integration wires this up on its own.
Note
ddd check and ddd generate accept a component file wherever they accept a
project file, which is what lets a component be checked before it is integrated. There is
no project around it, so the inputs it declares have no producer; add
-W missing-producer=ignore for that run. The generated a2l is then named after the
component instead of after a project.