Environments can be used to customize the host system configuration in order to be capable of using extra tools. This is what we call the plugins mechanism of PID, because it allows to add new automatic behaviors into native packages in such a way that they can use those developper tools.

Step 1: a first plugin

Structure of a plugin

A plugin is defined using an environment, so their creation follow same rules and principles. So let’s suppose we have created an environment called pkg-config that provides a plugin fo the well known pkg-config tool. We simply want to generate pkg-config configuration files for the packages generated into the workspace and check that the tool itself is installed in host platform.

Here is the general project description:

...
project(pkg-config)

PID_Environment(
      AUTHOR 		        Robin Passama
			INSTITUTION				CNRS/LIRMM
			MAIL							robin.passama@lirmm.fr
			YEAR 							2020
			LICENSE 					CeCILL-C
			ADDRESS						git@gite.lirmm.fr:pid/environments/pkg-config.git
			PUBLIC_ADDRESS		https://gite.lirmm.fr/pid/environments/pkg-config.git
			DESCRIPTION 			"using pkg-config tool to generate pc file from PID packages description"
      CONTRIBUTION_SPACE pid
      INFO              "automatically generating pkg-config modules from packages description."
      "To use pkg-config for retrieving generated libraries please set your environment variable PKG_CONFIG_PATH"
      "to @WORKSPACE_DIR@/install/@CURRENT_PLATFORM@/__pkgconfig__"
      "(e.g. export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:@WORKSPACE_DIR@/install/@CURRENT_PLATFORM@/__pkgconfig__)."
      "Typical usage: for building an executable use `pkg-config --static --cflags <name of the library>`,"
      "for linking use `pkg-config --static --libs <name of the library>`"
		)


PID_Environment_Constraints(OPTIONAL version exact CHECK check_pkg-config.cmake)
PID_Environment_Solution(HOST CONFIGURE configure_pkg-config.cmake)

build_PID_Environment()

Nothing really special in this description except the INFO argument of PID_Environment. This is useful to give a description of usage for the plugin. The text will be interpreted by a CMake generator so you can use surrounding @ tags to specify cmake variables that will finally be replaced by their values when text is printed. Is is printed anytime the workspace will be configured to use the plugin.

Writing the check script

The check script src/check_pkg-config.cmake is quite simple, it simply tests that the pkg-config tool is installed in host and, like for the gcc_toolchain environment it is used to check version constraint.

find_package(PkgConfig)
if(NOT PKG_CONFIG_FOUND)
  return_Environment_Check(FALSE)
endif()

check_Environment_Version(RESULT pkg-config_version "${pkg-config_exact}" "${PKG_CONFIG_VERSION_STRING}")
if(RESULT)
  return_Environment_Check(TRUE)
else()
  return_Environment_Check(FALSE)
endif()

The main different with the previous tutorial is that we do not use predefined CMake variables since pkg-config is not a default tool in CMake (simply because it is not related to a programming language used in a build process).

Writing the configuration script

The configuration script src/configure_pkg-config.cmake looks like:

# check if host matches the target platform
host_Match_Target_Platform(MATCHING)
if(NOT MATCHING)
  return_Environment_Configured(FALSE)
endif()

evaluate_Host_Platform(EVAL_RESULT)
if(NOT EVAL_RESULT)
  install_System_Packages(INSTALL_RESULT
                          APT     pkg-config
                          PACMAN  pkgconf)
  if(NOT INSTALL_RESULT)
    return_Environment_Configured(FALSE)
  endif()
  evaluate_Host_Platform(EVAL_RESULT)
endif()

if(EVAL_RESULT)
  configure_Environment_Tool(EXTRA pkg-config
                             PROGRAM ${PKG_CONFIG_EXECUTABLE}
                             PLUGIN AFTER_COMPS use_pkg-config.cmake)
  return_Environment_Configured(TRUE)
endif()

return_Environment_Configured(FALSE)

Again we can see than we use same calls than for an environment used to define new toolchains:

  • host_Match_Target_Platform verify that host and targets are compatible.
  • evaluate_Host_Platform calls the check script, and so is able to detect pkg-config tool in host platform.
  • install_System_Packages is used to install the tool if not found. Notice that here we provide an install procedure for host systems providing apt (debian like) or pacman (archlinux derivatives) packagers. More can be later defined for other package manager in order to adapt to various operating systems.

The main difference when writing a plugin is in the call to configure_Environment_Tool:

  • the EXTRA argument is used to tell that the environment generates an host configuration for an extra tool (not natively supported by PID).
  • the PROGRAM argument is used to specify the path to this extra tool. In this example the PKG_CONFIG_EXECUTABLE variable has been generated by the call to find_package in the check script.
  • the PLUGIN argument is used to specify script(s) that define custom behaviors to be added to default package behavior in order to make them capable of using the extra tool. Up to 4 scripts can be defined using the following arguments:

    • BEFORE_DEPS: the script that executes before any dependency is resolved into the package. For instance usefull if your plugin need to add some extra dependencies.
    • BEFORE_COMPS: the script that executes before any component is defined. For instance usefull if your plugin need to create components.
    • DURING_COMPS: the script that executes during each component definition. For instance usefull if your plugin need to set properties to components.
    • AFTER_COMPS: the script that executes after all component have been defined. For instance usefull if your plugin need to have a full description of the package to execute.

Since pkg-config tool is used to manage libraries at compile and link time, and since this plugin is used to generate the configuration files it uses for each library in a package, we need to define a AFTER_COMPS script to be sure that we get a complete information on libraries generated by the package.

Writing plugin scripts

So now let’s have a look at the difficult part: writing plugin scripts. Indeed these scripts implement the new behavior we want to add to PID and so it requires a deeper level of understanding on how PID works. That is why such kind of environment should be reserved to experienced users.

Here is the content of src/use_pkg-config.cmake script:

if(WIN32)
  set(LIBRARY_KEYWORD "")
elseif(UNIX)
  # Using -l:/some/absolute/path.so was an "undocumented ld feature, in
  # actual fact a ld bug, that has since been fixed".
  # This was apparently used (e.g. in ROS) because of pkg-config problems that
  # have since been fixed.
  # See: https://github.com/ros/catkin/issues/694#issuecomment-88323282
  # Note: ld version on Linux can be 2.25.1 or 2.24
  if (NOT CMAKE_LINKER)
    include(CMakeFindBinUtils)
  endif()

	get_filename_component(LINKER_NAME ${CMAKE_LINKER} NAME)
	if(LINKER_NAME STREQUAL "ld")
	  execute_process(COMMAND ${CMAKE_LINKER} -v OUTPUT_VARIABLE LD_VERSION_STR ERROR_VARIABLE LD_VERSION_STR)
	  string(REGEX MATCH "([0-9]+\\.[0-9]+(\\.[0-9]+)?)" LD_VERSION ${LD_VERSION_STR})
	  if(LD_VERSION VERSION_LESS "2.24.90")#below this version pkg-config does not handle properly absolute path
	    set(LIBRARY_KEYWORD "-l:")
	  else()#otherwise we can use full path
	    set(LIBRARY_KEYWORD "")
  	endif()
	else()
		set(LIBRARY_KEYWORD "")
	endif()
endif()


# clean_Pkg_Config_Files
# ----------------------
#
# clean the .pc files corresponding to the library defined in the current project
#
function(clean_Pkg_Config_Files path_to_build_folder library_name)
  get_Component_Target(RES_TARGET ${library_name})
  clean_Pkg_Config_Target(${path_to_build_folder} ${RES_TARGET})
  clean_Pkg_Config_Files_For_Dependencies(${path_to_build_folder} ${library_name})
endfunction(clean_Pkg_Config_Files)

function(clean_Pkg_Config_Target path_to_build_folder target)
  if(EXISTS ${path_to_build_folder}/${target}.pc)#remove file if existing
    file(REMOVE ${path_to_build_folder}/${target}.pc)
  endif()
  if(EXISTS ${path_to_build_folder}/${target}.pre.pc)#remove intermediary files
    file(REMOVE ${path_to_build_folder}/${target}.pre.pc)
  endif()
endfunction(clean_Pkg_Config_Target)

###
function(clean_Pkg_Config_Files_For_Dependencies path_to_build_folder library_name)
  get_Component_Dependencies_Targets(RES_DEPS ${library_name})
  foreach(dep IN LISTS RES_DEPS)
    clean_Pkg_Config_Target(${path_to_build_folder} ${dep})
  endforeach()
endfunction(clean_Pkg_Config_Files_For_Dependencies)


# generate_Pkg_Config_Files
# ----------------------------------
#
# generate the .pc file corresponding to the library defined in the current project
#
function(generate_Pkg_Config_Files path_to_build_folder package platform library_name)
  get_Package_Component_Target(RES_TARGET ${package} ${library_name})
  add_Managed_Target("${RES_TARGET}")
	#generate and install .pc files for the library
  setup_And_Install_Library_Pkg_Config_File(${path_to_build_folder} ${package} ${platform} ${library_name})
  # manage recursion with dependent packages (directly using their use file) to ensure generation of the library dependencies
  generate_Pkg_Config_Files_For_Dependencies(${path_to_build_folder} ${package} ${platform} ${library_name})
endfunction(generate_Pkg_Config_Files)

### auxiliary function to manage optimization of generation process
macro(add_Managed_Target target_name)
  if(MANAGED_PKG_CONFIG_${target_name})
    return()
  endif()
  set(MANAGED_PKG_CONFIG_${target_name} TRUE CACHE INTERNAL "")
  append_Unique_In_Cache(MANAGED_PC_FILES "${target_name}")
endmacro(add_Managed_Target)

macro(reset_Managed_PC_Files_Variables)
  foreach(target IN LISTS MANAGED_PC_FILES)
    set(MANAGED_PKG_CONFIG_${target} CACHE INTERNAL "")
  endforeach()
  set(MANAGED_PC_FILES CACHE INTERNAL "")
endmacro(reset_Managed_PC_Files_Variables)


###
function(generate_Pkg_Config_Files_For_Dependencies path_to_build_folder package platform library_name)
  list_Component_Direct_External_Package_Dependencies(DIRECT_EXT_PACK ${package} ${library_name})
  foreach(dep_package IN LISTS DIRECT_EXT_PACK)
    list_Component_Direct_External_Component_Dependencies(DIRECT_EXT_COMPS ${package} ${library_name} ${dep_package})
    foreach(dep_component IN LISTS DIRECT_EXT_COMPS)
      #generate the pkg-config file to be sure the adequate version used locally is existing
      generate_Pkg_Config_Files(${path_to_build_folder} ${dep_package} ${platform} ${dep_component})
    endforeach()
  endforeach()

  list_Component_Direct_Internal_Dependencies(DIRECT_INT_DEPS ${package} ${library_name})
  foreach(dep_component IN LISTS DIRECT_INT_DEPS)
    generate_Pkg_Config_Files(${path_to_build_folder} ${package} ${platform} ${dep_component})
  endforeach()

  list_Component_Direct_Native_Package_Dependencies(DIRECT_NAT_PACK ${package} ${library_name})
  foreach(dep_package IN LISTS DIRECT_NAT_PACK)
    list_Component_Direct_Native_Component_Dependencies(DIRECT_NAT_COMPS ${package} ${library_name} ${dep_package})
    foreach(dep_component IN LISTS DIRECT_NAT_COMPS)
      #generate the pkg-config file to be sure the adequate version used locally is existing
      generate_Pkg_Config_Files(${path_to_build_folder} ${dep_package} ${platform} ${dep_component})
    endforeach()
  endforeach()
endfunction(generate_Pkg_Config_Files_For_Dependencies)


###
#using a function (instead of a macro) ensures that local variable defined within macros will no be exported outside the context of the function
function(setup_And_Install_Library_Pkg_Config_File path_to_build_folder package platform library_name)
  #set the local variable of the project
  setup_Pkg_Config_Variables(${package} ${platform} ${library_name})
  # write and install .pc files of the project from these variables
  install_Pkg_Config_File(${path_to_build_folder} ${package} ${platform} ${library_name})
endfunction(setup_And_Install_Library_Pkg_Config_File)

### generate and install pkg-config .pc files
macro(install_Pkg_Config_File path_to_build_folder package platform library_name)
  get_Package_Component_Target(RES_TARGET ${package} ${library_name})
  if(NOT EXISTS ${path_to_build_folder}/${RES_TARGET}.pre.pc)#if intermediary file do not exist it means that that generate/install rule has not been defined yet
    #generate a temporary file with the adequate pkg-config format but whose content is not already generated from cmake
    get_Path_To_Environment(RES_PATH pkg-config)
    configure_file("${RES_PATH}/pkg_config.pc.pre.in" "${path_to_build_folder}/${RES_TARGET}.pre.pc" @ONLY)
    #the final generation is performed after evaluation of generator expression (this is the case for currently build package only, not for dependencies for which expression have already been resolved)
    file(GENERATE OUTPUT ${path_to_build_folder}/${RES_TARGET}.pc
                  INPUT ${path_to_build_folder}/${RES_TARGET}.pre.pc)
  	#finally create the install target for the .pc file corresponding to the library
  	set(PATH_TO_INSTALL_FOLDER ${WORKSPACE_DIR}/install/${platform}/__pkgconfig__) #put everything in a global folder so that adding this folder to PKG_CONFIG_PATH will be a quite easy task
    install(
  		FILES ${path_to_build_folder}/${RES_TARGET}.pc
  		DESTINATION ${PATH_TO_INSTALL_FOLDER} #put generated .pc files into a unique folder in install tree of the package
  		PERMISSIONS OWNER_READ GROUP_READ WORLD_READ OWNER_WRITE)
  endif()
endmacro(install_Pkg_Config_File)

# setup_Pkg_Config_Variables
# ----------------------------------
#
# set the variables that will be usefull for .pc file generation
#
macro(setup_Pkg_Config_Variables package platform library_name)
	get_Mode_Variables(TARGET_SUFFIX VAR_SUFFIX ${CMAKE_BUILD_TYPE}) #getting mode info that will be used for generating adequate names

  is_Native_Package(PACK_NATIVE ${package})
  get_Package_Version(RES_VERSION ${package})
  get_Component_Target(RES_TARGET ${library_name})

  set(_PKG_CONFIG_WORKSPACE_GLOBAL_PATH_ ${WORKSPACE_DIR})

  #1. Set general meta-information about the library
	#set the prefix
	set(_PKG_CONFIG_PACKAGE_PREFIX_ install/${platform}/${package}/${RES_VERSION})

  set(package_install_dir ${_PKG_CONFIG_WORKSPACE_GLOBAL_PATH_}/${_PKG_CONFIG_PACKAGE_PREFIX_})
	#set the version
	set(_PKG_CONFIG_PACKAGE_VERSION_ ${RES_VERSION})

	#set the URL
  get_Package_Project_Page(URL ${package})
	set(_PKG_CONFIG_PACKAGE_URL_ ${URL})

	#set the name (just something that is human readable so keep only the name of the library)
	set(_PKG_CONFIG_COMPONENT_NAME_ "${RES_TARGET}")
  set(_PKG_CONFIG_COMPONENT_DESCRIPTION_ "library ${library_name} from package ${package} (in ${CMAKE_BUILD_TYPE} mode)")
	#set the description
  get_Description(DESCR ${package} ${library_name})
  set(_PKG_CONFIG_COMPONENT_DESCRIPTION_ "${_PKG_CONFIG_COMPONENT_DESCRIPTION_}: ${DESCR}")

	#2. Set build information about the library
	#2.a management of cflags
	#add the include folder to cflags

  set(include_list)
  get_Package_Component_Includes(PACKAGE_INCLUDE_FOLDER_IN_INSTALL INCLUDE_DIRS_ABS INCLUDE_DIRS_REL ${package} ${library_name})
  foreach(inc IN LISTS INCLUDE_DIRS_REL)
    if(inc)# element maybe empty (relative to include folder itself)!
      list(APPEND include_list "-I\${includedir}/${inc}")
    else()
      list(APPEND include_list "-I\${includedir}")
    endif()
  endforeach()
  foreach(inc IN LISTS INCLUDE_DIRS_ABS)
    if(link MATCHES "^${_PKG_CONFIG_WORKSPACE_GLOBAL_PATH_}/(.+)$")#a workspace relative path
      list(APPEND include_list "-I\${global}/${CMAKE_MATCH_1}")
    else()#a system wide path
      list(APPEND include_list "-I${inc}")
    endif()
  endforeach()
  #add to cflags the specific includes used for that library
  set(_PKG_CONFIG_COMPONENT_CFLAGS_ "")
  if(include_list)
    list(REMOVE_DUPLICATES include_list)
    foreach(inc IN LISTS include_list)
      set(_PKG_CONFIG_COMPONENT_CFLAGS_ "${_PKG_CONFIG_COMPONENT_CFLAGS_} ${inc}")
    endforeach()
  endif()
	#add to cflags the definitions used for that library
  get_Package_Component_Compilation_Info(RES_DEFS RES_OPTS ${package} ${library_name})
	foreach(def IN LISTS RES_DEFS)
		set(_PKG_CONFIG_COMPONENT_CFLAGS_ "${_PKG_CONFIG_COMPONENT_CFLAGS_} -D${def}")
	endforeach()
	#add to cflags the specific options used for that library
	foreach(opt IN LISTS RES_OPTS)
		set(_PKG_CONFIG_COMPONENT_CFLAGS_ "${_PKG_CONFIG_COMPONENT_CFLAGS_} -${opt}")
	endforeach()
  #add also the corresponding C and C++ standards in use
  get_Component_Language_Standard(MANAGED_AS_STANDARD RES_STD_C RES_STD_CXX RES_C_OPT RES_CXX_OPT ${package} ${library_name})
  if(RES_STD_C)
    set(_PKG_CONFIG_COMPONENT_CFLAGS_ "${_PKG_CONFIG_COMPONENT_CFLAGS_} ${RES_C_OPT}")
  endif()
  set(_PKG_CONFIG_COMPONENT_CFLAGS_ "${_PKG_CONFIG_COMPONENT_CFLAGS_} ${RES_CXX_OPT}")

  #2.b management of libraries
  get_Package_Component_Links(PACKAGE_LIB_FOLDER REL_LINKS PUB_LINKS PRIV_LINKS ${package} ${library_name})
  foreach(link IN LISTS REL_LINKS)
    set(_PKG_CONFIG_COMPONENT_LIBS_ "${LIBRARY_KEYWORD}\${libdir}/${link}")
  endforeach()
  foreach(link IN LISTS PUB_LINKS)
    if(link MATCHES "^${_PKG_CONFIG_WORKSPACE_GLOBAL_PATH_}/(.+)$")
      set(_PKG_CONFIG_COMPONENT_LIBS_ "${_PKG_CONFIG_COMPONENT_LIBS_} ${LIBRARY_KEYWORD}\${global}/${CMAKE_MATCH_1}")
    else()
      set(_PKG_CONFIG_COMPONENT_LIBS_ "${_PKG_CONFIG_COMPONENT_LIBS_} ${link}")
    endif()
  endforeach()
  foreach(link IN LISTS PRIV_LINKS)
    if(link MATCHES "^${_PKG_CONFIG_WORKSPACE_GLOBAL_PATH_}/(.+)$")
      set(_PKG_CONFIG_COMPONENT_LIBS_PRIVATE_ "${_PKG_CONFIG_COMPONENT_LIBS_PRIVATE_} ${LIBRARY_KEYWORD}\${global}/${CMAKE_MATCH_1}")
    else()
      set(_PKG_CONFIG_COMPONENT_LIBS_PRIVATE_ "${_PKG_CONFIG_COMPONENT_LIBS_PRIVATE_} ${link}")
    endif()
  endforeach()

	#3 management of dependent packages and libraries
  set(_PKG_CONFIG_COMPONENT_REQUIRES_)
  set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_)

  #3.a manage dependencies to external packages
  # dependencies are either generated when source package is built
  # or if not, just after the call to setup_Pkg_Config_Variables
  list_Component_Direct_External_Package_Dependencies(DIRECT_EXT_PACK ${package} ${library_name})
  foreach(dep_package IN LISTS DIRECT_EXT_PACK)
    list_Component_Direct_External_Component_Dependencies(DIRECT_EXT_COMPS ${package} ${library_name} ${dep_package})
    foreach(dep_component IN LISTS DIRECT_EXT_COMPS)
      get_Package_Component_Target(RES_TARGET ${dep_package} ${dep_component})
      get_Package_Version(VERSION_STR ${dep_package})
      set(DEPENDENT_PKG_MODULE_NAME "${RES_TARGET} >= ${VERSION_STR}") # version constraint here is less STRONG as any version greater than the one specified should work
      is_Component_Exported(PACK_EXPORTED ${package} ${library_name} ${dep_package} ${dep_component})
      if( FORCE_EXPORT OR PACK_EXPORTED)#otherwise shared libraries export their dependencies in it is explicitly specified (according to pkg-config doc)
        if(_PKG_CONFIG_COMPONENT_REQUIRES_)
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${_PKG_CONFIG_COMPONENT_REQUIRES_}, ${DEPENDENT_PKG_MODULE_NAME}")
        else()
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${DEPENDENT_PKG_MODULE_NAME}")
        endif()
      else()
        if(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_)
          set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_}, ${DEPENDENT_PKG_MODULE_NAME}")
        else()
          set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${DEPENDENT_PKG_MODULE_NAME}")
        endif()
      endif()
    endforeach()
  endforeach()

  #3.b manage internal dependencies
  list_Component_Direct_Internal_Dependencies(DIRECT_INT_DEPS ${package} ${library_name})
  foreach(dep_component IN LISTS DIRECT_INT_DEPS)
    get_Package_Component_Target(RES_TARGET ${package} ${dep_component})
    get_Package_Version(VERSION_STR ${package})
    set(DEPENDENT_PKG_MODULE_NAME "${RES_TARGET} = ${VERSION_STR}")# STRONG version constraint between component of the same package
    is_Component_Exported(PACK_EXPORTED ${package} ${library_name} ${package} ${dep_component})
    if( FORCE_EXPORT OR PACK_EXPORTED) #otherwise shared libraries export their dependencies if it is explicitly specified (according to pkg-config doc)
        if(_PKG_CONFIG_COMPONENT_REQUIRES_)
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${_PKG_CONFIG_COMPONENT_REQUIRES_}, ${DEPENDENT_PKG_MODULE_NAME}")
        else()
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${DEPENDENT_PKG_MODULE_NAME}")
        endif()
    else()
      if(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_)
        set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_}, ${DEPENDENT_PKG_MODULE_NAME}")
      else()
        set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${DEPENDENT_PKG_MODULE_NAME}")
      endif()
    endif()
  endforeach()

  #3.c manage dependencies to other packages
  # dependencies are either generated when source package is built
  # or if not, just after the call to setup_Pkg_Config_Variables
  list_Component_Direct_Native_Package_Dependencies(DIRECT_NAT_PACK ${package} ${library_name})
  foreach(dep_package IN LISTS DIRECT_NAT_PACK)
    list_Component_Direct_Native_Component_Dependencies(DIRECT_NAT_COMPS ${package} ${library_name} ${dep_package})
    foreach(dep_component IN LISTS DIRECT_NAT_COMPS)
      get_Package_Component_Target(RES_TARGET ${dep_package} ${dep_component})
      get_Package_Version(VERSION_STR ${dep_package})
      set(DEPENDENT_PKG_MODULE_NAME "${RES_TARGET} >= ${VERSION_STR}") # version constraint here is less STRONG as any version greater than the one specified should work
      is_Component_Exported(PACK_EXPORTED ${package} ${library_name} ${dep_package} ${dep_component})
      if( FORCE_EXPORT OR PACK_EXPORTED)#otherwise shared libraries export their dependencies in it is explicitly specified (according to pkg-config doc)
        if(_PKG_CONFIG_COMPONENT_REQUIRES_)
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${_PKG_CONFIG_COMPONENT_REQUIRES_}, ${DEPENDENT_PKG_MODULE_NAME}")
        else()
          set(_PKG_CONFIG_COMPONENT_REQUIRES_ "${DEPENDENT_PKG_MODULE_NAME}")
        endif()
      else()
        if(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_)
          set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_}, ${DEPENDENT_PKG_MODULE_NAME}")
        else()
          set(_PKG_CONFIG_COMPONENT_REQUIRES_PRIVATE_ "${DEPENDENT_PKG_MODULE_NAME}")
        endif()
      endif()
    endforeach()
  endforeach()
endmacro(setup_Pkg_Config_Variables)

message("generating pkg-config modules...")
reset_Managed_PC_Files_Variables()

#first clean existing pc files
list_Defined_Libraries(LIST_OF_LIBS)
foreach(library IN LISTS LIST_OF_LIBS)
	clean_Pkg_Config_Files(${CMAKE_BINARY_DIR}/share ${library})
endforeach()

#second generate pc files
foreach(library IN LISTS LIST_OF_LIBS)
  get_Component_Type(RES_TYPE ${library})
	if(NOT RES_TYPE STREQUAL "MODULE")#module libraries are not intended to be used at compile time
		generate_Pkg_Config_Files(${CMAKE_BINARY_DIR}/share ${PROJECT_NAME} ${CURRENT_PLATFORM} ${library})
	endif()
endforeach()

reset_Managed_PC_Files_Variables()

As you may see the code is quite complex. It consists in getting internal information about the package using dedicated plugin API in order to generate a pkg-config file for each library defined in the package and its dependencies. Functions of this API are for instance list_Component_Direct_Internal_Dependencies, list_Component_Direct_Native_Package_Dependencies, get_Package_Component_Target, get_Package_Version, etc. As any other CMake script you can define functions and macros to ease the description, as well as any other CMake command, like configure_file and install commands that are useful in this example to generate and install pkg-config files (look at the command install_Pkg_Config_File for instance).

You can also notice that it is possible to add extra files that can be found in the environemnt folder. Here the script uses the file src/pkg_config.pc.pre.in as a pattern of pkg-config file that is then adapted using configure_file. To do this the function get_Path_To_Environment is able to retrieve the path to the given environment making it possible to get access to some of its content.

Build the environment

Now, to test evaluation of the environemnt, we simply do as usual:

pid cd pkg-config
pid build

If everything works as expected, the output looks like:

...
[PID] INFO : environment pkg-config has been evaluated.
[PID] description of environment pkg-config solution
- configured extra tools:
  + pkg-config:
    * program: /usr/bin/pkg-config
    * plugin callbacks:
         + after components: /home/robin/soft/PID/pid-workspace/environments/pkg-config/src/use_pkg-config.cmake
...

It sepcifies that the executable pkg-config has been found on host and plugin call back has been registered.

Test the environment

In order to trully test the plugin you need to directly see its effects on packages. To do so you need to configure the workspace with a profile and then reconfigure a package to know what is the effect of a plugin, which make them a bit difficult to test.

Anyway we advise to use simple packages, like pid-rpath to test effects of a plugin, before testing with more complex packages.

Step 2: example of a plugin that provides new language support

Plugins can be used for many differents things, its up to developpers to decide what kind of behavior they want to add to PID. In the following example we will show, with the f2c environment, how to deal with code generators, that is a quite common type of tool. f2c is a code generator that transforms Fortran code into C code and that is useful if no fortran compiler is available on host platform for generating binary code for the target platform (that may be host itself).

Structure the plugin

Environment for f2c is created and described as usual:

project(f2c)

PID_Environment(
      AUTHOR 		        Robin Passama
			INSTITUTION				CNRS/LIRMM
			MAIL							robin.passama@lirmm.fr
			YEAR 							2020
			LICENSE 					CeCILL-C
			ADDRESS						git@gite.lirmm.fr:pid/environments/f2c.git
			PUBLIC_ADDRESS		https://gite.lirmm.fr/pid/environments/f2c.git
			DESCRIPTION 			"using the Fortran To C compiler to generate C code from fortran code"
      INFO              "generating C code from Fortran 77 code"
		)

PID_Environment_Constraints(CHECK check_f2c.cmake)
PID_Environment_Solution(HOST CONFIGURE configure_f2c.cmake)

build_PID_Environment()

As you can see it is very similar to the environment for pkg-config and nothing is special here. Simply notice that we defined no version constraint for the environment, simply because we consider that the f2c tool is now really stable and available versions are capable of generating code for any Fortran code.

Writing the check script

The check script src/check_f2c.cmake simply consists in checking if f2c tool is installed in host:

find_program(F2C_EXECUTABLE NAMES f2c)
if(NOT F2C_EXECUTABLE)#no f2c compiler found => no need to continue
	return_Environment_Check(FALSE)
endif()

return_Environment_Check(TRUE)

Writing the configuration script

The configuration script src/configure_f2c.cmake is also very simple:

evaluate_Host_Platform(EVAL_RESULT)
if(NOT EVAL_RESULT)
  install_System_Packages(
      APT     f2c
      PACMAN  f2c
  )
  evaluate_Host_Platform(EVAL_RESULT)
endif()
if(EVAL_RESULT)
  configure_Environment_Tool(EXTRA f2c
                             PROGRAM ${F2C_EXECUTABLE}
                             ON_DEMAND
                             CONFIGURATION f2c-libs            #require f2c library on target platform
                             PLUGIN DURING_COMPS use_f2c.cmake)
  return_Environment_Configured(TRUE)
endif()
return_Environment_Configured(FALSE)

The same pattern as for pkg-config configuration script is followed. The only specific aspect here is in the configure_Environment_Tool function:

  • the PLUGIN argument defines a script that is flagged as DURING_COMPS which means that the plugin script will execute just at beginning of the definition of each component. This is a quite common way of doing for code generators as the script needs to look into the component sources to check files with given file extensions (here fortran extensions) in order to be capable of generating C/C++ code from these files.
  • the ON_DEMAND argument specifies that the plugin will execute only if it is explicitly required by the package. This is also a common requirement for code generators. Indeed, those tools suppose that there are files with specific extensions among the source files of components, so a package that uses a code generator should always specify it using the check_PID_Environment function. For f2c, it is a bit more complex because the f2c tool has to be used only if no Fortran compiler is available so packages have to do a double check: first testing is Fortran language is available then if not requiring the use of f2c.
  • the CONFIGURATION argument is used to require, at package level, the use of a specific system configuration for target platform. In this example f2c requires f2c-libs, the libraries used to link C code generated by f2c tool. This is also a quite common case for code generators as they try to minimize the amount of generated code and so use intermediate functions provided by libraries.

Writing plugin scripts

So now let’s have a look at the script src/use_f2c.cmake:

set(managed_extensions .f .F .for)#Note: only Fortran 77 supported

is_Language_Available(AVAILABLE Fortran)
if(AVAILABLE)
  get_Current_Component_Files(ALL_PUB_HEADERS ALL_Fortran_SOURCES "${managed_extensions}")
  if(ALL_Fortran_SOURCES)#only do something if there are Fortran sources
    convert_Files_Extensions(ALL_GENERATED_FILES ALL_Fortran_SOURCES ".c")
    remove_Residual_Files("${ALL_GENERATED_FILES}")#remove C files if any already generated
  endif()
  return() #if Fortran is managed no need to use f2c
endif()

get_Environment_Configuration(f2c PROGRAM F2C_EXE)

is_First_Package_Configuration(FIRST_ONE)
if(FIRST_ONE)#generation of C files is made only once (Debug or Release) per package configuration
  # need to translate all fortran sources into C sources
  get_Current_Component_Files(ALL_PUB_HEADERS ALL_Fortran_SOURCES "${managed_extensions}")
  generate_Code_In_Place("${ALL_Fortran_SOURCES}" ".c" ${F2C_EXE} "[f2c] generating ")

  #finally add some info to the component (to give it info to build C/C++ code)
  configure_Current_Component(f2c INTERNAL CONFIGURATION f2c-libs)
endif()

To understand this script remmeber that:

  • it executes at beginning of each component definition
  • it generates C code from Fortran code
  • it only executes if the f2c environment has been explicitly required at package level.

The first action consists in testing if the Fortran language is available in current configuration of the workspace, using is_Language_Available function. If yes, then there is no need for the script to execute and even this could lead to bugs since the Fortran code could be used twice: first time directly by the Fortran compiler and second time with f2c. In this case the script simply removes the C files generated from Fortran files in a previous execution of f2c. Indeed we never know if the workspace configuration has changed in the meantime, now providing a compiler for Fortran, so we need to clean the component sources from generated code to avoid any trouble:

  • get_Current_Component_Files return the list of source files filtered with given extensions
  • convert_Files_Extensions is an utility that simply converts a list of file to a list of files with another extension.
  • remove_Residual_Files removes a list of files from component sources.

If Fortran is not supported then f2c can be used. So first thing to do is to get access to the f2c executable which is achieved by using the get_Environment_Configuration: the code generator can now be used from CMake code.

The use of is_First_Package_Configuration is for optimization, just to avoid regenerating the C code when the package in build in release and debug modes at same time (i.e. using the build command with default options).

The code is generated in the component sources folder using generate_Code_In_Place: the program F2C_EXE is called on all Fortran sources.

Finally, the component is configured to link with the libraries provided by f2c-libs system configuration using configure_Current_Component.

Build the environment

Now, to test evaluation of the environment, simply do:

pid cd f2c
pid build

If everything works as expected, the output looks like:

...
[PID] INFO : environment f2c has been evaluated.
[PID] description of environment f2c solution
- configured extra tools:
  + f2c:
    * program: /usr/bin/f2c
    * platform requirements: f2c-libs
    * plugin callbacks:
         + during components: /home/robin/soft/PID/pid-workspace/environments/f2c/src/use_f2c.cmake
...

Like for pkg-config it sepcifies that the executable for f2c has been found on host and plugin call back has been registered. The only difference is that there is also the requirement that target platform satisfies f2c-libs system configuration (i.e. that f2c libraries are installed in system).


Now you know how to generate configurations for customizing host platform with new behavior by defining plugins in environments, let’s see how to use environments to configure a workspace.