It is sometimes required or preferred to use specific build tools. The reason may be for instance to enforce specific compiler features (e.g. full C++17 language support) or to crosscompile to a specific target platform (e.g. targeting a raspberry pi card). In PID customizing the build environment used at workspace level is made with environments. An environment is a deployment unit defining how to configure the build process of packages.

Generally we use environments to achieve two different tasks:

  • managing a toolchain. For instance gcc_toolchain environment defines how to configure the build process when using the GNU C/C++ compiler ; nvcc_toolchain environment defines how to configure the build process when using the NVIDIA CUDA compiler, etc.

  • describing a specific platform instance, this is the description of the complete build environment for a given target platform. They are used to agregate a set of toolchains with specific constraints (for instance minimum version of toolchains required).

Following sections explain how to define new environments.

Warning

Writing environment may be a complex task, and is very hard to generalize as it is completey bound to the specificities of the host, of the compiler in use, of the target platform, etc.

That is why writing new environments should be reserved to advanced users.

Step 1: create the environment repository and project

First of all we need to create the project for the environment and its online repository.

1.1: create online repository

Go into your git hosting server (gitlab for LIRMM) and create a new repository with same name as environment projects.

In this example we create a repository named gcc_toolset, that will finally be more or less a copy of gcc_toolchain. We name it this way to avoid conflicts with the already existing gcc_toolchain environment.

Then copy the SSH address of this git repository.

1.2: create the environment project

cd <pid-workspace>/pid
make create environment=gcc_toolset url=<url previously copied>
cd <pid-workspace>/environments/gcc_toolset

The environment project should have been created and put into folder <pid-workspace>/environments. In the following sections we use <gcc_toolset> as a short name for the path to <pid-workspace>/environments/gcc_toolset.

Step 2: describe the content of the environment

Now first thing to do is to define adequate meta information of the environment. Edit the CMakeListst.txt file in <gcc_toolset> and paste the code:

CMAKE_MINIMUM_REQUIRED(VERSION 3.0.2)
set(WORKSPACE_DIR ${CMAKE_SOURCE_DIR}/../.. CACHE PATH "root of the PID workspace directory")
list(APPEND CMAKE_MODULE_PATH ${WORKSPACE_DIR}/share/cmake/system) # using generic scripts/modules of the workspace
include(Environment_Definition NO_POLICY_SCOPE)

project(gcc_toolset)

PID_Environment(
      AUTHOR            Your Name
      MAIL              your.mail@company.something
      YEAR              2019
      LICENSE           CeCILL-C
      ADDRESS           <SSH address to gcc_toolset.git>
      PUBLIC_ADDRESS    <HTTP address to gcc_toolset.git>
      DESCRIPTION       "using GNU gcc toolchain to build C/C++ code of projects"
)

PID_Environment_Platform(CHECK check_gcc.cmake)

PID_Environment_Constraints(OPTIONAL version exact) # give the version of the desired gcc toolset
#for now only define a solution for ubuntu distributions
PID_Environment_Solution(OS linux DISTRIBUTION ubuntu CONFIGURE ubuntu/configure_gcc.cmake)

build_PID_Environment()

Explanations:

  • include(Environment_Definition NO_POLICY_SCOPE) is used to import the API for writing environments.
  • PID_Environment command (equivalent to declare_PID_Environment) transforms the current CMake project into a PID environment. Arguments to provide are more or less the same as for native packages:

    • the LICENSE argument must be provided.
    • AUTHOR, INSTITUTION, MAIL refer to the contact author of the environment project.
    • ADDRESS and PUBLIC_ADDRESS have the same meaning than for native packages.
  • PID_Environment_Platform (equivalent to define_PID_Environment_Platform) defines constraints on target platform. In this example there is no specific restriction on target platform:

    • CHECK argument defines the script file used to check if host system matches target platform regarding toolchain in use. We define the script check_gcc.cmake that lies in project src folder.
  • PID_Environment_Constraints defines environment specific constraints that can be applied to current environment. In this example we define version and exact constraints as usual for toolchain definition environments. Both are optional so they are defined using the OPTIONAL keyword.

  • PID_Environment_Solution defines a solution used to deploy the toolchain when host that does not macth target requirements. In this example we have only one solution defined that may be used anytime host platform is a linux with an ubuntu distribution.

    • CONFIGURE argument defines the script file used to generate the adequate configuration. It is supposed to be placed in src/ubuntu folder.
  • build_PID_Environment configures the environment and create its build commands.

Step 3: Writing the check script

The first thing to do is to edit the script src/check_gcc.cmake. This script simply checks if host is already configured with the toolchain defined by gcc_toolset, eventually taking into account constraints defined in root CMakeLists.txt. Edit this file and paste the code:

# check if host already matches the constraints
#host must have a GNU compiler !!
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  return_Environment_Check(FALSE)
endif()

if(gcc_toolchain_version)#a version constraint has been specified
  if(gcc_toolchain_exact)
    if(NOT CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL gcc_toolchain_version)
      return_Environment_Check(FALSE)
    endif()
  else()
    if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS gcc_toolchain_version)
      return_Environment_Check(FALSE)
    endif()
  endif()
endif()

# thats it for checks => host matches all requirements of this solution
return_Environment_Check(TRUE)

The check script basically tests if the default host c++ compiler is a GNU compiler. If not then return_Environment_Check(FALSE) exits this script on error.

The script should also takes into account used defined constraints, in this example the version related constraints. The variable gcc_toolchain_version is correctly valued according to value given to the version constraint or is not defined if no version constraint has been specified. So it is simple to handle constraints in CMake script by simly checking their value.

Finally if all tests succeeded the script should exit with return_Environment_Check(TRUE).

Step 4: Writing the configure script

Last part consists in writing configuration scripts for each solution. There is only one solution for now, usable if host is an ubuntu platform. Edit the src/ubuntu/configure_gcc.cmake file and paste the code:

function(check_gcc_version_ok RESULT)
  set(${RESULT} FALSE PARENT_SCOPE)
  if(NOT ${PROJECT_NAME}_C_COMPILER)
    return()
  endif()
  #get the information about compiler
  execute_process(COMMAND ${${PROJECT_NAME}_C_COMPILER} -v WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  OUTPUT_VARIABLE compiler_specs ERROR_VARIABLE compiler_errors)
  if(NOT compiler_specs AND compiler_errors)
    set(compiler_specs ${compiler_errors})#using error output is sometimes necessary as it may contain the ouput of the command
  endif()
  set(version)
  string(REPLACE "\n" ";" LIST_OF_LINES "${compiler_specs}")
  foreach (line IN LISTS LIST_OF_LINES)
    if(line MATCHES "^gcc[ \t]+version[ \t]+([^ \t]+)[ \t]+.*$")
      set(version ${CMAKE_MATCH_1})
      break()
    endif()
  endforeach()
  if(NOT version)
    return()
  endif()
  #we know the version of gcc in use => check again version constraint, is any
  if(gcc_toolchain_version)#a version constraint has been specified
    if(gcc_toolchain_exact)
      if(NOT version VERSION_EQUAL gcc_toolchain_version)
        return()
      endif()
    else()
      if(version VERSION_LESS gcc_toolchain_version)
        return()
      endif()
    endif()
    #if no version required then simply do not check anything
  endif()
  set(${RESULT} TRUE PARENT_SCOPE)
endfunction(check_gcc_version_ok)

# if this script executes code is built on a ubuntu system
# build the pool of available versions depending on the target specification
get_Environment_Target_Platform(DISTRIBUTION target_distrib DISTRIB_VERSION target_distrib_version
TYPE target_proc ARCH target_bits OS target_os ABI target_abi)
get_Environment_Host_Platform(DISTRIB_VERSION host_distrib_version
TYPE proc_host ARCH bits_host ABI host_abi)

if(target_os STREQUAL linux)
  if(target_distrib STREQUAL ubuntu)#target is ubuntu and host is ubuntu as well (by definition if this script is called)
    if(host_distrib_version STREQUAL target_distrib_version)
      if(proc_host STREQUAL target_proc AND bits_host EQUAL target_bits)
        #I know the procedure to install gcc whatever the processor specification are (and I know binaries will be compatibles)
        execute_OS_Command(apt-get install -y gcc g++)#getting last version of gcc/g++
        evaluate_Host_Platform(EVAL_RESULT)#evaluate again the host (only check that version constraint is satisfied)
        if(EVAL_RESULT)
          #only ABI may be not compliant
          get_Environment_Target_ABI_Flags(ABI_FLAGS ${target_abi})
          if(ABI_FLAGS)#an ABI constraint is explicilty specified => need to force it!
            configure_Environment_Tool(LANGUAGE CXX FLAGS ${ABI_FLAGS})
          endif()
          return_Environment_Configured(TRUE)#that is OK gcc and g++ have just been updated and are now OK
        endif()#no solution found with OS installers -> using alternative
      endif()
    endif()
  elseif(target_distrib STREQUAL raspbian
        AND target_proc STREQUAL arm
        AND target_bits EQUAL 32)#targetting a raspbian system

    if( host_distrib_version VERSION_EQUAL 16.04
        AND proc_host STREQUAL x86
        AND bits_host EQUAL 64)#we have built a cross compiler for this situation !!

        if(gcc_toolset_version) #there is a constraint on version
          if(NOT gcc_toolset_version VERSION_EQUAL 5.4.0
          OR NOT (gcc_toolset_version VERSION_LESS 5.4.0 AND NOT gcc_toolset_exact))
            return_Environment_Configured(FALSE)#only gcc version 5.4.0 is provided
          endif()
        endif()
        set(PATH_TO_ROOT ${CMAKE_SOURCE_DIR}/src/ubuntu/raspi-x86_64/armv8-rpi3-linux-gnueabihf)
        #assembler
        set(PATH_TO_AS ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-as)
        configure_Environment_Tool(LANGUAGE ASM COMPILER ${PATH_TO_AS})
        #C compiler
        set(PATH_TO_GCC ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-gcc)
        set(PATH_TO_GCC_AR ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-ar)
        set(PATH_TO_GCC_RANLIB ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-ranlib)
        set(PATH_TO_GPP ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-gcc-5.4.0)
        configure_Environment_Tool(LANGUAGE C COMPILER ${PATH_TO_GCC} AR ${PATH_TO_GCC_AR} RANLIB ${PATH_TO_GCC_RANLIB})
        #c++ compiler (always explicitly setting the C++ ABI)
        get_Environment_Target_ABI_Flags(ABI_FLAGS ${target_abi})
        configure_Environment_Tool(LANGUAGE CXX COMPILER ${PATH_TO_GPP} FLAGS ${ABI_FLAGS} AR ${PATH_TO_GCC_AR} RANLIB ${PATH_TO_GCC_RANLIB})

        # system tools
        set(PATH_TO_LINKER ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-ld)
        set(PATH_TO_NM ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-nm)
        set(PATH_TO_OBJCOPY ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-objcopy)
        set(PATH_TO_OBJDUMP ${PATH_TO_ROOT}/bin/armv8-rpi3-linux-gnueabihf-objdump)
        configure_Environment_Tool(SYSTEM LINKER ${PATH_TO_LINKER} NM ${PATH_TO_NM} OBJCOPY ${PATH_TO_OBJCOPY} OBJDUMP ${PATH_TO_OBJDUMP})

        #sysroot !!
        set(PATH_TO_SYSROOT ${PATH_TO_ROOT}/armv8-rpi3-linux-gnueabihf/sysroot)
        configure_Environment_Tool(SYSTEM SYSROOT ${PATH_TO_SYSROOT})

        return_Environment_Configured(TRUE)
    endif()
    #TODO directly put the compiler for proc x86 64 bits
  #else add other OS/distrib when supported
  endif()
elseif(target_os STREQUAL Generic)#no target OS => we compile for bare metal system
  if(target_proc STREQUAL "arm") # compile for ARM microcontrollers
    if(target_bits EQUAL 32)#armv7 for pic microcontrollers
      ... # installing a gcc compiler for bare metal compilation
    endif()
  endif()
endif()

return_Environment_Configured(FALSE)

As you may see the script is a bit complex (and has even been truncated from its original version for sake of readbility). The goal of this script is to configure the build environment to use a gcc toolchain when the host is an ubuntu distribution.

The function check_gcc_version_ok has been defined to automate the verification of the compiler version constraint once a compiler has been defined. It first detects the version of the GNU compiler then check constraints using dedicated variables, like in check script.

Then from a functional point of view writing the configuration script consists in comparing host and target platforms and, depending on their differences, apply adequate configuration actions. get_Environment_Target_Platform and get_Environment_Host_Platform are used to get informations about those platforms that can then be compared so the whole script file should be organized around a hierarchy of if/elseif tests that describe all cases that are managed.

For instance if both target and host platforms have same OS, distribution and processor related properties then it means that host either has a differen abi OR a different version. So a solution may be to simply update the compiler OS packages using execute_OS_Command(apt-get install -y gcc g++). Then evaluate_Host_Platform redo an evaluation of the current host default environment and call again the check file to see if after update the host is now compliant with target. Finally the get_Environment_Target_ABI_Flags is called if the check is successful to know what flags must be passed to the C++ compiler in order to use OLD or NEW ABI. Those flags will be set as default in workspace configuration when the configure_Environment_Tool(LANGUAGE CXX FLAGS ${ABI_FLAGS}) is used. the call to return_Environment_Configured(TRUE) simply tells that the configuration process is finished AND successful.

The following part of the script defines how to configure gcc to target a raspberry Pi card. In this situation only version 5.4 of gcc/g++ is available AND only for an ubuntu 16.04 OS because we built only this version on this platform. We suppose that the complete toolchain version lies in the folder src/ubuntu/raspi-x86_64. The configuration here simply consists in targetting adequate tools using the configure_Environment_Tool function. A sysroot being provided by this gcc toolchain we also set the sysroot with configure_Environment_Tool(SYSTEM SYSROOT ${PATH_TO_SYSROOT}). So instead of installing bianries using OS package you can also directly put toolchain versions in the source tree and use it to configure the default environment.

Step 4: Configuring the environment project

Once a description has been defined you can test you environment by doing:

cd <gcc_toolset>/build
cmake ..
make build

On an ubuntu workstation, you should see more or less the same info as for workspace, something like:

[PID] INFO : Target platform in use is x86_64_linux_abi11:
 + processor family= x86
 + binary architecture= 64
 + operating system=linux (ubuntu 16.04)
 + compiler ABI= CXX11

The build command of environment has no side effect outside of the environment itself. It is used to test environment, eventually by passing same arguments as for the configure command. If you want to use the environment you must call the configure command of the workspace:

cd <pid-workspace>/pid
make configure environment=gcc_toolset

Since there is not target platform constraint specified the environment will simply check if gcc is the default compiler on host.

To test if environment constraints are working as expected you may do:

version=7.0 make configure environment=gcc_toolset

With default settings on ubuntu 16.04 this configuration should fail with message:

[PID] CRITICAL ERROR: cannot configure host with environment gcc_toolset.
 No valid solution found.

While if we do:

version=5.4 make configure environment=gcc_toolset

Everything should work as expected because default compiler on ubuntu 16.04 is GNU 5.4.x.

Step 5: Referencing the environment

Once your environment is ready to be used you should reference it in the workspace:

cd <gcc_toolset>/build
make referencing

Then as for native packages commit the reference file that has just been generated and propose a merge request to the official pid workspace. This way other people can use the environment you defined.

Step 6: Using environments to describe a platform instance

Now we learned how to define new environment in order to manage new toolchains like gcc, clang or nvcc we have to learn how to combine to create more “complete” description of the host and target platforms, what we call platform instance.

A platform instance is a more or less complete description of an host system configured to build for a specific target system. In the following sections we decide to create an environment named test_instance that will completely specify a platform instance in order to reflect the exact configuration of a given host system.

6.1: create online repository and environment project

Create a new repository with name test_instance into your git hosting server (gitlab for LIRMM), copy its address then:

cd <pid-workspace>/pid
make create environment=test_instance url=<url previously copied>
cd <pid-workspace>/environments/test_instance

The environment project should have been created and put into folder <pid-workspace>/environments. In the following sections we use <test_instance> as a short name for the path to <pid-workspace>/environments/test_instance.

6.2: describing environment

Edit the root CMakeLists.txt file of test_instance and paste the code

CMAKE_MINIMUM_REQUIRED(VERSION 3.0.2)
set(WORKSPACE_DIR ${CMAKE_SOURCE_DIR}/../.. CACHE PATH "root of the PID workspace directory")
list(APPEND CMAKE_MODULE_PATH ${WORKSPACE_DIR}/share/cmake/system) # using generic scripts/modules of the workspace
include(Environment_Definition NO_POLICY_SCOPE)

project(test_instance C CXX ASM)

PID_Environment(
    AUTHOR      Your Name
    MAIL        your.mail@company.something
    YEAR        2019
    LICENSE     CeCILL-C
    ADDRESS     <SSH url>
    DESCRIPTION "used environment for the test_instance host"
)

#give constraints about target platform in use
PID_Environment_Platform(
  PLATFORM x86_64_linux_abi11
  DISTRIBUTION ubuntu
  DISTRIB_VERSION 16.04
  CONFIGURATION posix cuda
)

# here no need to check host specific capabilities (no check file), in the end the check will be performed by gcc_toolchain environment
PID_Environment_Solution(DEPENDENCIES gcc_toolchain[version=5.4,exact=true] gfortran_toolchain[version=5.4,exact=true] nvcc_toolchain[version=7.5,exact=true])

build_PID_Environment()

Description is made the usual way with same commands as for first example, but arguments changed .

  • PID_Environment_Platform is now used to define a complete platform instance spécification by using together PLATFORM, DISTRIBUTION and DISTRIB_VERSION specification. Furthermore we use the CONFIGURATION keyword to specify configurations that must exist on host platform to be considered as target platform, which is particularly useful to check presence of OS configuration that cannot be installed. In the example host platform must have posix and cuda configurations available on OS. Notice that we do not need check scripts here

  • PID_Environment_Solution defines a unique solution whatever the host is. This solution itself depends on a set of toolchain definition environments (using DEPENDENCIES keyword) : gcc_toolchain, gfortran_toolchain and nvcc_toolchain with specific constraints. Notice that there is no configuration script because configurations of build process by test_instance will be finally completely performed by the different dependencies it uses. Adding a configure script is feasible and woud have been useful for instance to configure other build tools than those managed by dependencies (dependencies are always checked first).

The evaluation of the dependencies will be performed with target platform specified in PID_Environment_Platform and additional constraints, same way as if they had been passed as arguments by user via the configure command.

So for instance gcc_toolchain will (try to) configure the workspace with version 5.4 of gcc usable on a host with an ubuntu 16.04 for x86_64_linux_abi11 platform. Same logic applies for all dependencies.

So finally environments can also be used to define a complete target platform configuration, allowing to share and reuse sets of build toolchains.

6.3: use the environment

To test it simply do :

make configure environment=test_instance

If your host system matches the target platform the configuration will be successful. But if your host platform is different then the configuration process may lead either :

  • to an error, if no solution exists to configure your host for generating code for the target platform.
  • to the use of specific toolchains versions if all dependencies have resolved target platform constraints.