Managing Build Variants

Building different variants of an application is often necessary for various reasons. Here are a few examples:

TC inheritance can be one way to structure TCs to make it possible to build different application variants while reducing duplication of TC properties. Use of variables or JavaScript expressions in TC properties is another option. However, none of these mechanisms have proven sufficient for large models with many build variants. With an increasing number of ways to parameterize a build, the number of possible build variants grows very quickly. Defining a new build variant may require a large number of TCs to either be created or updated. This soon becomes tedious, and is also a usability problem for users that have a huge number of TCs to choose from when deciding what to build.

The model compiler provides a solution for this problem that allows you to only have one set of TCs that are common for all variants of an application that need to be built. The idea is to only store properties that are common for all build variants in the TCs, and dynamically add or modify the TC properties that are specific for a particular build variant. In essence this makes it possible to create the variants of a TC dynamically at build-time without having to manually create a TC file for each and every build variant.

The dynamic manipulation of TC properties is achieved by means of writing one or many scripts (using JavaScript) that are run by the model compiler. Such scripts can be run either just before ("pre-processing") or just after ("post-processing") the default interpretation of TC properties. Depending on the kind of build variant you want to implement you can choose to write "pre-processing" or "post-processing" scripts (or both). In a pre-processing script you can for example assign values to variables that are referenced in TC properties, while in a post-processing script you can directly modify the properties in the built TC or its prerequisites. All modifications performed by the scripts are transient, which means that they will never be stored in a TC file, but will only be used in the current build.

The model compiler provides a Transformation Configuration Framework (TCF) which is a JavaScript API for working with TCs. It provides functions for reading and writing TC properties, traversing prerequisite TCs, working with TC inheritance and much more. Together with the built-in functionality of JavaScript this provides for a very flexible and powerful way of dynamically manipulating TCs in order to build desired application variants.

There is also another JavaScript API, known as the Build Variant Framework (BVF), which allows to define which build variants to make available for users when they build a TC (either from the user interface or command-line). The idea here is that an advanced user (a build expert) writes the scripts that implement the different build variants, and also defines the user interface with the controls other users will see when they build a TC. Each value set for those controls in the user interface maps to the execution of one or two scripts at build-time (pre-process script, post-process script or both).

To enable the support for build variants, set the preference RealTime Development – Build/Transformations – C++ – Use build variants for build configuration. Then set the Build variants preference to reference a build variant script. This script will be interpreted by Model RealTime when an interactive build is performed. It can use the BVF API to contribute a custom user interface where choices can be made for defining which variant of the application that should be built. For example:

Let's look at an example of what such a build variants script could look like in order to provide a user-interface with two controls (a checkbox and a dropdown menu) that appears when a TC is built.

let debug = {
   name: 'Debug',
   script: 'debug.js',
   control : { kind: 'checkbox' },
   defaultValue : false,
   description: 'Build for debugging'
};

let target = {
   name: 'Target',
   alternatives: [
    {  
      name: 'Solaris', 
      script: 'Target.js',
      args: [ 'Solaris' ], 
      description: 'Settings for Solaris target platform' 
    },
    { 
      name: 'Linux', 
      script: 'Target.js', 
      args: [ 'Linux' ], 
      defaultValue: true, 
      description: 'Settings for Linux target platform' 
    }, 
    { 
      name: 'Win64', 
      script: 'Target.js',
      args: [ 'Windows' ], 
      description: 'Build settings for Windows 64bit' 
    }
   ]
}

function initBuildVariants(tc) {
  BVF.add(debug, target)
}

The function initBuildVariants is called when a TC is built (either from the Model RealTime user interface or from the command-line). It is responsible for creating objects that represent the build variants and adding them by calling the function add() on the predefined BVF object. The build variant objects define two controls in the user interface. The user interface controls are defined using JavaScript objects (debug and target). Some properties of such an object define what the user interface control should look like (for example, if it should be a checkbox or a drop-down list). Other properties define what should happen during the build when that particular build variant is enabled. In the above example, the build variants script contribute one "Debug" checkbox and one "Target" drop-down menu to the user interface. Each of these controls is bound to the execution of a build variant script by means of the "script" property.

Here is what the user interface will look like when you build a TC with this build variants script enabled:

The controls after the separator in the Build Variants group are contributed by the build variants script. When pressing the Build button the TC will first be processed in the usual way. Then the scripts associated with the user interface controls will be invoked so they can modify the TC properties in order to achieve the specified build customization. If choices are made according to the picture above, the script "Target.js" will be invoked with the array [ "Linux" ] as argument (according to the "args" property for the Target object).

Each combination of choices made in this dialog defines a set of enabled build variants. We call that set a build configuration. Even with only the two simple controls of the above example, we have already defined 2 * 3 = 6 possible build configurations. Each build configuration builds one specific variant of the application.

If you need to build a particular build configuration more frequently than others you can save it by pressing the "Save As" button in the dialog. Give the build configuration a descriptive name. For example:

Now you can build this particular build configuration simply by choosing it from the list of build configurations in the dialog.

Named build configurations are also listed in the Build Active TC button menu so that you can build them quickly. The menu shows both which TC that will be built, and which build configuration that will be used for building it.

You can change the active build configuration either by means of the "Configure Build Variants for …" command in this menu, or by marking the checkbox in the build dialog:

When you build a TC with an active build configuration, the dialog with the build variants user interface does not pop up. This means that once you have decided which build configuration to use, you can build your TCs in the normal way without having to bother with any extra steps.

Each build configuration has a textual representation that consists of a semicolon-separated list of build variants, where each boolean build variant (checkbox in the user-interface) is identified by its name, and each enumeration build variant (drop-down menu in the user interface) is identified by its name followed by an equal sign and then followed by the name of one of its alternatives. For example: "Debug; Target=Linux". You can use this textual representation to specify which build configuration to use when running a command-line build. Use the --buildConfig parameter when invoking the model compiler.

Build Variant and Transformation Configuration Framework APIs

To be able to write a build variants script and the scripts it references, you need to have a basic knowledge of JavaScript. Being widely used in many applications domains it is easy to find good tutorials about JavaScript on the web. Here is one example.

In addition to the built-in JavaScript functions you will use the Build Variant Framework API for defining the available build variants, and the Transformation Configuration Framework API for working with transformation configurations inside the build variant scripts. These APIs are described in the built-in Help at Model RealTime Java APIs – Model RealTime Transformation Developer's Guide - Reference - API Reference - Transformation Configuration and Build Variants JavaScript API.

Debugging Build Variant Scripts

If the build variant scripts don't work as you have intended, you need to debug them. In simple cases it may be enough to "debug" by tracing messages to the console. You can for example use the function BVF.formatInfo() to print such messages. They will be printed to the Build Variants console if the script runs as part of an interactive build in the UI, and as a model compiler message if the script runs in the context of the model compiler.

Sometimes you may need to do real JavaScript debugging to find a problem in a build variant script. One way to do that is described in the built-in Help at Model RealTime User's Guide - Articles - Building - Build Variants - Debugging.