Fortran: Importing `.mod` with fallback to `.f90`

I am thinking of a workaround compatibility until the CXX modules logic can be ported to Fortran as well. My idea is the following:

  1. Pacakge both the .mod files and the .f90 files with the install(TARGETS) all of which are installed at PUBLIC_HEADER (Maybe FILE_SET would be better, but how to do that?)
  2. Inside the <Package>Config.cmake run try_compile consuming a .mod file
  3. If it fails, then target_sources(INTERFACE) that imported target

My only concern here is, how to hide the packaged .mod file from being used. My hope is that CMAKE_Fortran_MODULE_DIRECTORY will take precedence over -I include search. What do you guys think?

1 Like

I don’t think Fortran compilers support .mod-only compilation modes. Also, .mod files are searched for via -I flags, so precedence is a difficult thing to untangle there.

FILE_SET would definitely be a better way of doing this, but as long as module searches are implicit searches rather than “find module X in this file”, I don’t think there’s a lot to gain in practice.

@ben.boeckel I have found quite a neat way to support it in the meantime:

	# TODO: CMake 3.25 use the modern try_compile signature. Remove the explicit CMakeScratch
	try_compile(spglib_fortran_try_compile ${CMAKE_BINARY_DIR}/CMakeFiles/CMakeScratch/spglib_fortran
			SOURCES ${CMAKE_CURRENT_LIST_DIR}/try_compile.f90
			LINK_LIBRARIES Spglib::fortran_mod
	)
	if (spglib_fortran_try_compile)
		# If the compilation was successful, use the module version of the library
		add_library(Spglib::fortran ALIAS Spglib::fortran_mod)
	else ()
		# Otherwise, assume it was because of incompatible compiler
		# Add the bundled `.f90` files as sources instead
		add_library(Spglib::fortran ALIAS Spglib::fortran_include)
	endif ()

Where I basically have defined 2 targets, one with target_sources(INTERFACE), the other with the normal link library.

I think it would be good to document this design, what do you think?

1 Like

That feels like it’s going to end up with duplicated symbols because the .f90 files will be compiled and linked in the linking target as well. Not that Fortran has an ODR that I’m aware of, but linkers still do.

The key part is that fortran_include target contains only target_sources (and link libraries to dependencies) so it does not link to the compiled library in fortran_mod. In other words the former is add_library(INTERFACE), and the latter add_library(SHARED) [1]. There will be duplicated fortran symbols as each consuming project will compile the f90 files themselves, but that should be fine if it’s not re-exposed isn’t it?


  1. spglib/fortran/CMakeLists.txt at develop · spglib/spglib · GitHub ↩︎

If you have:

add_library(uses_fortran_sources)
target_link_libraries(uses_fortran_sources PUBLIC Spglib::fortran_include) # sources added here

add_library(transitive)
target_link_libraries(transitive uses_fortran_sources) # sources also added here

Oh, fair point. Users should be using target_link_libraries(PRIVATE) whenever possible. Would making an OBJECT layer between that help, assuming that uses_fortran_sources and transitive do not re-export the interface of Spglib?

I.e.:

	# TODO: CMake 3.25 use the modern try_compile signature. Remove the explicit CMakeScratch
	try_compile(spglib_fortran_try_compile ${CMAKE_BINARY_DIR}/CMakeFiles/CMakeScratch/spglib_fortran
			SOURCES ${CMAKE_CURRENT_LIST_DIR}/try_compile.f90
			LINK_LIBRARIES Spglib::fortran_mod
	)
	if (spglib_fortran_try_compile)
		# If the compilation was successful, use the module version of the library
		add_library(Spglib::fortran ALIAS Spglib::fortran_mod)
	else ()
		# Otherwise, assume it was because of incompatible compiler
		# Add the bundled `.f90` files as sources instead
		add_library(Spglib_fortran_obj OBJECT)
		add_library(Spglib_fortran_interface INTERFACE)
		target_link_libraries(Spglib_fortran_obj PRIVATE Spglib::fortran_include PUBLIC Spglib::symspg)
		target_link_libraries(Spglib_fortran_interface INTERFACE Spglib_fortran_obj $<TARGET_OBJECTS:Spglib_fortran_obj>)
		add_library(Spglib::fortran ALIAS Spglib_fortran_interface)
	endif ()

Should also confirm a few things about this:

  • Is the restriction on namespace::target convention only for shared/static/executable or does it apply for object/interface as well?
  • Is it possible to export object libraries such that it include the source and not the compiled objects there?

This syntax is for exported targets (which become IMPORTED targets on the other side) and ALIAS targets (which you can add in your build to match consumers). When CMake sees a :: in a linked library name, it knows it expects a target (rather than falling back to “try -lstring”).

Probably; still feels like an ODR violation waiting to happen to me, but I don’t see why this wouldn’t work.

Ok I did some simple tests. On the project side, we cannot export and object library to be an object library on the import side. It will still be an INTERFACE IMPORTED. At least it would not install object files.

Ok, so if we were to make an object target for that it would have to non-namespaced, and given the previous experiment, it would have to live in the Config.cmake file.

It should solve the ODR w.r.t. object/modules for the build, but yeah it is prone to duplication as it is consumed across multiple libraries. But I don’t think there would be a design around that without injecting a shared library in one of the consuming projects.

The one downside that I could see in this approach is that the object libraries does not know when it should be PIC and not. Is there a downside for making it always PIC? Are there other irregularities around shared vs static?

I think you need to explicitly install the objects with install(OBJECTS DESTINATION) to get an OBJECT library on the other side.

AFAIK, no. It allows your binaries to be ASLR-located.

Oh interesting. I’ve tried it out a bit, but the OBJECT IMPORTED does not get any private sources link-libraries, etc. that it can use to re-build itself, and we cannot attach anything other than INTERFACE on them, so we would still need to define it in Config.cmake

Ok, then we likely need some more thought into how this goes. Note that $<COMPILE_ONLY> allows for hiding this kind of stuff from linking usages. But we’ll probably want a FILE_SET for Fortran modules like we do for C++ to separate things out. It’ll definitely need a policy.

While formalizing this as a feature request issue might be warranted, I really don’t know if/when any time to make it a priority would happen.