CMake for Fortran: Avoiding Build Failures with Module Files

Summary

A developer transitioning a legacy Fortran project from a manual Makefile to CMake encountered a critical build failure. Despite explicitly setting include directories, the compiler failed to locate the .mod files required for the USE statements in the Fortran source. The build process stalled because the compiler flags intended to point to the library modules were not being correctly propagated to the compilation phase of the specific translation units.

Root Cause

The failure stems from a misunderstanding of how CMake manages build targets and the distinction between Linker Search Paths and Compiler Include Paths.

  • Incorrect Scope: While target_include_directories was used, it often fails in complex builds if the Fortran compiler (like GFortran) requires specific flag formatting or if the directory is being overridden by a global property.
  • Flag vs. Directory Confusion: The user attempted add_compile_options(-I/path/...), which is a global command. In modern CMake, global commands are discouraged because they lack target-based granularity, leading to race conditions or ignored flags when multiple sub-projects are involved.
  • Module Path Specificity: Fortran compilers are extremely strict about the Module Path (-I flag). If the CMake configuration generates a build system where the compiler invocation for CLSetup.f does not explicitly include the -I flag at the moment of compilation, the .mod files become invisible.

Why This Happens in Real Systems

In production environments, this “Transition Friction” occurs due to:

  • Legacy Complexity: Large-scale scientific computing projects often rely on tightly coupled, non-standard library structures (like Focal/CLFortran) that were designed for manual flag injection, not modern dependency management.
  • Abstraction Leaks: CMake is an abstraction layer. When a developer tries to force “old-school” Makefile logic into a “modern” CMake structure, the abstraction breaks, and the user loses visibility into exactly what command the compiler is actually executing.
  • Implicit vs. Explicit Dependencies: In a Makefile, you manually order everything. In CMake, if a dependency is not defined as an exported target, the compiler might attempt to compile a source file before the environment is correctly configured for that specific file’s requirements.

Real-World Impact

  • Developer Velocity Loss: Engineers spend hours debugging “file not found” errors that are actually configuration errors, not code errors.
  • Broken CI/CD Pipelines: Build scripts that work on a local machine with hardcoded paths fail in automated environments where absolute paths are invalid.
  • Integration Bottlenecks: As shown in this case, a single developer’s inability to integrate their module into the “larger program” managed by colleagues halts the entire team’s progress.

Example or Code (if necessary and relevant)

The correct way to handle this in modern CMake is to treat the library as a Target rather than a collection of strings.

# 1. Define the Focal Library as a formal interface target
add_library(Focal_Library INTERFACE)

# 2. Attach the include directories (for .mod files) to the INTERFACE
target_include_directories(Focal_Library INTERFACE 
    "/path_to_focal/focal/mod"
    "/usr/local/cuda/include"
)

# 3. Attach the link directories (for .a/.so files) to the INTERFACE
target_link_directories(Focal_Library INTERFACE 
    "/path_to_focal/focal/lib"
    "/usr/local/cuda/lib64"
)

# 4. Attach the specific libraries needed
target_link_libraries(Focal_Library INTERFACE 
    Focal 
    OpenCL 
    stdc++
)

# 5. Simply link your executable to the Focal_Library target
# CMake will now automatically propagate all -I and -L flags to OurTarget
add_executable(OurTarget main.f CLSetup.f)
target_link_libraries(OurTarget PRIVATE Focal_Library)

How Senior Engineers Fix It

Senior engineers move away from “Path Management” and toward “Target Management”:

  • Encapsulation: Instead of passing raw paths around the CMakeLists.txt, they wrap the external library into an INTERFACE target. This ensures that anyone who links to Focal_Library automatically inherits the correct -I (include) and -L (link) flags.
  • Verification: They use make VERBOSE=1 or ninja -v to inspect the actual command line being sent to the compiler. This confirms whether the -I/path/to/mod flag is actually present in the gfortran call.
  • Avoid Hardcoding: They use find_path and find_library to locate Focal and CUDA, making the build portable across different developer workstations and build servers.

Why Juniors Miss It

  • Focus on “The Error” rather than “The System”: Juniors see “Cannot open module file” and try to fix it by adding more include paths, rather than investigating how those paths are being applied to the compiler.
  • Procedural vs. Declarative Thinking: Juniors often think procedurally (“I must tell the compiler where this is”), while CMake is a declarative system (“I am defining a relationship between these two targets”).
  • Misunderstanding Scopes: They often use link_directories() or include_directories() (global scope) which can be overwritten or ignored in complex, multi-directory CMake projects, rather than using target_ scoped commands.

Leave a Comment