Developing Carbonite

Carbonite supports three platforms:

  • linux-aarch64 (tegra)

  • linux-x86_64

  • windows-x86_64

At first you might think this imposes a large burden while developing but it doesn’t have to be that way. If you choose to use Visual Studio Code it becomes much easier and requires minimal configuration. The configuration for VS Code is already committed and maintained in the repo. Since we use packman for all dependencies, including development toolchains, all paths are relative to the workspace which is key to allowing us to commit a .vscode configuration that works for everybody. These are the steps you need to perform:

  1. Download VS Code and install it.

  2. If you plan to make code changes to Carbonite and submit merge requests you should fork the repo, rather than using the root fork. A full guide can be found at Forking Carbonite.

  3. Open up the Carbonite folder in VS Code using “Open folder…”

  4. Install recommended extensions. The popup will appear upon first opening of the Carbonite folder. If you missed it, activate the command palette via [CTRL]+[SHIFT]+[P] and start typing “show recommended extensions”.

Before you make code changes you should create a task branch. This is easy to do from within VS Code.

  1. Activate the command palette via [CTRL]+[SHIFT]+[P]

  2. Type “create branch” and hit enter

  3. Enter a name for your task branch

You can now make code changes. It’s preferable that you write unit tests to cover any new features you have added. Please consult testing guide for details on how to do that. If you plan to contribute to Carbonite (thank you) please consult Contributing to Carbonite.

To build your changes use [CTRL]+[SHIFT]+[B]. For those that prefer [F7] for this action please select File → Preferences → Keyboard Shortcuts and change the mapping there.

Once your changes have been built you now want to run the unit tests to verify they work. Press [CTRL]+[SHIFT]+[D] and select from the drop down menu the target to debug, typically “(Linux) test.unit” for Linux and “(Windows) test.unit” for Windows. Once this selection has been made you can always launch debugging by pressing [F5] (and terminate via [SHIFT]+[F5]).

When you are happy with the changes and ready to commit use [CTRL]+[SHIFT]+[G] to stage and diff. Before you commit run clang-format on your staged changes. This is really easy to do, just execute ./format_code.sh or format_code.bat from the root of the repo. You can now review the changes made by the formatting process by diffing them against your staged changes. clang-format will often compact a list of array initializers into something unreadable so we commonly reject those changes and put // clang-format off and // clang-format on guards around those.

After accepting the formatting changes you are ready to commit. Your newly created branch can be published to your fork by pressing the cloud publish icon in the lower left corner, next to the branch name.

If your branch is ready to be merged into Carbonite create a merge request using the GitLab web UI from your fork’s main page.

That’s it. It works the same for all the targets on all the platforms.

Adding a New Project

All of Carbonite’s projects and build information (and those of all Carbonite based apps) are controlled through a single platform independent build script called premake5.lua. This is located in the root of the Carbonite tree and uses the premake project creation toolchain. Any new projects or changes to existing projects must be listed in this file. For most projects, all source and header files found within a project’s directory will be automatically added to that project.

When a new project (ie: an example app, test app, new plugin, etc) needs to be created, it will need to be added to the premake5.lua script. In most cases, this can be accomplished by copying the project layout for another simple project and changing some parts of it (ie: the project name, folder, link dependencies, etc). By default, a new Carbonite project will add all .cpp and .h files found in the new project’s source folder to the project.

For many new plugin projects, defining a new one would only require a couple lines defining the source and header file directories for the new plugin:

project "carb.myawesomeplugin.plugin"
    define_plugin {
        ifaces = "include/carb/myawesomeplugin",
        impl = "source/plugins/carb.myawesomeplugin"
    }

Some additional information would have to be added if the new plugin depends on a third party library or SDK package as well. This would include libdirs and links lines, as well as includedirs lines. Handling platform specific differences for various libraries, headers, and compiler settings can be done using filter {} sections.

Adding a new executable app project is a little more involved and requires several different sections. As an example:

project "example.awesomeplugin"
    kind "ConsoleApp"
    location (workspaceDir.."/%{prj.name}")
    links { "carb" }
    dependson { "carb.myawesomeplugin" }
    files { "source/examples/example.awesomeplugin/*.*" }
    vpaths { [''] = "source/examples/example.awesomeplugin/*.*" }
    repo_build.copy_to_targetdir('source/examples/example.awesomeplugin/example.awesomeplugin.config.*')

Most of the lines in this example project are self explanatory or documented parts of the premake tool. Some others are parts that are some specifically for the Carbonite project setup. These include:

  • location (workspaceDir.."/%{prj.name}"): this specifies the location of the project files for this app (ie: the MSVC project files or makefiles).

  • links { "carb" }: this gives the example app a dependency on the Carbonite framework module. This is a required part of every Carbonite app and must be loaded before any other Carbonite plugin can be loaded. Other libraries may also linked to the app here or on other links lines. If other external link libraries are also needed, libdirs and includedirs lines may also be needed to provide search paths for the library and header files for them.

  • dependson { "carb.myawesomeplugin" }: this specifies the set of Carbonite plugins that this app has a hard dependency on. Without these plugins present, this app will not function properly. This does not necessarily have to be the specific plugin related to an example or test app. While this doesn’t create a direct link dependency for the app, it does ensure the named plugins themselves are built before building this app.

  • files { "source/examples/example.awesomeplugin/*.*" }: this specifies the location of the app’s source files. These paths may use a **.* wildcard to specify that source and header files from a full folder hierarchy should be included as well.

  • vpaths { [''] = "source/examples/example.awesomeplugin/*.*" }: this specifies the relative paths for all of the source and header files listed in the project. This does not affect how files are built, but only how they are displayed in an IDE’s project layout. If this line is missing, the files will still be added to the project properly and it will still build, however, the project folder structure leading to each file may be displayed in an odd manner in the IDE (ie: listed under the “..” folder).

  • repo_build.copy_to_targetdir(): this copies all matching files to the build output directory on each successful build. This is useful to ensure that the most recent versions of all non-source files are copied to the output folder on each build. This is most commonly used for an app’s config files.

Premake Notes

  • When defining prebuild commands under premake using the prebuildcommands{} command, the behaviour of the generated script differs depending on both platform and how the build is run:

    • On Windows or Linux when run under build.{bat|sh}: the commands in the script are run as a sub-shell of the shell that had previously run various other build steps. This means that any previously defined environment variables are still available to the prebuild command. The environment variables exported by packman are often used in this way. Prebuild commands run in this way often ‘just work’ but are typically making some assumptions about availability of various values.

    • On Windows when run under MSVC: the commands in the script are run in a fresh shell that only inherits the system default environment and Visual Studio’s environment variables. This means that none of the variables exported by tools like packman or repo will be available in MSVC. The prebuild script itself is responsible for ensuring those variables are present and available first.

    • On Linux when run under build.sh: the gmake2 tool runs each command in the prebuild script in its own sub-shell. This means that each command either needs to be on a single line separated by semicolons (;), or each line must be able to function as an independent command. If the script requires environment variables or some other persistent state (ie: running a python app which requires PYTHONHOME and PYTHONPATH to be defined), all commands for the script must be on a single line. If possible, it is best to encapsulate the entire script in a file on disk and simply make the prebuild script execute that.