CBuild wiki: Creating buildscript

Basic buildscript

scripts/main.cpp, implements main() function, provided by CBuild core. It is not advisible to edit this file if you do not want to make something very special/not standard.

/**
 * <doxygen file header and license header>
 */
// C++ libraries
#include "filesystem"
#include "stdlib.h"
#include "string"
#include "vector"
// CBuild headers
#include "CBuild/CBuild.hpp"
// Userspace headers
#include "user_init.hpp"
// Rebuild call
void rebuild() {
  auto path = std::string(std::filesystem::current_path().c_str());
  path += "/scripts";
  CBuild::rebuild(path);
}
// Main function
int main(int argc, char **argv, char **envp) {
  // Run user init
  init();
  // Hold parsed command line arguments, see CBuild::parse in CBuild.cpp for
  // more details
  lib::map<std::string, std::string> args;
  // Parse arguments, and also get type of run
  CBuild::RType mode = CBuild::parse(&args, argc, argv);
  // We have some error
  if (mode == CBuild::ERROR)
    exit(0xFF);
  // If we need to rebuild
  if (mode == CBuild::REBUILD)
    rebuild();
  // Add base path
  args.push_back("curr_path",
                 std::string(std::filesystem::current_path().c_str()));
  // Run main loop of CBuild (execute given toolchain / module and exit)
  CBuild::loop(mode, &args);
  // Safe exit without errors
  return 0;
}

scripts/user_init.hpp, header used to glue all cpp files together, part of user scripts.

/**
 * <doxygen file header and license header>
 */
#ifndef __CBUILD_USER_INIT_HPP__
#define __CBUILD_USER_INIT_HPP__
void init();
#endif // __CBUILD_USER_INIT_HPP__

scripts/user_init.cpp, main buildscript file, this is place where you need to write buildscript (but it is not required). Script here builds dynamic library and executable that depends on it. Also it have compilation for windows and linux, done in very dumb way.

/**
 * <doxygen file header and license header>
 */
// C++ libraries
#include "stdio.h"
// Userspace headers
#include "user_init.hpp"
// CBuild headers
#include "CBuild/build/g++.hpp"
#include "CBuild/build/mingw-g++.hpp"
#include "CBuild/print.hpp"
#include "CBuild/register.hpp"
#include "CBuild/task/Task.hpp"

CBuild::GXX       multiplication("mull", "Multiplication test");
CBuild::GXX       multiplication_lib("mull_lib", "mull");
CBuild::MINGW_GXX multiplication_win("mullw", "Multiplication test");
CBuild::MINGW_GXX multiplication_lib_win("mullw_lib", "mull");

void init() {
  multiplication.set_standart("c++20");
  multiplication.add_file("src/main.cpp");
  multiplication.warn();
  multiplication.set_type(CBuild::EXECUTABLE);
  multiplication.depends_on("mull_lib");
  CBuild::Registry::RegisterTarget(&multiplication);
  multiplication_lib.set_standart("c++20");
  multiplication_lib.add_file("src/test.cpp");
  multiplication_lib.warn();
  multiplication_lib.set_type(CBuild::DYNAMIC_LIBRARY);
  CBuild::Registry::RegisterTarget(&multiplication_lib);
  multiplication_win.set_standart("c++20");
  multiplication_win.add_file("src/main.cpp");
  multiplication_win.warn();
  multiplication_win.set_type(CBuild::EXECUTABLE);
  multiplication_win.depends_on("mullw_lib");
  CBuild::Registry::RegisterTarget(&multiplication_win);
  multiplication_lib_win.set_standart("c++20");
  multiplication_lib_win.add_file("src/test.cpp");
  multiplication_lib_win.warn();
  multiplication_lib_win.set_type(CBuild::DYNAMIC_LIBRARY);
  CBuild::Registry::RegisterTarget(&multiplication_lib_win);
}

This is simple buildscript template. If you do not need to do some very specific tasks you simple can extend/reduce this buildscript.


Basics of CBuild external API

CBuild provide full-feathured API for specifying build attributes. It has only one problem - it is based on gcc, so expect some problems with clang (clang support will be implemented in future). If you want to see all functions of CBuild::Toolchain class please use IDE with autocompletition support or check CBuild/build/Build.hpp header.

Also CBuild provides CBuild::Task class that can be used if you need to do some things, like preprocess files, postprocess some runtime data or make some build related things that is not connected with compilation proccess. This is very simple abstract class (if you need to create task you need to create new class that use CBuild::Task as base class. If you do this you need to create constructor, that takes std::string as task id and std::vector<std::string> as list of required task ids. Also, you need to provide implementation for main task function - void call(std::vector<std::string> args), that will be called when this task is called. Here you can do anything what you want, CBuild::Task is simple abstractiong to add ability to register function in CBuild::Registry ;). In some point in future CBuild may has some basic tasks included with a core.

This all basic classes, used in CBuild. There is also CBuild::generator_base that can be registered and used to provide abilites simmilar to integrated in CBuild generators of compile_commands.json and Makefile. But for almost all buildscript this ability is not realy necessary.

Also, there is CBuild::Registry namespace, this namespace is used to register all things inside CBuild core. If your only requirments is ability to compile some c/c++ project, you need only functions that match this signature CBuild::Registry::Register...(...* arg). Look for these functions in CBuild/register.hpp file or use your IDE's autocompletition.

If you are writing simple buildscript, you dont need other CBuild functionality, but if you are interested in what things are inside CBuild headers, you can check comments inside hpp files inside CBuild directory, check doxygen documentation on this website or you can directly check files inside CBuild git repository.


CBuild tips and tricks

Using external dependency in your buildscript

In CBuild::Registry namespace there is a function CBuild::Registry::GetRebuildTarget(). This function returns pointer to CBuild::Toolchain that is used to compile your buildscript. Also, don't forget that you need to explicitely specify -rpath for your custom libraries and for libraries, compiled by this project too.


Using multiple files in your buildscript

This is a pretty easy task. You simply need to add a function definition to a hpp file inside scripts directory, and then you need to create a cpp file that implements this function. As a header you can use user_init.hpp or any other header that you create. Also, you do not need to specify that you create new cpp file, all files inside scripts directory are threated as buildscript sources.


File proccessing

There are two main file proccessing types: preprocessing and postprocessing. File preprocessing for compilation is mostly a header proccessing, like runtime fill of variables. This can be done using functions from CBuild/files.hpp file. There is set_var() and replace() function. First replace all variable occurence and I found it not realy useful. Second simply replace token with some std::string, and it is realy useful to add some defines like version or some autogenerated help message etc. File postprocessing is mostly involved in some file copying or moving and this can be accomplished using CBuild::fs namespace's functions, std::filesystem namespace's functions or CBuild::system() function with call to cp, mv etc.


Use of custom arguments

Let's say we need to pass some argument to build() function of our toolchain. So, to speecify an argument you need to add this command line argument to CBuild executable call: -a <argument>. Then, in your build function you can use this syntax to retrieave all arguments in std::vector<std::string>* - this->args, to get access to function of this array - this->args->.... Then you can parse this strings for some argument in your code.


Cross-compilation toolchain

Let's say we want to target some different architecture/OS. So, let's say we have this compilers: gcc-x86_64 and gcc-arm64. One way to achieve cross compilation is an addition of second target, but this is a very dumb way, so - how to create proper toolchain to compile using different compilers. With this implementation you simply need to call set_target() function with value from Target enum.

Toolchain that use two compilers, implementation

/**
 * <doxygen file header and license header>
 */
// We use gcc toolchain as our base
#include "CBuild/build/gcc.hpp"
#ifndef __CROSS_GCC__
#define __CROSS_GCC__
typedef enum {
    X86_64,
    ARM64
} Target;
class cross_gcc : CBuild::gcc {
    protected:
        Target target;
    public:
        void build() override {
            if (this->target == Target::X86_64) {
                // Things specific to x86_64
            } else if (this->target == Target::ARM64) {
                // Things specific to arm
            }
            // Other part of build
        }
        // Other funcs that you need/want to implement
        void set_target(Target target) {
            this->target = target;
            if (this->target == Target::X86_64) {
                this->compiler = "g++-x86_64";
                this->linker = "g++-x86_64";
                this->packer = "ar-x86_64 cr";
            } else if (this->target = Target::ARM64) {
                this->compiler = "g++-arm64";
                this->linker = "g++-arm64";
                this->packer = "ar-arm64 cr";
            }
        }
};
#endif // __CROSS_GCC__

Here is a different approach. With this aproach you need to provide -a target:x86_64 or -a target:arm64 when building, and it defaults to a x86_64 target.

Toolchain that use two compilers and select one at runtime, implementation

/**
 * <doxygen file header and license header>
 */
// We use gcc toolchain as our base
#include "CBuild/build/gcc.hpp"
#ifndef __CROSS_GCC__
#define __CROSS_GCC__
typedef enum {
    X86_64,
    ARM64
} Target;
class cross_gcc : CBuild::gcc {
    protected:
        Target target;
    public:
        void pre_build() override {
            this->target = Target:X86_64;
            for (auto elem : (*(this->args))) {
                if(elem == std::string("target:x86_64")) {
                    this->target = Target::X86_64;
                    this->compiler = "g++-x86_64";
                    this->linker = "g++-x86_64";
                    this->packer = "ar-x86_64 cr"
                } else if (elem == std::string("target:arm64")) {
                    this->target = Target::ARM64;
                    this->compiler = "g++-arm64";
                    this->linker = "g++-arm64";
                    this->packer = "ar-arm64 cr";
                }
            }
        }
        void build() override {
            if (this->target == Target::X86_64) {
                // Things specific to x86_64
            } else if (this->target == Target::ARM64) {
                // Things specific to arm
            }
            // Other part of build
        }
        // Other funcs that you need/want to implement
};
#endif // __CROSS_GCC__

If your buildscript binary broken...

If your buildscript is broken you can always rebuild it using this command: CBuild_rebuild scripts/ "<your custom build args for buildscript>" "<your custom link args for buildscript>" <your buildscript name or CBuild.run>. But now this is very hard to brake buildscript except when you are liking some liraries and don't specify runtime libry paths.


BACK