Skip to content

Project Structure and Build System

A Termina program is organized as a project: a directory with a fixed layout, a configuration file, and the source modules that make up the application. This chapter describes that layout, the configuration file that governs it, and the commands that turn the source into a running program.

Creating a project

A project is created with the new command, which takes the project name and, optionally, the target platform with -p or --platform. When no platform is given, the project is created for posix-gcc, the development platform:

$ termina new hello_world

The command generates the project directory with its initial structure:

hello_world/
├── .gitignore
├── termina.yaml
├── app/
│   └── app.fin
├── src/
└── output/

The command also initializes a Git repository in the new directory, unless the directory already lies inside one, and writes a .gitignore that excludes the built executables in output/bin/. The option --vcs none skips the repository, although the .gitignore is still written. The option --vscode adds the files that Visual Studio Code needs to work on the project inside the Termina development container: .devcontainer/devcontainer.json, which names the container image, .vscode/tasks.json, with a termina: build task, and .vscode/launch.json, with a debug configuration for posix-gcc.

The project layout

The file termina.yaml holds the project configuration. The app directory contains the application module, app.fin, where the system is assembled. The src directory holds the source modules that define the component classes and functions of the program. The output directory receives the C code that the transpiler generates, and is empty until the project is built.

The modules under src are organized in subdirectories, and the directory structure mirrors the dotted paths used in import declarations. A module at src/resources/counter.fin is imported as resources.counter, and one at src/common/types.fin as common.types. A module always belongs to a subdirectory; the root of src holds directories, not modules.

The configuration file

The termina.yaml file records the settings the transpiler needs. A freshly created project contains the following:

builder: make
efp-folder: efp
name: hello_world
output-folder: output
platform: posix-gcc
source-modules: src

The name is the name of the project, and platform is the target it is built for. The remaining entries name the parts of the layout: the folder of the source modules, the folder for the generated output, the folder reserved for analysis support files, and the build tool used to compile that output. The application module has no entry of its own, since it is always app/app.fin.

The optional profile entry selects how the output is built: release, the default, or debug, which ties the generated code to the Termina sources for the debugger, as the chapter on debugging describes.

Some features of the runtime are switched on here as well. Setting enable-system-port to true deploys the system_entry resource that provides the system interface, and the platform-flags section enables platform-specific sources, such as the keyboard interrupt of the posix-gcc platform:

enable-system-port: true
platform-flags:
  posix-gcc:
    enable-kbd-irq: true

The keys of platform-flags name platforms. The flags under a key that names no platform would be dropped without notice, so such a key is rejected.

A project that calls C code keeps it in an extern directory at its root and lists it in an extern section of termina.yaml, as the chapter on calling C code describes.

Building a project

Building a project is a two-stage process: the transpiler turns the Termina sources into C, and the platform's build system turns that C into a program.

Building a project
The two build stages: termina build, then make

The first stage is transpilation, carried out by the build command from the project's root directory:

$ termina build

The transpiler reads the application module and the source modules, checks the program, and writes the resulting C into the output directory. The output mirrors the source: a src tree of C files, an include tree of headers, and a Makefile that compiles them against the Operating System Abstraction Layer for the configured platform.

The second stage compiles that generated code. On the posix-gcc platform, this is done with make from inside the output directory, and it produces an executable in bin that can be run directly:

$ cd output
$ make
$ ./bin/hello_world

For an embedded target, the same two stages apply, but the compilation uses the cross-toolchain of the chosen platform, and the result is an image to be loaded onto the device or an emulator rather than an executable for the host.

The generated Makefile includes platform/common.mk from the OSAL, which compiles every platform in C11 with -pedantic-errors and a set of warnings beyond -Wall and -Wextra, among them -Wconversion and -Wshadow. The code the transpiler emits is expected to compile without warnings under those flags, so a warning comes either from the transpiler or from C code written by hand, such as a platform driver.

Static analysis

The same Makefile provides two targets that run Cppcheck over the generated sources. The cppcheck target runs the analysis with the warning, style, performance and portability checks enabled, and cppcheck-misra adds the MISRA addon of Cppcheck, restricted to the mandatory and required rules of MISRA C:

$ cd output
$ make cppcheck
$ make cppcheck-misra

The OSAL is analyzed in its own repository, so both targets analyze only the sources of the project. Each target keeps its results in a directory of its own under the build directory, and a second run only analyzes the files that have changed. The targets report their findings without failing; setting CPPCHECK_ERROR_EXITCODE=1 on the make command line makes them fail when there is any, which is the setting for a continuous integration job. The Docker image ships Cppcheck, whereas an installation from source requires it on the host.

Findings of the MISRA addon are identified by the number of the rule, such as misra-c2012-11.5. A class member receives its instance as a void * and converts it into a pointer to its class, so every project gets findings under Rule 11.5 that come from the way the generated code is built.

Platforms

The posix-gcc platform builds the application for a conventional Unix-like host, using the system's own compiler, and is intended for development and validation. The embedded platforms are rtems5-leon3sf-nexysa7, for the LEON3 processor under RTEMS 5; rtems6-zynq7000-pynqz2, for the Zynq-7000 of the PYNQ-Z2 board under RTEMS 6; and freertos10-stm32l432-nucleol432kc, for the STM32L432 microcontroller under FreeRTOS 10. The platform is chosen when the project is created, with the --platform option of the new command, or changed afterwards by editing the platform field in termina.yaml. Each target beyond posix-gcc requires its cross-toolchain to be installed, as described in the chapter on installation.