Case Study 2: Shipping It

"Code that only builds on your machine is a hobby. Code that anyone can build is software."

Executive Summary

Where the first case study read an existing stack, this one asks you to build one. You have a working heat solver — modules, a derived type, a namelist config, error handling — but it lives as a pile of .f90 files and a build command you keep in your head. In this study you turn it into a real, shareable fpm package: a project with a manifest, an external dependency, an automated test that guards its correctness, and generated documentation. This is the Chapter 16 Project Checkpoint carried to its conclusion, and it is how Part III ends — with your solver ready for the numerical, performance, and parallel work ahead, packaged the way real scientific software is packaged.

Skills applied: creating and structuring an fpm project (§16.3); writing an fpm.toml with metadata and a dependency (§16.3); adding a test (§16.5, forward to Chapter 37); documenting with FORD (§16.5); the reproducibility discipline of pinning dependencies (§16.3); building on modules (Chapter 8) and error handling (Chapter 13).

Background

Your solver, as of the end of Part II, is a set of files you compile roughly like this:

$ gfortran -c kinds.f90 && gfortran -c heat_solver.f90 && gfortran -c heat_io.f90 \
      && gfortran main.f90 kinds.o heat_solver.o heat_io.o -o heat

It works — but only for you, only in this directory, and only as long as you remember the file order. Nobody else can clone it and build it in one command; there is no test that fails when someone breaks the physics; there is no documentation but the source. We fix all three by packaging it with fpm. The target is a project a collaborator can git clone and build with a single fpm build, that self-tests with fpm test, and whose API is documented by ford.

Phase 1 — Scaffold and Populate

Create the skeleton and drop your existing code into fpm's fixed layout:

$ fpm new heat-solver
$ # move the library modules into src/ :
$ #   kinds.f90, heat_solver.f90, heat_io.f90, heat_types.f90
$ # move the driver `program heat` into app/main.f90
$ fpm build

The layout is not decoration; it is the contract that lets fpm do your bookkeeping:

heat-solver/
├── fpm.toml
├── README.md
├── app/
│   └── main.f90          program heat (the driver)  -> the executable
├── src/
│   ├── kinds.f90         module kinds  (owns dp)
│   ├── heat_types.f90    module heat_types  (the field_t derived type)
│   ├── heat_solver.f90   module heat_solver  (the update)
│   └── heat_io.f90       module heat_io  (namelist read, field write)
└── test/
    └── test_step.f90     a correctness check (Phase 3)

You did not write a Makefile, and you did not tell fpm that heat_solver depends on kinds. fpm reads the use statements, builds the dependency graph, and compiles in order — the very ordering you tracked by hand in Chapter 8. That automation is the first thing packaging buys you.

Phase 2 — The Manifest and a Dependency

The manifest gives the project an identity and pulls in stdlib, so you can replace hand-rolled utilities (a mean, a standard deviation, a text-table load) with reviewed library code:

name = "heat-solver"
version = "0.1.0"
license = "MIT"
author = "Your Name"
maintainer = "you@example.com"
copyright = "Copyright 2026, Your Name"

[build]
auto-executables = true
auto-tests = true

[dependencies]
# Pin a tag for reproducibility — a moving 'latest' can change your build overnight.
stdlib = { git = "https://github.com/fortran-lang/stdlib", tag = "v0.7.0" }

The tag is the reproducibility discipline that Chapter 37 will make non-negotiable: an unpinned dependency means "whatever that repository happens to contain today," which is poison for a result you intend to publish. (The repository URL is canonical; the exact tag string is the kind of Tier-2 detail to confirm against stdlib's current releases rather than copy from memory — including from this book.)

Phase 3 — A Test That Guards the Physics

Here is the payoff that matters most for scientific code. Add a test that pins a known-correct result, so that when you optimize the stencil in Part VII or parallelize it in Part VIII, a single fpm test tells you instantly whether you changed the answer. On a 3×3 plate with a hot top edge (100) and cold sides (0), one averaging step must set the interior cell to exactly $\tfrac{1}{4}(100+0+0+0) = 25$. A test asserts precisely that:

! test/test_step.f90 — a regression check standing in for a pFUnit/test-drive test.
!   (fpm test compiles and runs everything under test/)
program test_step
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp) :: u(3,3)
  real(dp), parameter :: expected = 25.0_dp, tol = 1.0e-12_dp

  u = 0.0_dp
  u(1,:) = 100.0_dp          ! hot top edge
  call jacobi_step(u)

  if (abs(u(2,2) - expected) < tol) then
    print '(a)', 'PASS: one relaxation step gives u(2,2) = 25.0'
  else
    print '(a, f8.4)', 'FAIL: u(2,2) = ', u(2,2)
  end if

contains

  subroutine jacobi_step(f)
    real(dp), intent(inout) :: f(:,:)
    real(dp), allocatable   :: fn(:,:)
    integer :: i, j
    fn = f
    do j = 2, size(f,2) - 1
      do i = 2, size(f,1) - 1
        fn(i,j) = 0.25_dp * ( f(i-1,j) + f(i+1,j) + f(i,j-1) + f(i,j+1) )
      end do
    end do
    f = fn
  end subroutine jacobi_step
end program test_step

! Hand computation: u(2,2) = 0.25*(100+0+0+0) = 25.0; |25.0 - 25.0| < tol -> PASS.
!
! Expected output:
! PASS: one relaxation step gives u(2,2) = 25.0

A real project would write this as a pFUnit or test-drive test with an @assertEqual, and pFUnit could even run it under MPI once the solver is parallel — but the idea is identical: encode a value you know to be correct, and let the build tell you when it changes. In production the test would call the heat_solver module from src/; it is inlined here so the file compiles on its own under the book's no-execution policy.

Why a tolerance, not equality? We compare with abs(... ) < tol rather than == because floating-point results are rarely bit-exact after real arithmetic — a habit Chapter 20 will justify in full. Here the arithmetic is exact, but the tolerance is the right reflex.

Phase 4 — Documentation with FORD

Annotate the public API with FORD doc comments — ordinary comments the compiler ignores and FORD turns into a website:

!> Advance the temperature field by one relaxation step.
subroutine step(field, alpha, dt)
  type(field_t), intent(inout) :: field   !! the temperature field to update
  real(dp),      intent(in)    :: alpha    !! thermal diffusivity
  real(dp),      intent(in)    :: dt       !! time-step size
  ! ...
end subroutine step

Then a tiny FORD project file configures the doc build:

---
project: heat-solver
summary: A 2D heat-equation solver, built across the book.
src_dir: ./src
output_dir: ./doc
---
Documentation for the heat solver. Generated by FORD from the source and its doc comments.

Run ford project.md and you have a browsable manual of every module, type, and procedure. We wire this into continuous integration — docs and tests running automatically on every change — in Chapter 37.

Phase 5 — Ship It

Two commands now do what your fragile shell one-liner did, plus everything it did not:

$ fpm build     # fetch & build stdlib, then the library, driver, and tests, in order
$ fpm test      # run test/test_step.f90 -> PASS

Add a README.md (what it solves, how to build, how to run), commit the pinned fpm.toml, and your solver is genuinely software: a collaborator clones the repository, runs fpm build, and it works — dependency and all — on their machine, not just yours. That is the line the epigraph draws between a hobby and software, and crossing it is what Part III has been quietly building toward.

Discussion Questions

  1. Packaging added a manifest, a dependency, a test, and docs. Rank these four by how much risk each removes from a scientific project, and defend your top choice.
  2. The test asserts one hand-known value (25.0). What is the strongest argument that a single such test is worth writing before any optimization work begins? (Connect to the regression-test idea previewed in Chapter 37.)
  3. Why pin the stdlib dependency to a tag rather than track the latest commit? Give a concrete scenario where not pinning silently corrupts a published result.

Your Turn: Extensions

  • Option A. Actually do it: run fpm new, drop in the smallest version of the solver you have (even just the Jacobi step), add the test/ program above, and get fpm test to print PASS. Predict the output first.
  • Option B. Add a second test that checks a different known value — e.g., a plate with all edges at 50 should leave the interior at 50 after one step (a fixed point). Compute the expected value by hand, write the assertion, and confirm it passes.
  • Option C. Replace one hand-rolled utility in the solver (say, a mean of the field for a sanity print) with the equivalent stdlib_stats call, add the dependency, and rebuild with fpm. Note what you deleted.

Key Takeaways

  • Packaging with fpm turns a pile of files and a remembered build command into software anyone can clone and build in one command — fpm reads your use graph and compiles the modules in order for you.
  • The fpm.toml manifest gives the project identity and dependencies; pin dependencies to a tag for reproducible builds.
  • A single test that pins a hand-known result (fpm test → PASS) is the cheapest insurance in scientific computing: it catches the day you accidentally change the numbers while "just optimizing."
  • FORD comments are free — they are ordinary comments — and they turn into a real manual. Tests, docs, and CI are developed fully in Chapter 37; this study is your first working version of all three.