Tutorial

There are two sorts of things that yotta builds: modules, and executables. Modules are re-usable, and the source code for them is distributed in the yotta registry. Executables are stand-alone programs that depend on modules, and which are not normally published themselves.

We’ll look at creating and publishing a module, then using that module in a simple command line executable.

This tutorial assumes your are building and running natively on OS X or Linux, For a guide to writing cross-compiled programs for mbed OS using yotta, see the mbed OS documentation.

Creating a Module

Before creating a module you should always check the yotta registry to see if another module already exists that does the same thing. If it does, think about submitting a pull-request to that module first.

Let’s create a module that provides a really simple logging framework:

yotta init

First create a directory with the name we want, and cd into it.

# in these code examples, lines that you type have "> " at the beginning, other
# lines are output from the running program:
> mkdir simplelog
> cd simplelog

yotta init, will create the module description for us, after asking a few questions. It’ll provide some helpful defaults, too.

> yotta init
Enter the module name: <simplelog> 
Enter the initial version: <0.0.0> 
Is this an executable (instead of a re-usable library module)? <no> 
Short description: Really simple logging.
Keywords:  <> logging, simple, example
Author: James Crosby <james.crosby@arm.com>
Repository url (where people can submit bugfixes): ssh://git@github.com/ARMmbed/simplelog
Homepage: http://github.com/ARMmbed/simplelog
What is the license for this project (Apache-2.0, ISC, MIT etc.)?  <Apache-2.0> 
> 

Note: you can use any valid SPDX license expression for the license of your module, although simple permissive open-source licenses like Apache-2.0 are encouraged, and will make it easier for people to use your module and contribute improvements.

Module Structure

yotta init also creates some directories for us to put things in:

> ls -l
total 4
-rw-r--r--  1 james  staff   420B 15 Sep 16:55 module.json
drwxr-xr-x  4 james  staff   136B 15 Sep 16:35 simplelog
drwxr-xr-x  4 james  staff   136B 15 Sep 16:41 source
drwxr-xr-x  4 james  staff   136B 15 Sep 16:41 test

Any source files placed in the source directory will be compiled automatically by yotta, when we build, and each source file placed into the test directory will be compiled into a separate executable (to learn more about testing with yotta see the testing tutorial.

Headers that define the interface to your module should be placed in the directory with the same name as your module (simplelog in this case). This pattern forces headers to be included with their full paths e.g. #include "simplelog/log.h", instead of just #include "log.h", which might be a header name that multiple different modules use.

Implement!

The implementation of our logging module is just three files, a header file, an implementation file and a test:

The header file, ./simplelog/log.h, declares the public interface to our module. It’s important to keep the public interface to the bare minimum (avoid including unnecessary headers, for example), as all users of your module (direct and indirect) will be exposed to the contents of your public header files.

For the same reason we prefix everything in the public header with the name of our module. Here we’re using the conventions that types are UpperCamelCase, functions are lowerCamelCase, and global variables are Upper_Case_Underscore_Separated. The exact conventions you use are up to you, but you must prefix all exported symbols.

// header guard: C and C++ headers must guard themselves against multiple
// inclusion.
#ifndef SIMPLELOG_LOG_H
#define SIMPLELOG_LOG_H

// make sure the header works when included from C++
#ifdef __cplusplus
extern "C" {
#endif // def __cplusplus

// Log levels: note that we prefix everything with the module name. It's OK to
// use various Casing and underscore separated conventions to denote different
// types of symbols, since module names are forced to be lower case, and cannot
// include underscores (only - characters).
enum SimpleLogLevel{
    Simple_Log_Critical = 0,
    Simple_Log_Notice   = 1,
    Simple_Log_Error    = 2,
    Simple_Log_Warning  = 3,
    Simple_Log_Info     = 4,
    Simple_Log_Debug    = 100
};

// log the message at the given level
void simpleLog(enum SimpleLogLevel level, const char* msg);

// shortcut functions, still prefixed
void simpleLogError(const char* msg);
void simpleLogWarning(const char* msg);
void simpleLogInfo(const char* msg);
void simpleLogDebug(const char* msg);

#ifdef __cplusplus
} // extern "C"
#endif // def __cplusplus

// end the header inclusion guard
#endif // ndef SIMPLELOG_LOG_H

The source file, ./source/log.c, simply implements the header, using printf to log messages:

// include the libc header for printf
#include <stdio.h>

// include our own public header
#include "simplelog/log.h"

const char* prefixForLevel(enum SimpleLogLevel level){
    if(level <= Simple_Log_Critical){
        return "[critical]";
    }else if(level <= Simple_Log_Notice){
        return "[notice]";
    }else if(level <= Simple_Log_Error){
        return "[error]";
    }else if(level <= Simple_Log_Warning){
        return "[warning]";
    }else if(level <= Simple_Log_Info){
        return "[info]";
    }else{
        return "[debug]";
    }
}

// log the message at the given level
void simpleLog(enum SimpleLogLevel level, const char* msg){
    printf("%s %s\n", prefixForLevel(level), msg);
}

void simpleLogError(const char* msg){
    return simpleLog(Simple_Log_Error, msg);
}
void simpleLogWarning(const char* msg){
    return simpleLog(Simple_Log_Warning, msg);
}
void simpleLogInfo(const char* msg){
    return simpleLog(Simple_Log_Info, msg);
}
void simpleLogDebug(const char* msg){
    return simpleLog(Simple_Log_Debug, msg);
}

After implementing these files, our module’s structure should look something like this:

├── module.json
├── simplelog
│   └── log.h
├── source
│   └── simplelog.c
└── test

yotta build

Now we have a header and implementation file, we can build our module!

> yotta build
info: generate for target: x86-osx-native 0.0.3 at /Dev/simplelog/yotta_targets/x86-osx-native
-- Configuring done
-- Generating done
-- Build files have been written to: /Dev/simplelog/build/x86-osx-native
Scanning dependencies of target simplelog
[100%] Building C object source/CMakeFiles/simplelog.dir/Dev/simplelog/source/simplelog.c.o
Linking C static library libsimplelog.a
[100%] Built target simplelog

Great, we’ve built our library! What next? Testing it, of course.

Test Test Test

Our module is not complete without a test that it’s working. For now we’ll just add a simple program, but ideally you would exercise all of your module’s API in one or more tests.

yotta build scans the ./test directory, and will build a separate executable for each source file found there, so to add our test we can add a file called ./test/basic.c with the following main() routine:

#include "simplelog/log.h"

int main(){
    simpleLogDebug("hello simplelog!");
    return 0;
}

Note that if you are compiling a program for mbed OS with yotta, then the entry point is app_start, instead of main.

Now, when we run yotta build, yotta will also build our test:

> yotta build
info: generate for target: x86-osx-native 0.0.3 at /Dev/simplelog/yotta_targets/x86-osx-native
-- Configuring done
-- Generating done
-- Build files have been written to: /Dev/simplelog/build/x86-osx-native
[ 50%] Built target simplelog
Scanning dependencies of target simplelog-test-basic
[100%] Building C object test/CMakeFiles/simplelog-basic.dir/Dev/simplelog/test/basic.c.o
Linking C executable simplelog-test-basic
[100%] Built target simplelog-test-basic

Now let’s run our test, you’ll find it at ./build/x86-osx-native/test/simplelog-test-basic (more on that path name later):

> ./build/x86-osx-native/test/simplelog-test-basic
[debug] hello simplelog!

Looks like things are working! Now, more on that path. It’s made up of three parts:

  • ./build/: the build directory, everything that yotta generates when building is placed in here.
  • ./build/x86-osx-native: this is the target that yotta was building for. If you don’t explicitly set the target, then yotta builds to run on the current computer. (If you built on a linux or windows computer x86-osx-native would be different.)
  • ./build/x86-osx-native/test/simplelog-test-basic: the path to the test. The test name is automatically prefixed with <modulename>-test- as executable names must be globally unique.

A Few Words About Targets

In ARM we use yotta to build software for embedded devices – not just desktop computers. When you’re compiling the same software for lots of different devices you need a mechanism to do different things, and often to include different dependencies, for each of the different devices.

The yotta target command lets you do this. It defaults to the system you’re building on (x86-osx-native on mac, x86-linux-native on linux, etc.) You can display the current target by running yotta target with no arguments:`

> yotta target
x86-osx-native

If you set a different target, by running yotta target <targetname>, then yotta will build code for that target instead.

yotta publish

The most important part of yotta is the ability to publish our module so other can use it. Now that we’ve tested our module, lets do that!

> yotta publish
info: generate archive extracting to "simplelog-0.0.0"
warning: no readme.md file detected
info: published latest version: 0.0.0

Now anyone can install and use our module in their own program. We should probably add some documentation, too. Do this by creating a readme.md that uses Markdown to describe what your module does.

Publishing publishes the source of the module, not the built binaries (as the binaries would only be useful to people building on exactly the same platform).

Note that if you also created a module called simplelog you won’t be able to publish it – only one module can exist in the public registry with any given name, so if you’ve created a better logging module (simplelog doesn’t make the bar very high), then you’ll need to think of a new name and change it by editing your module.json file. See the module.json reference for details.

Creating an Executable

Executables are runnable programs that depend on any number of other modules, and add their own code to build a program that does something useful. yotta isn’t designed for distributing executables, so they shouldn’t normally be published to the public registry, but it works great for building them.

yotta init

Let’s start by creating a new directory for our executable, and running yotta init again, if you previously created the simplelog module, then cd up one directory first: cd ..

> mkdir helloyotta
> cd helloyotta
helloyotta james$ yotta init
Enter the module name: <helloyotta>
...
Is this module an executable? <no> yes

Note that this time we answered yes to the last question. This makes yotta add a "bin" property in the module.json module description file that it generates. If we open this file in a text editor, we’ll see that it says to create a binary executable out of the contents of the ./source directory:

{
  "name": "helloyotta",
  ...
  "bin": "./source"
}

Add Dependencies!

Now we can tell yotta that we want to use the module we created earlier, simplelog.

> yotta install simplelog
info: get versions for x86-osx-native
info: get versions for simplelog
info: download simplelog

This has saved simplelog as a dependency in the module.json file, specifying that the version should be approximately equal to the current version. See the yotta install documentation for more details.

  "dependencies": {
    "simplelog": "~0.0.0"
  },

yotta install also creates a yotta_modules directory in the module’s directory, which it uses to store your dependencies. This, along with a corresponding yotta_targets directory, and the build directory, may be overwritten by yotta at any time, so generally shouldn’t be modified, and you should not create files in them.

Implement

When yotta builds, it will automatically arrange that our executable is linked against the dependencies we’ve specified, and that their header files are available, so we can create a single file in the ./source directory to get a working executable. For this example, use a file called ./source/hello.cpp:

#include "simplelog/log.h"

int main(){
    simpleLog(Simple_Log_Info, "Hello yotta!");
    return 0;
}

Build

Just like building a module, when we run yotta build yotta will arrange for everything necessary to be compiled, and produce a result:

> yotta build
info: generate for target: x86-osx-native 0.0.3 at /Dev/helloyotta/yotta_targets/x86-osx-native
-- Configuring done
-- Generating done
-- Build files have been written to: /Dev/helloyotta/build/x86-osx-native
Scanning dependencies of target simplelog
...
[100%] Building C object source/CMakeFiles/helloyotta.dir/Dev/helloyotta/source/hello.cpp.o
Linking C executable helloyotta
[100%] Built target helloyotta

Notice that yotta also built simplelog’s library: as modules are distributed as source, they need to be compiled too.

Our executable will have been placed in ./build/x86-osx-native/source, (or ./build/<targetname>/source if you’re compiling for a different target.). Let’s run it:

> ./build/x86-osx-native/source/helloyotta
[info] Hello yotta!

That’s it!

That’s all of the basic principles of using yotta, for more information check out the full command reference.