Visualizing Fortran Projects with fpm: Create Stunning Module Dependency Charts

I just created the project fpm-modules, a fpm plugin to generate module dependency graphs. Since it took me some time and research to create it, I wrote a tutorial out of it, hoping it could be useful for anyone else willing to try to use the fpm api. Any comments about this tutorial are more than welcome.

Introduction

Ever tried to untangle a complex Fortran project? It’s like navigating a maze without a map. Dependency charts can be a lifesaver, showing how modules and source files connect, making refactoring or debugging much easier. The Fortran Package Manager (fpm) is a modern tool that simplifies Fortran development, similar to how Cargo powers Rust. Beyond building projects, fpm’s API lets you create custom plugins, like one for generating interactive module dependency charts.

Inspired by a Fortran community discussion, I discovered fpm’s ability to dump a project’s structure into a JSON or TOML file. This “build model” contains all the info needed to analyze dependencies programmatically. While you could parse this file manually, fpm’s API makes it easier by letting you work directly with the model in Fortran. In this tutorial, we’ll build a plugin to create a visual dependency chart using Mermaid, a JavaScript-based diagramming tool. Whether you’re refactoring legacy code or starting fresh, this tool will help you see the big picture.

The complete code is available on GitHub.

Why Module Dependency Charts?

Modern Fortran projects rely heavily on modules, but tools like Doxygen are better suited for older Fortran 77 projects, where they generate caller-callee graphs. For example, here’s a Doxygen-generated graph for the daskr project. With the following Doxyfile, it’s relatively easy to generate this kind of graph.

INPUT                  = ./src
HAVE_DOT               = YES
EXTRACT_ALL            = YES
EXTRACT_PRIVATE        = YES
EXTRACT_STATIC         = YES
CALL_GRAPH             = YES
CALLER_GRAPH           = YES
DISABLE_INDEX          = YES 
GENERATE_TREEVIEW      = YES
RECURSIVE              = YES
COLLABORATION_GRAPH    = YES
GRAPHICAL_HIERARCHY    = YES
DOT_CLEANUP            = NO
GENERATE_HTML          = YES
GENERATE_LATEX         = NO

While useful for functional code, Doxygen struggles with module dependencies. Our plugin will use fpm’s API to create a chart like this :

Sample module dependency chart

Our goal is to loop through a project’s modules, identify their dependencies, and output an interactive chart using Mermaid. There is already fpm-deps to visualize project dependencies, but nothing for modules.

Understanding fpm’s Role

fpm is more than a build tool—it’s a gateway to modern Fortran development. By parsing your project’s structure, fpm creates a build model that details every module, source file, and dependency. Our plugin will tap into this model to generate a visual dependency chart. Let’s get started!

Setting Up the Project

First, ensure fpm is installed. Then, follow these steps:

  1. Create a New Project:
    Run this command to generate a project called fpm-modules:

    fpm new fpm-modules
    

    This creates a directory with a fpm.toml file, a src folder, and sample code.

  2. Configure fpm.toml:
    Open fpm.toml and add fpm as a dependency to use its API. Here’s the updated file:

    name = "fpm-modules"
    version = "0.1.0"
    license = "MIT"
    author = "Your Name"
    maintainer = "Your Name"
    copyright = "Copyright 2025, Your Name"
    description = "Generate dependency graphs of Fortran modules"
    
    [build]
    auto-executables = true
    auto-tests = true
    auto-examples = true
    module-naming = false
    
    [install]
    library = false
    test = false
    
    [fortran]
    implicit-typing = false
    implicit-external = false
    source-form = "free"
    
    [dependencies]
    fpm = { git = "https://github.com/fortran-lang/fpm" }
    

    The [dependencies] section pulls in fpm’s source code, giving us access to its API.

  3. Set Up the Main Module:
    In the src folder, create package.f90 with the necessary fpm modules:

    module fpm_package
        use fpm_strings, only: string_t
        use fpm_command_line, only: fpm_build_settings, get_command_line_settings, get_fpm_env
        use fpm_dependency, only: dependency_tree_t, new_dependency_tree
        use fpm_error, only: error_t, fpm_stop
        use fpm_filesystem, only: join_path
        use fpm_manifest, only: package_config_t, get_package_data
        use fpm, only: build_model
        implicit none
        private
    
        type, extends(package_config_t), public :: package
        contains
            procedure, public :: create => package_create
        end type
    end module
    

These modules provide access to fpm’s build model, file system utilities, and error handling. The key components are package_config_t (for project configuration) and build_model (for generating the dependency tree).

Exploring fpm’s Build Model

fpm’s build model is a structured representation of your project’s modules, source files, and dependencies. Our plugin will use this model to identify module dependencies and create a chart.

The key component is the package_config_t type, which contains a nested model object. Here’s a simplified view:

-    package_config_t
                    |- model
                           |- external_modules(:)
                           |- packages(:)
                                       |- name
                                       |- sources(:)
                                                  |- modules_provided(:)
                                                  |- module_used(:)
  • external_modules: External modules (e.g., ifcore from Intel or openmp) linked to the project but not in the source code.
  • packages: Project packages, each containing:
    • name: The package name.
    • sources: Source files, each with:
      • modules_provided: Modules defined in the file (e.g., module math_utils).
      • module_used: Modules imported via use statements (e.g., use string_utils).

For example, if your project has a file math.f90:

module math_utils
    use string_utils
    implicit none
end module

The build model lists math_utils under modules_provided and string_utils under module_used.

Note: fpm organizes its model by source files, not individual modules. If a file contains multiple modules, fpm associates all module_used entries with the file. Our plugin will link dependencies to the first module in a file to simplify the chart.

Building the Dependency Model

We’ll use the build_model subroutine to create the model. It requires a fpm_build_settings object, which we’ll configure with minimal settings since we’re only generating the model, not compiling the project.

Here’s the code:

subroutine package_create(this)
    class(package), intent(inout) :: this
    type(fpm_build_settings) :: settings
    type(error_t), allocatable :: error

    ! Configure build settings (most are irrelevant for model generation)
    settings = fpm_build_settings( &
        & profile=" ", & ! No specific build profile
        & dump="fpm_model.toml", & ! Output model file (optional)
        & prune=.false., & ! Don’t prune unused dependencies
        & compiler=get_fpm_env("FC", "gfortran"), & ! Default Fortran compiler
        & c_compiler=get_fpm_env("CC", " "), & ! No C compiler needed
        & cxx_compiler=get_fpm_env("CXX", " "), & ! No C++ compiler needed
        & archiver=get_fpm_env("AR", " "), & ! No archiver needed
        & path_to_config=" ", & ! No config file needed
        & flag=" ", & ! No Fortran flags
        & cflag=" ", & ! No C flags
        & cxxflag=" ", & ! No C++ flags
        & ldflag=" ", & ! No linker flags
        & list=.false., & ! Don’t list files
        & show_model=.false., & ! Don’t display model
        & build_tests=.false., & ! Exclude tests
        & verbose=.false.) ! Minimal output

    ! Build the model
    call build_model(this%model, settings, this%package_config_t, error)
    if (allocated(error)) then
        call fpm_stop(1, '*package_create* Model error: '//error%message)
    end if
end subroutine

Generating the Dependency Chart

We’ll generate an HTML file with Mermaid syntax to create an interactive flowchart. The chart includes subgraphs for each package and edges for module dependencies, rendered with Mermaid and Panzoom for interactivity.

Here’s the code:

subroutine generate_chart(this, filepath)
    class(package), intent(inout) :: this
    character(len=*), intent(in) :: filepath
    integer :: unit, i, j, k, l

    ! Open HTML file for the Mermaid chart
    open(newunit=unit, file=filepath, action='write', status='replace')
    
    ! Write HTML and Mermaid header
    write(unit, '(*(A,/))') &
        '<!DOCTYPE html>', &
        '<html lang="en">', &
        '<head>', &
        '    <meta charset="utf-8">', &
        '</head>', &
        '<body>', &
        '    <div class="diagram-container" id="diagram-container">', &
        '        <pre class="mermaid">', &
        '        flowchart LR'

    ! Create subgraphs for each package
    do i = 1, size(this%model%packages)
        write(unit, '("      subgraph package_", A)') this%model%packages(i)%name
        do j = 1, size(this%model%packages(i)%sources)
            do k = 1, size(this%model%packages(i)%sources(j)%modules_provided)
                write(unit, '("          ", A)') this%model%packages(i)%sources(j)%modules_provided(k)%s
            end do
        end do
        write(unit, '(A)') '      end'
    end do

    ! Create a subgraph for external modules
    write(unit, '("      subgraph external_module")')
    do j = 1, size(this%model%external_modules)
        write(unit, '("          ", A)') this%model%external_modules(j)%s
    end do
    write(unit, '(A)') '      end'

    ! Add edges for module dependencies
    do i = 1, size(this%model%packages)
        do j = 1, size(this%model%packages(i)%sources)
            do k = 1, size(this%model%packages(i)%sources(j)%modules_provided)
                do l = 1, size(this%model%packages(i)%sources(j)%modules_used)
                    write(unit, '("      ", A, "-->", A)') &
                        this%model%packages(i)%sources(j)%modules_provided(k)%s, &
                        this%model%packages(i)%sources(j)%modules_used(l)%s
                end do
                exit ! Link dependencies to the first module in the file
            end do
        end do
    end do

    ! Close Mermaid and HTML, add scripts for rendering
    write(unit, '(*(A,/))') &
        '        </pre>', &
        '    </div>', &
        '<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>', &
        '<script src="https://unpkg.com/@panzoom/panzoom@4.6.0/dist/panzoom.min.js"></script>', &
        '<script>', &
        '    mermaid.initialize({startOnLoad:false, maxEdges: 4000});', &
        '    mermaid.run({', &
        '        querySelector: ".mermaid",', &
        '        postRenderCallback: (id) => {', &
        '            const container = document.getElementById("diagram-container");', &
        '            const svgElement = container.querySelector("svg");', &
        '            const panzoomInstance = Panzoom(svgElement, {', &
        '                maxScale: 5,', &
        '                minScale: 0.5,', &
        '                step: 0.1,', &
        '            });', &
        '            container.addEventListener("wheel", (event) => {', &
        '                panzoomInstance.zoomWithWheel(event);', &
        '            });', &
        '        }', &
        '    });', &
        '</script>', &
        '</body>', &
        '</html>'
    
    close(unit)
end subroutine

This code:

  1. Creates an HTML file with Mermaid syntax.
  2. Defines subgraphs for packages and external modules.
  3. Adds edges for dependencies (e.g., module_a --> module_b).
  4. Includes scripts for interactive rendering.

To view the chart, save the output as chart.html and open it in a browser. Try it with a small project to see the dependencies visualized.

Tip: Paste the Mermaid syntax into mermaid.live to preview the chart instantly.

Testing with a Sample Project

To see the plugin in action, create a small Fortran project with two modules:

! src/string_utils.f90
module string_utils
    implicit none
    ! ... module code ...
end module

! src/math_utils.f90
module math_utils
    use string_utils
    implicit none
    ! ... module code ...
end module

Run the plugin with:

fpm run

This generates chart.html, which you can open to see a chart showing math_utils --> string_utils.

Common Issues and Solutions

  • Error: “Model error”: Ensure fpm.toml includes the fpm dependency and your Fortran files are valid.
  • Chart not rendering: Check that your browser supports JavaScript and the Mermaid CDN is accessible.
  • Multiple modules per file: The plugin links dependencies to the first module in a file. If this is an issue, split modules into separate files.

Conclusion and Next Steps

You’ve built a powerful fpm plugin to visualize Fortran module dependencies! This tool makes it easier to understand project structures, saving time during refactoring or debugging. Here are some ways to extend it:

  • Try Other Formats: use force-graph for interactive 3D charts.
  • Automate with CI/CD: Add chart generation to your CI pipeline for up-to-date documentation.
  • Enhance the Chart: Add colors or labels to the Mermaid chart for clarity.
  • Contribute: Check the plugin on GitHub.

I hope this tutorial inspires you to explore fpm’s API further. If you create a cool chart, share it with the community—we’d love to see it!

15 Likes

Great project! There was also a request from @rouson to add this to fpm. See here: Option to output module dependency tree · Issue #687 · fortran-lang/fpm · GitHub

I have made a similar prototype in a private branch. I used it to plot the fpm module dependencies:

I think I used the neato or fdp layout engine to draw this. One recognizes leaf modules, which typically fulfill concrete tasks. And then you have this inner highly inter-connected part of all the abstract model stuff. There are probably a couple fake dependencies in there too. A hierarchical view can be found here: Option to output module dependency tree · Issue #687 · fortran-lang/fpm · GitHub

I’ll surely take a closer look at fpm-modules to see if there is anything I could borrow for fpm-deps (discussed here previously - Dependency graph for FORD). Vice-versa, I think having a Graphviz option in fpm-modules would be nice. You can find an example here: fpm-deps/app/fpm-deps/fpm-deps.f90 at 2fa1ee77ae78c4b0d4b67d92da4b9a62ad6557ab · ivan-pi/fpm-deps · GitHub :slight_smile:

(ps: if you install fpm-modules in a place on the PATH, it can be invoked as fpm modules due to the fpm wrapper machinery)

Edit: another very useful feature would be to allow drawing these graphs from a list of source files (for non fpm-based projects).

2 Likes

Hi @ivanpribec, Thanks!
fpm-deps has been a source of inspiration for that project.

Considering that I already have a mermaid export, outputting a dot file should be a no-brainer.
mermaid

Among the things you may be interested in, you can have a look at the pan and zoom options.

  <div class="diagram-container" id="diagram-container">
      <pre class="mermaid">
          <!-- mermaid graph -->
      </pre>
  </div>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script src="https://unpkg.com/@panzoom/panzoom@4.6.0/dist/panzoom.min.js"></script>
<script>mermaid.initialize({startOnLoad:false, maxEdges: 4000});
   mermaid.run({
   querySelector: ".mermaid",
   postRenderCallback: (id) => {
       const container = document.getElementById("diagram-container");
       const svgElement = container.querySelector("svg");
       // Initialize Panzoom
       const panzoomInstance = Panzoom(svgElement, {
           maxScale: 5,
           minScale: 0.5,
           step: 0.1,
       });
       // Add mouse wheel zoom
       container.addEventListener("wheel", (event) => {
           panzoomInstance.zoomWithWheel(event);
       });
   }
});
</script>

That would definitely be useful, but also require writing an entire source file parser. I could probably reuse some lower level functions from fpm though.

Great!! Yes we developed the fpm build --dump filename{.json, .toml} exactly for this, that information about the project’s dependencies could be passed to user interfaces (VS Code) or other build systems.

I haven’t looked thoroughly at your source, but note that as of fpm v0.12.0, the package dependency representation is also now available as a (sorted) list of package dependencies, i.e.

[deps.dependencies]
[deps.dependencies.fpm]
name = "fpm"
path = "."
version = "0.12.0"
proj-dir = "./."
done = true
update = false
cached = false
package-dep = [ "toml-f", "M_CLI2", "fortran-regex", "jonquil", "fortran-shlex" ]

that is used to create the library linking commands and will be used for the fpm tree visualization, hopefully soon!

PS just curious: did you have any AI help out only drafting the documentation, or did AI also happen to contribute to the code development? I’m curious to understand if AI-assisted tools would enable the whole fpm source code to be “seen” by the AI context window.

That’s great, I was really hoping for this. My initial idea was to use daglib and do a toposort, but if it comes out of the box it’s even better.

For the coding part, I am quite old-school so I did it on my own. It would be an interesting exercise to estimate what any AI knows about the fpm API. The training dataset is rather small at the moment so I do not expect much. For the tutorial, I wrote the first draft and use grok to review and rephrase some of it. The code snippets and examples are mine though.

1 Like

Great plug-in! I have just tested it (on itself). Maybe you should now add in the README.md the fpm install command, which allows to use it as a plug-in by typing fpm modules in any directory.

1 Like

Thanks.
Cool tip. I just added the instructions to install it.

1 Like

Just out of interest: how do “package dependencies”, module use association and submodule use association play together and can these levels be represented on these graphs in , say, different colours or line types?

At the moment, submodule constructs are not handled separately, but that’s a good idea. I’ll look into it