How do you make a conda package from a cmake based fortran project?

I’m trying to make a fortran library (this repo) into a conda-forge package. After installing via conda, I want to be able to access the package in an external project via something like.

find_package(fortran-yaml-c REQUIRED)
target_link_libraries(<target> ${FORTRAN_YAML_C_LIBRARIES})
target_include_directories(<target> PUBLIC ${FORTRAN_YAML_C_INCLUDE_DIRS})

The find_package command would define the two variables, FORTRAN_YAML_C_LIBRARIES and FORTRAN_YAML_C_INCLUDE_DIRS, which can be easily linked to new libraries.

I’m having issues working out how to put together the install part of my CMakeLists.txt, so that the conda package contains all necessary when I run conda-build .

Do you intent to make a pre-built library which you download from conda-forge? If so I think your only option is to make a C-interface to your library. Fortran procedures does not have a standard interface and different compilers will do name mangling differently. If your code is in a module (which of course is good practice) then you will have to ship .mod-files in your pacakge in order to be able to do use mylib, only: ... in the code that uses it. The format of .mod-files does however differ for each compiler so your package can only be used by the same compiler you built it with. Potentially that also includes the same version of that comiler!

For distribution of Fortran libraries it is generally better to have users compile the source code themselves. This is the approach fpm is taking and it is sensible. If you want to continue to use CMake I’d recommend that you look into CMake Package Manager which does the same kind of dependency management as fpm does for CMake.

2 Likes

You will need a lot of boilerplate to make CMake install properly, for conda packages especially you also want the possibility to find your dependencies rather than vendoring them (e.g. via git-submodules). Aiming for a complete install structure like the one shown here, requires almost 300 lines of CMake.

$PREFIX
├── lib
│   ├── cmake
│   │   └── fortran-yaml-c
│   │       ├── Findyaml.cmake
│   │       ├── fortran-yaml-c-config-version.cmake
│   │       ├── fortran-yaml-c-config.cmake
│   │       ├── fortran-yaml-c-targets-relwithdebinfo.cmake
│   │       └── fortran-yaml-c-targets.cmake
│   ├── fortran-yaml-c
│   │   ├── fortran_yaml_c.mod
│   │   └── yaml_types.mod
│   ├── libfortran-yaml-c.a
│   └── pkgconfig
│       └── fortran-yaml-c.pc
└── share
    └── licenses
        └── fortran-yaml-c
            └── LICENSE

I attached my changes in a patch below, feel free to use it for your project.

CMake patch
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e31bc66..51ff77f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,18 +1,70 @@
 
 cmake_minimum_required(VERSION 3.4)
-project (FORTRAN_YAML_C VERSION 0.1.0 LANGUAGES C Fortran)
+project (fortran-yaml-c VERSION 0.1.0 LANGUAGES C Fortran)
+
+# Follow GNU conventions for installing directories
+include(GNUInstallDirs)
+
+# General settings
+add_subdirectory("config")
 
 include(FortranCInterface)
 FortranCInterface_VERIFY()
 
-add_subdirectory(libyaml EXCLUDE_FROM_ALL)
+if(NOT TARGET "yaml::yaml")
+  find_package("yaml" REQUIRED)
+endif()
+
+add_library(${PROJECT_NAME} yaml_types.f90 fortran_yaml_c.f90 libyaml_interface.c)
+set_target_properties(
+  ${PROJECT_NAME}
+  PROPERTIES
+  Fortran_MODULE_DIRECTORY "${PROJECT_BINARY_DIR}/modulefiles"
+)
+target_link_libraries(${PROJECT_NAME} yaml)
+target_include_directories(${PROJECT_NAME} PRIVATE libyaml/include)
+target_include_directories(
+  ${PROJECT_NAME}
+  PUBLIC
+  $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/modulefiles>
+  $<INSTALL_INTERFACE:${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}>
+)
+if(NOT EXISTS "${PROJECT_BINARY_DIR}/modulefiles")
+  make_directory("${PROJECT_BINARY_DIR}/modulefiles")
+endif()
 
-add_library(fortran-yaml-c yaml_types.f90 fortran_yaml_c.f90 libyaml_interface.c)
-target_link_libraries(fortran-yaml-c yaml)
-target_include_directories(fortran-yaml-c PUBLIC libyaml/include)
+# Install library, create target file
❯ git diff main cmake | cat
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e31bc66..51ff77f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,18 +1,70 @@
 
 cmake_minimum_required(VERSION 3.4)
-project (FORTRAN_YAML_C VERSION 0.1.0 LANGUAGES C Fortran)
+project (fortran-yaml-c VERSION 0.1.0 LANGUAGES C Fortran)
+
+# Follow GNU conventions for installing directories
+include(GNUInstallDirs)
+
+# General settings
+add_subdirectory("config")
 
 include(FortranCInterface)
 FortranCInterface_VERIFY()
 
-add_subdirectory(libyaml EXCLUDE_FROM_ALL)
+if(NOT TARGET "yaml::yaml")
+  find_package("yaml" REQUIRED)
+endif()
+
+add_library(${PROJECT_NAME} yaml_types.f90 fortran_yaml_c.f90 libyaml_interface.c)
+set_target_properties(
+  ${PROJECT_NAME}
+  PROPERTIES
+  Fortran_MODULE_DIRECTORY "${PROJECT_BINARY_DIR}/modulefiles"
+)
+target_link_libraries(${PROJECT_NAME} yaml)
+target_include_directories(${PROJECT_NAME} PRIVATE libyaml/include)
+target_include_directories(
+  ${PROJECT_NAME}
+  PUBLIC
+  $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/modulefiles>
+  $<INSTALL_INTERFACE:${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}>
+)
+if(NOT EXISTS "${PROJECT_BINARY_DIR}/modulefiles")
+  make_directory("${PROJECT_BINARY_DIR}/modulefiles")
+endif()
 
-add_library(fortran-yaml-c yaml_types.f90 fortran_yaml_c.f90 libyaml_interface.c)
-target_link_libraries(fortran-yaml-c yaml)
-target_include_directories(fortran-yaml-c PUBLIC libyaml/include)
+# Install library, create target file
+install(
+  TARGETS
+  "${PROJECT_NAME}"
+  EXPORT
+  "${PROJECT_NAME}-targets"
+  LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+  ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+)
+# Install target file
+install(
+  EXPORT
+  "${PROJECT_NAME}-targets"
+  NAMESPACE
+  "${PROJECT_NAME}::"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
+)
+# Install all module files
+install(
+  DIRECTORY
+  "${PROJECT_BINARY_DIR}/modulefiles/"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}"
+)
+# Install the license file
+install(
+  FILES
+  "LICENSE"
+  DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/${PROJECT_NAME}"
+)
 
 add_executable(test_yaml test_yaml.f90)
-target_link_libraries(test_yaml fortran-yaml-c)
+target_link_libraries(test_yaml ${PROJECT_NAME})
 
 add_executable(example example.f90)
-target_link_libraries(example fortran-yaml-c)
+target_link_libraries(example ${PROJECT_NAME})
diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt
new file mode 100644
index 0000000..706c4a1
--- /dev/null
+++ b/config/CMakeLists.txt
@@ -0,0 +1,59 @@
+option(BUILD_SHARED_LIBS "Whether the libraries built should be shared" FALSE)
+
+# Set build type as CMake does not provide defaults
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(
+    CMAKE_BUILD_TYPE "RelWithDebInfo"
+    CACHE STRING "Build type to be used."
+    FORCE
+  )
+  message(
+    STATUS
+    "Setting build type to '${CMAKE_BUILD_TYPE}' as none was specified."
+  )
+endif()
+
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" PARENT_SCOPE)
+install(
+  DIRECTORY
+  "${CMAKE_CURRENT_SOURCE_DIR}/cmake/"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
+)
+
+include(CMakePackageConfigHelpers)
+configure_package_config_file(
+  "${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake"
+  INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
+)
+write_basic_package_version_file(
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake"
+  VERSION "${PROJECT_VERSION}"
+  COMPATIBILITY SameMinorVersion
+)
+install(
+  FILES
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake"
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
+)
+
+if(BUILD_SHARED_LIBS)
+  set(PKG_CONFIG_REQUIRES "Requires.private")
+else()
+  set(PKG_CONFIG_REQUIRES "Requires")
+endif()
+set(PKG_CONFIG_REQUIREMENTS)
+
+configure_file(
+  "${CMAKE_CURRENT_SOURCE_DIR}/pc.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc"
+  @ONLY
+)
+install(
+  FILES
+  "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig"
+)
+
diff --git a/config/cmake/Findyaml.cmake b/config/cmake/Findyaml.cmake
new file mode 100644
index 0000000..465ada0
--- /dev/null
+++ b/config/cmake/Findyaml.cmake
@@ -0,0 +1,141 @@
+# SPDX-Identifier: MIT
+
+#[[.rst:
+Find yaml
+---------------
+
+Makes the yaml project available.
+
+Imported Targets
+^^^^^^^^^^^^^^^^
+
+This module provides the following imported target, if found:
+
+``yaml``
+  The yaml library
+
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+This module will define the following variables:
+
+``YAML_FOUND``
+  True if the yaml library is available
+
+``YAML_SOURCE_DIR``
+  Path to the source directory of the yaml project,
+  only set if the project is included as source.
+
+``YAML_BINARY_DIR``
+  Path to the binary directory of the yaml project,
+  only set if the project is included as source.
+
+Cache variables
+^^^^^^^^^^^^^^^
+
+The following cache variables may be set to influence the library detection:
+
+``YAML_FIND_METHOD``
+  Methods to find or make the project available. Available methods are
+  - ``cmake``: Try to find via CMake config file
+  - ``pkgconf``: Try to find via pkg-config file
+  - ``subproject``: Use source in subprojects directory
+  - ``fetch``: Fetch the source from upstream
+
+``YAML_DIR``
+  Used for searching the CMake config file
+
+``YAML_SUBPROJECT``
+  Directory to find the yaml subproject, relative to the project root
+
+#]]
+
+set(_lib "yaml")
+set(_pkg "YAML")
+set(_url "https://github.com/yaml/libyaml")
+
+if(NOT DEFINED "${_pkg}_FIND_METHOD")
+  if(DEFINED "${PROJECT_NAME}-dependency-method")
+    set("${_pkg}_FIND_METHOD" "${${PROJECT_NAME}-dependency-method}")
+  else()
+    set("${_pkg}_FIND_METHOD" "cmake" "subproject" "fetch")
+  endif()
+  set("_${_pkg}_FIND_METHOD")
+endif()
+
+foreach(method ${${_pkg}_FIND_METHOD})
+  if(TARGET "${_lib}")
+    break()
+  endif()
+
+  if("${method}" STREQUAL "cmake")
+    message(STATUS "${_lib}: Find installed package")
+    if(DEFINED "${_pkg}_DIR")
+      set("_${_pkg}_DIR")
+      set("${_lib}_DIR" "${_pkg}_DIR")
+    endif()
+    find_package("${_lib}" CONFIG QUIET)
+    if("${_lib}_FOUND")
+      message(STATUS "${_lib}: Found installed package")
+      break()
+    endif()
+  endif()
+
+  if("${method}" STREQUAL "subproject")
+    if(NOT DEFINED "${_pkg}_SUBPROJECT")
+      set("_${_pkg}_SUBPROJECT")
+      set("${_pkg}_SUBPROJECT" "libyaml")
+    endif()
+    set("${_pkg}_SOURCE_DIR" "${PROJECT_SOURCE_DIR}/${${_pkg}_SUBPROJECT}")
+    set("${_pkg}_BINARY_DIR" "${PROJECT_BINARY_DIR}/${${_pkg}_SUBPROJECT}")
+    if(EXISTS "${${_pkg}_SOURCE_DIR}/CMakeLists.txt")
+      message(STATUS "Include ${_lib} from ${${_pkg}_SUBPROJECT} directory")
+      add_subdirectory(
+        "${${_pkg}_SOURCE_DIR}"
+        "${${_pkg}_BINARY_DIR}"
+        EXCLUDE_FROM_ALL
+      )
+
+      break()
+    endif()
+  endif()
+
+  if("${method}" STREQUAL "fetch")
+    message(STATUS "Retrieving ${_lib} from ${_url}")
+    include(FetchContent)
+    FetchContent_Declare(
+      "${_lib}"
+      GIT_REPOSITORY "${_url}"
+      GIT_TAG "HEAD"
+      )
+    FetchContent_MakeAvailable("${_lib}")
+
+    FetchContent_GetProperties("${_lib}" SOURCE_DIR "${_pkg}_SOURCE_DIR")
+    FetchContent_GetProperties("${_lib}" BINARY_DIR "${_pkg}_BINARY_DIR")
+    break()
+  endif()
+
+endforeach()
+
+if(TARGET "${_lib}")
+  set("${_pkg}_FOUND" TRUE)
+else()
+  set("${_pkg}_FOUND" FALSE)
+endif()
+
+if(DEFINED "_${_pkg}_SUBPROJECT")
+  unset("${_pkg}_SUBPROJECT")
+  unset("_${_pkg}_SUBPROJECT")
+endif()
+if(DEFINED "_${_pkg}_DIR")
+  unset("${_lib}_DIR")
+  unset("_${_pkg}_DIR")
+endif()
+if(DEFINED "_${_pkg}_FIND_METHOD")
+  unset("${_pkg}_FIND_METHOD")
+  unset("_${_pkg}_FIND_METHOD")
+endif()
+unset(_lib)
+unset(_pkg)
+unset(_url)
diff --git a/config/config.cmake.in b/config/config.cmake.in
new file mode 100644
index 0000000..3521b73
--- /dev/null
+++ b/config/config.cmake.in
@@ -0,0 +1,11 @@
+@PACKAGE_INIT@
+
+if(NOT TARGET "@PROJECT_NAME@::@PROJECT_NAME@")
+  include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake")
+
+  include(CMakeFindDependencyMacro)
+
+  if(NOT TARGET "yaml::yaml")
+    find_dependency("yaml")
+  endif()
+endif()
diff --git a/config/pc.in b/config/pc.in
new file mode 100644
index 0000000..095b69d
--- /dev/null
+++ b/config/pc.in
@@ -0,0 +1,10 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@
+includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
+
+Name: @PROJECT_NAME@
+Description: @PROJECT_DESCRIPTION@
+@PKG_CONFIG_REQUIRES@: @PKG_CONFIG_REQUIREMENTS@
+Version: @PROJECT_VERSION@
+Libs: -L${libdir} -l@PROJECT_NAME@
+Cflags: -I${libdir}/@PROJECT_NAME@
1 Like

@plevold Well the fortran library is an interface to a C-library. So making a C-interface for it wouldn’t be too useful haha.

Fortran procedures does not have a standard interface and different compilers will do name mangling differently.

OK this is an issue.

CMake Package Manager looks great!! Thanks! I think this might be the best solution for now.

@awvwgk Thanks so much for the boiler plate cmake. I think, given the issues with fortran compilers being incompatible, I’m going to just go with the CMake Package Manager.

1 Like

I tested out the CMake Package Manager. It is works really well! Highly recommended.

1 Like