Sunday, May 20, 2018

Introducing MultiBuilder

When I'm working on OmniThreadLibrary, I have to keep backwards compatibility in mind. And man, is that hard! OmniThreadLibrary supports every Delphi from 2007 onward and that means lots of IFDEFs and some ugly hacks.

Typically I develop new stuff on Berlin or Tokyo and then occasionally start a batch script that tests if everything compiles and runs unit tests for all supported platforms in parallel. (I wrote about that system two years ago in article Setting Up a Parallel Build System.) Dealing with 14 DOS windows, each showing compilation log, is cumbersome, though, and that's why I do this step entirely too infrequently.

For quite some time I wanted to write a simple framework that would put my homebrew build batch into a more formal framework and which would display compilation results in a nicer way. Well, this weekend I had some time and I sat down and put together just that - a MultiBuilder. I can promise that it will be extensively used in development of OmniThreadLibrary v4. (Which, incidentally, will drop support for 2007 to XE. You've been notified.)

The rest of this post represents a short documentation for the project, taken from its GitHub page.

I don't plan to spend much time on this project. If you find it useful and if you would like to make it better, go ahead! Make the changes, create a pull request. I'll be very happy to consider all improvements.

EDIT 2018-05-24: Immediately after initial release the documentation below became outdated. Be sure to check the current documentation on the GitHub page if you plan to use MultiBuilder.


MultiBuilder is a simple tool that allows you to run a set of programs on a different configurations (called environments in the program) at a same time. Results from programs that fail execution are presented for inspection.
Currently, MultiBuilder can be compiled for Windows.
If you are in a hurry and just need compiled binary, you can grab it here.


Execution environment consists of two parts - a configuration file defining all execution environments (consisting mostly of variable definitions) and a configuration file defining the project (mostly describing programs that should be executed). It is possible to use one generic environment configuration with different project configurations.


Environment definitions are stored in a INI-style configuration file with extension .mbenv. Default environment configuration file is stored in C:\Users\Public\Documents\multibuilder.mbenv. You can use a different configuration file by providing its name as a command-line parameter or by clicking the Load Environment button. Edit Environment button opens up a simple text editor where you can modify the configuration file.
Environment configuration file is split into sections. Besides the special section named Global you can define any number of sections, each describing its own environment. The following example shows environment file used for testing OmniThreadLibrary.
[Delphi 2007]
[Delphi 2009]
[Delphi 2010] *
[Delphi XE]
[Delphi XE2]
[Delphi XE3]
[Delphi XE4]
[Delphi XE5]
[Delphi XE6]
[Delphi XE7]
[Delphi XE8]
[Delphi 10 Seattle]
[Delphi 10.1 Berlin]
[Delphi 10.2 Tokyo]
OmniThreadLibrary supports Delphi compilers from version 2007 to 10.2 Tokyo and this configuration file reflects that. Each environment section defines variable Path. We'll see in the section Project below how this variable is used in practice.
Special section Global defines variables that are defined for all environments. In this example it defines variable Scratch that is used in the project file and special directive ForceDir specifying folder that MultiBuilder should create before the project is executed. (For more information see Running projects below.)
Special macro $(EnvironmentName) always expands to the name of environment settings used to run the project. In the example above, this macro could expand to Delphi 2007, Delphi XE5, Delphi 10.1 Berlin, and so on.


Project definition is stored in a INI-style configuration file with extension .mbproj. There is no default project configuration file. You can provide it via the command-line parameter or by clicking the Load Project button. Edit Project button opens up a simple text editor where you can modify the project file.
Project configuration file is split into sections. Special section Global defines commands that are always executed. Sections with names corresponding to environment names contain commands that are only executed in that environment. If a section for specific environment doesn't exist in the project file, commands from the special Default section will be used. (For more information see Running projects below.)
The following example shows project file used for unit-testing OmniThreadLibrary.
Cmd=$(Path)\dcc32.exe CompileAllUnits -b -u..;..\src;....\fastmm -i.. -nsSystem;System.Win;Winapi;Vcl;Vcl.Imaging;Vcl.Samples;Data;Xml -e"$(Scratch)\exe" -n0"$(Scratch)\dcu\win32" -dCONSOLE_TESTRUNNER
Cmd=$(Path)\dcc32.exe TestRunner -b -u..;..\src;....\fastmm -i.. -nsSystem;System.Win;Winapi;Vcl;Vcl.Imaging;Vcl.Samples;Data;Xml -e"$(Scratch)\exe" -n0"$(Scratch)\dcu\win32" -dCONSOLE_TESTRUNNER
Cmd=$(Path)\dcc64.exe CompileAllUnits -b -u..;..\src;....\fastmm -i.. -nsSystem;System.Win;Winapi;Vcl;Vcl.Imaging;Vcl.Samples;Data;Xml -e"$(Scratch)\exe" -n0"$(Scratch)\dcu\win64" -dCONSOLE_TESTRUNNER
Cmd=$(Path)\dcc64.exe TestRunner -b -u..;..\src;....\fastmm -i.. -nsSystem;System.Win;Winapi;Vcl;Vcl.Imaging;Vcl.Samples;Data;Xml -e"$(Scratch)\exe" -n0"$(Scratch)\dcu\win64" -dCONSOLE_TESTRUNNER
[Delphi 2007]
[Delphi 2009]
[Delphi 2010]
[Delphi XE]
Section Global specifies a folder which is used as a working directory for each command. Commands are all introduced with key Cmd. All other keys are ignored.

Running projects

If you click the Run All button or press the F9 key, the current project is started in all environments.
If an environment is selected and you click the Run Selected button or press the Alt+F9 key combo, the project is started in selected environment.
Each environment executes the project in its own thread. Therefore you can select one project, click Run Selected, repeat that with another projects, and both will be executed in parallel.
In each environment the project is executed by following these steps:
  • Environment-specific section is located in the project configuration file. If it is not found, the special section Default is used instead. (See example below.)
  • ForceDir setting is read from the environment section of the environment configuration file. If it is not found there, it is read from the Global section of the same file.
  • Macros in the ForceDir settings are expanded. (See Macro expansion below.)
  • All folders in the ForceDir list (semicolon-delimited) are created.
  • Folder setting is read from the environment-specific section. If it is not found there, it is read from the Global section.
  • Macros in the Folder setting are expanded. (See Macro expansion below.)
  • For each Cmd setting in the Global section the following steps are executed:
    • Macros in the setting are expanded. Be careful to use double-quotes at appropriate places if a macro expansion can contain a space characted. (See Macro expansion below.)
    • Program is executed in the Folder directory.
    • If program cannot be started or if it exits with a non-zero exit code, execution stops immediately.
    • Otherwise the execution continues with the next Cmd setting.
  • For each *Cmd setting in the environment-specific section the same steps are executed.
For example, if we use the OmniThreadLibrary configurations mentioned above, environment Delphi 2007 would execute all commands from the Global section. As there exists a Delphi 2007 section, commands from the Default section are ignored. Delphi 2007 section contains just one setting with key Null, which is ignores. (At least one setting must be stored in a section, otherwise the information about that section is lost when configuration file is loaded.)
Environment Delphi 10.1 Berlin, on the other hand, would firstly execute all commands from the Global section, followed by all commands from the Default section.
Following command can be used to start MultiBuilder and load appropriate environment and project configurations:
Multibuilder.exe SmokeTest.mbenv SmokeTest.mbproj

Macro expansion

To request macro expansion, variable name must be wrapped into $( and ). For example, the following two lines from an environment configuration file define variable Scratch that refers to macro $(EnvironmentName) and special command ForceDir refers to macro $(Scratch).
For example, when running the OmniThreadLibrary project in the Delphi 2010 environment, these two settings would expand to:
Scratch=c:\0\MultiBuilder\Delphi 2010
ForceDir=c:\0\MultiBuilder\Delphi 2010\exe;c:\0\MultiBuilder\Delphi 2010\dcu;c:\0\MultiBuilder\Delphi 2010\dcu\win32;$(Scratch)\dcu\win64
Except when referring to a special variable EnvironmentName, macro expander first tries to find the variable in the currntly active environment section and if that fails it uses the value from the Global section.
Variables can only be defined in the environment configuration file.

User interface

The list on the left contains all environments. An icon left to the environment name indicates the status of operation in this environment.

☑ Indicates that the project run without a problem.
☒ Indicates that problems were encountered.
⏵ Indicates that the project is still executing.

The editor on the right shows problems in the selected environment (Delphi XE2). First line contains exit code [1] and program line d:\Delphi\9.0\bin\dcc32.exe CompileAllUnits .... It is followed by one empty line and the output of the program.


I'll gladly accept any pull request that makes this project better and more versatile. If you would like to contribute but don't know where to start, check the Issues.

1 comment:

  1. Can it have different goals/targets within a project? Like, core library / ide experts / stabestand utils

    I just compare with "want" used for for example building/installing CnWizards