[flang-commits] [flang] [llvm] [Flang] Add mock flang driver (PR #203481)

Michael Kruse via flang-commits flang-commits at lists.llvm.org
Fri Jun 12 01:21:04 PDT 2026


https://github.com/Meinersbur created https://github.com/llvm/llvm-project/pull/203481

Add a mock driver to pass CMake's compiler introspection for CMake_Fortran_COMPILER. It's purpose is to not having to build the full flang compiler for the runtimes-configure phase in bootstrapping-runtimes builds, but only when the Fortran compiler is actually needed (e.g. flang-rt-mod, libomp-mod).

To detect LLVMFlang, CMake executes
```
   ${CMAKE_Fortran_COMPILER} -v -c -target=...  CMakeFortranCompilerId.F
```
and expects a new file to appear in the working directory. This would usually be an object file (e.g. ELF), but it doesn't matter for CMake as it parses it for the preprocessor result of CMakeFortranCompilerId.F which would appear as string literals in the binary file (CMake cannot execute the file because it might be cross-compiling). Just passing it through the preprocessor yields the same result. The most relevant preprocessor definition is __flang__ which leads to CMAKE_Fortran_COMPILER_ID="LLVMFlang".

This is more robust than #198205:
1. It does not rely on `CMake_Fortran_COMPILER_FORCED` which the CMake manual documents as [Do not use](https://cmake.org/cmake/help/latest/module/CMakeForceCompiler.html), nor the undocumented `CMAKE_Fortran_COMPILER_ID_RUN`.
2. Incremental builds are more robust, there should be no risk of the make program (ninja/make) mistakenly interpreting the empty/mock executable as the up-to-date build artifact. Creating the shim/empty flang executable is done in the outer bootstrapping built (rather than in the inner runtimes build); having control over both rules that create bin/flang allows this.

Also fixes the problem that check-flang-rt/check-openmp [do not trigger (re-)building flang](https://github.com/llvm/llvm-project/pull/198205#pullrequestreview-4412291879).


Design considerations:

 * This relies on the way how CMake currently introspects the compiler. However, I expect it to stay "look into the produced files" in the foreseable future and will also not rely on runtime string concatenation because debug builds are not going to optimized them into a single string constant. That is, there are reasons CMake can only rely on the preprocessor.

 * There is one situation where incremental builds to not completely work: deleting bin/flang manually and then building runtimes-configure will not recreate either bin/flang. No impact if runtimes-configure was actually run before because in that case CMake has already cached the CMAKE_Fortran_COMPILER inspection result.
 
 * ninja does not allow two rules to create the same file (one to create bin/flang only if it is missing and another that replaces it with the full flang), one of the build artifacts must be undeclated. Thast could be the full flang that would only be created with a separate `flang-eager` target, but that would have required more changes.
 
 * bin/flang could be copied over with a PRE_BUILD rules before running runtimes-configure and all such forwarded targets. But a new target with that PRE_BUILD rule that runtimes-configure could depend on would always run, robbing us if that satisfying "ninja: no work to do" experience (and run the nested cmake configure every time). Adding it to all the targets that may call the nested cmake would require more changes


>From a5dba0f3e538aad1c592e4d4ee578d3bb69fe320 Mon Sep 17 00:00:00 2001
From: Michael Kruse <llvm-project at meinersbur.de>
Date: Wed, 10 Jun 2026 19:15:49 +0200
Subject: [PATCH] Add mock flang driver

check-flang-rt and check-openmp depend on flang
---
 flang/test/Driver/fakeflang.F                 | 28 ++++++
 flang/tools/CMakeLists.txt                    |  3 +
 flang/tools/fakeflang/CMakeLists.txt          | 26 +++++
 .../tools/fakeflang/ensure_flang_exists.cmake | 11 +++
 flang/tools/fakeflang/fakeflang.cpp           | 97 +++++++++++++++++++
 flang/tools/flang-driver/CMakeLists.txt       | 18 ++++
 .../modules/LLVMExternalProjectUtils.cmake    |  5 +-
 llvm/runtimes/CMakeLists.txt                  | 27 +++---
 runtimes/cmake/config-Fortran.cmake           | 39 ++------
 9 files changed, 208 insertions(+), 46 deletions(-)
 create mode 100644 flang/test/Driver/fakeflang.F
 create mode 100644 flang/tools/fakeflang/CMakeLists.txt
 create mode 100644 flang/tools/fakeflang/ensure_flang_exists.cmake
 create mode 100644 flang/tools/fakeflang/fakeflang.cpp

diff --git a/flang/test/Driver/fakeflang.F b/flang/test/Driver/fakeflang.F
new file mode 100644
index 0000000000000..bd4ed88409bd7
--- /dev/null
+++ b/flang/test/Driver/fakeflang.F
@@ -0,0 +1,28 @@
+! This is how CMake probes the compiler for CMAKE_Fortran_COMPILER_ID etc.
+
+! RUN: rm -rf %t
+! RUN: mkdir -p %t
+! RUN: cd %t
+
+! RUN: fakeflang -v -c --target=x86_64-unknown-linux-gnu %s
+! RUN: FileCheck %s --input-file=a.out --check-prefixes=CHECK,GNU
+! RUN: rm a.out
+
+! RUN: fakeflang -v -c --target=x86_64-pc-windows-msvc %s
+! RUN: FileCheck %s --input-file=a.out --check-prefixes=CHECK,MSVC --match-full-lines
+! RUN: rm a.out
+
+
+! CHECK: CMAKE_Fortran_COMPILER_ID=1
+CMAKE_Fortran_COMPILER_ID=__flang__
+
+! CHECK: CMAKE_Fortran_COMPILER_VERSION={{[0-9]+}} . {{[0-9]+}} . {{[0-9]+}}
+CMAKE_Fortran_COMPILER_VERSION=__flang_major__.__flang_minor__.__flang_patchlevel__
+
+! GNU: CMAKE_Fortran_SIMULATE_ID=_MSC_VER
+! MSVC: CMAKE_Fortran_SIMULATE_ID={{[0-9]+}}
+CMAKE_Fortran_SIMULATE_ID=_MSC_VER
+
+! GNU: CMAKE_Fortran_PLATFORM_ID=_WIN32
+! MSVC: CMAKE_Fortran_PLATFORM_ID=1
+CMAKE_Fortran_PLATFORM_ID=_WIN32
diff --git a/flang/tools/CMakeLists.txt b/flang/tools/CMakeLists.txt
index 1b297af74cae7..d76d7d7211073 100644
--- a/flang/tools/CMakeLists.txt
+++ b/flang/tools/CMakeLists.txt
@@ -8,6 +8,9 @@
 
 add_subdirectory(bbc)
 add_subdirectory(flang-driver)
+if (NOT FLANG_STANDALONE_BUILD)
+  add_subdirectory(fakeflang)
+endif ()
 add_subdirectory(tco)
 add_subdirectory(f18-parse-demo)
 add_subdirectory(fir-opt)
diff --git a/flang/tools/fakeflang/CMakeLists.txt b/flang/tools/fakeflang/CMakeLists.txt
new file mode 100644
index 0000000000000..8e67a40714c7a
--- /dev/null
+++ b/flang/tools/fakeflang/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(LLVM_LINK_COMPONENTS
+  Support
+)
+set(FLANG_BUILD_TOOLS OFF) # Do not install
+add_flang_tool(fakeflang
+  fakeflang.cpp
+)
+target_compile_definitions(fakeflang PRIVATE "CMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}")
+
+# Using $<TARGET_FILE:file> in an add_custom_command would automatically
+# add a dependency.
+set(_flang_exe "${LLVM_RUNTIME_OUTPUT_INTDIR}/flang${CMAKE_EXECUTABLE_SUFFIX}")
+
+# Creates ${_flang_exe} using fakeflang if it does not exist yet.
+# To be extra careful, go through a script that will not overwrite
+# a previously compiled full flang
+add_custom_command(POST_BUILD
+  TARGET fakeflang
+  COMMAND "${CMAKE_COMMAND}" "-DINPUT_FILE=$<TARGET_FILE:fakeflang>" "-DOUTPUT_FILE=${_flang_exe}" "-P" "${CMAKE_CURRENT_SOURCE_DIR}/ensure_flang_exists.cmake"
+)
+
+# Phony target that ensures bin/flang exists; if the full flang has not been
+# compiled yet, use a mock flang driver only for passing CMake's
+# CMake_Fortran_COMPILER tests.
+add_custom_target(flang-lazy)
+add_dependencies(flang-lazy fakeflang)
diff --git a/flang/tools/fakeflang/ensure_flang_exists.cmake b/flang/tools/fakeflang/ensure_flang_exists.cmake
new file mode 100644
index 0000000000000..4279649cd2885
--- /dev/null
+++ b/flang/tools/fakeflang/ensure_flang_exists.cmake
@@ -0,0 +1,11 @@
+# Copy INPUT_FILE to OUTPUT_FILE, but only
+# if OUTPUT_FILE does not already exist
+
+if (NOT EXISTS "${OUTPUT_FILE}")
+  cmake_path(GET OUTPUT_FILE PARENT_PATH OUTPUT_DIR)
+  file(MAKE_DIRECTORY "${OUTPUT_DIR}")
+
+  # This could also be the symlink but its small size is not worth the effort
+  # handling platform differences.
+  file(COPY_FILE "${INPUT_FILE}" "${OUTPUT_FILE}")
+endif ()
diff --git a/flang/tools/fakeflang/fakeflang.cpp b/flang/tools/fakeflang/fakeflang.cpp
new file mode 100644
index 0000000000000..f3b05957e3c4d
--- /dev/null
+++ b/flang/tools/fakeflang/fakeflang.cpp
@@ -0,0 +1,97 @@
+//===-- fakeflang.cpp - Mock Flang Driver ---------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+/// Mock driver to pass CMake's compiler introspection for
+/// CMake_Fortran_COMPILER. It's purpose is to not having to build the full
+/// flang compiler for the runtimes-configure phase in bootstrapping-runtimes
+/// builds, but only when the Fortran compiler is actually needed (e.g.
+/// flang-rt-mod, libomp-mod).
+///
+/// To detect LLVMFlang, CMake executes
+///
+///   ${CMAKE_Fortran_COMPILER} -v -c -target=... CMakeFortranCompilerId.F
+///
+/// and expects a new file to appear in the working directory. This would
+/// usually be an object file (e.g. ELF), but it doesn't matter for CMake as it
+/// parses it for the preprocessor result of CMakeFortranCompilerId.F which
+/// would appear as string literals in the binary file (CMake cannot execute the
+/// file because it might be cross-compiling). Just passing it through the
+/// preprocessor yields the same result.
+///
+/// The most relevant preprocessor definition is __flang__ which leads to
+/// CMAKE_Fortran_COMPILER_ID="LLVMFlang".
+//
+//===----------------------------------------------------------------------===//
+
+#include "flang/Version.inc"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/Program.h"
+#include "llvm/Support/WithColor.h"
+#include <cstdint>
+#include <string>
+
+#define STRINGIFY(X) #X
+#define STRINGIFY_EXPANDED(X) STRINGIFY(X)
+
+static std::string getExecutablePath(const char *argv0) {
+  void *anchor = (void *)(intptr_t)getExecutablePath;
+  return llvm::sys::fs::getMainExecutable(argv0, anchor);
+}
+
+[[noreturn]] static void fail(llvm::Twine Error) {
+  llvm::WithColor::error(llvm::errs(), "fakeflang") << Error << "\n";
+  exit(EXIT_FAILURE);
+}
+
+int main(int argc, const char **argv) {
+  llvm::InitLLVM X(argc, argv);
+
+  llvm::WithColor::remark(llvm::errs())
+      << "This is a mock flang compiler; Use '" STRINGIFY_EXPANDED(
+             CMAKE_MAKE_PROGRAM) " flang' to replace it with the real "
+                                 "compiler\n";
+
+  std::string SelfExe = getExecutablePath(argv[0]);
+  llvm::SmallString<256> ClangExe{llvm::sys::path::parent_path(SelfExe)};
+  llvm::sys::path::append(ClangExe, "clang");
+
+  llvm::ArrayRef<const char *> AllArgs(argv, static_cast<size_t>(argc));
+  bool hasDashO = AllArgs.size() > 1 &&
+      llvm::any_of(AllArgs.drop_front(), [](const char *Arg) {
+        return llvm::StringRef(Arg).starts_with("-o");
+      });
+
+  // Assemble invocation of the preprocessor
+  // `-E`: Invoke the preprocessor
+  // `-P`: No #line directives
+  // `-D..`: Preprocessor definitions that CMake probes
+  // `-x c`: Usually Clang would forward Fortran files to gfortran; Interpret as
+  //         C for clang to preprocess the files itself
+  // `-o`: -E by default emits to stdout, but CMake expects a new file to appear
+  //       in the cwd
+  llvm::SmallVector<llvm::StringRef, 32> Args;
+  Args.append({ClangExe, "-E", "-P", "-D__flang__=1",
+      "-D__flang_major__=" FLANG_VERSION_MAJOR_STRING,
+      "-D__flang_minor__=" FLANG_VERSION_MINOR_STRING,
+      "-D__flang_patchlevel__=" FLANG_VERSION_PATCHLEVEL_STRING, "-x", "c"});
+  for (int I = 1; I < argc; ++I)
+    Args.push_back(argv[I]);
+  if (!hasDashO)
+    Args.append({"-o", "a.out"});
+
+  std::string ErrMsg;
+  int RC = llvm::sys::ExecuteAndWait(ClangExe, Args, /*Env=*/std::nullopt,
+      /*Redirects=*/{}, /*SecondsToWait=*/0, /*MemoryLimit=*/0, &ErrMsg);
+  if (RC < 0)
+    fail(ErrMsg);
+  return RC;
+}
diff --git a/flang/tools/flang-driver/CMakeLists.txt b/flang/tools/flang-driver/CMakeLists.txt
index 4dfc0d40cd55d..d11e7f58a32b8 100644
--- a/flang/tools/flang-driver/CMakeLists.txt
+++ b/flang/tools/flang-driver/CMakeLists.txt
@@ -49,3 +49,21 @@ install(TARGETS flang DESTINATION "${CMAKE_INSTALL_BINDIR}")
 # Keep "flang-new" as a symlink for backwards compatiblity. Remove once "flang"
 # is a widely adopted name.
 add_flang_symlink(flang-new flang)
+
+
+if (NOT FLANG_STANDALONE_BUILD)
+  # This ensures that flang is always built after fakeflang. If fakeflang is
+  # built first, the bin/flang may have a newer timestamp than all of flang's
+  # source files and the make program might consider it 'up-to-date'.
+  # fakedepend.cpp which depends on fakeflang will have to be created after
+  # fakeflang and therefore have a newer timestamp than bin/flang, triggering a
+  # rebuild and therefore overwrite bin/flang with the real version.
+  add_custom_command(
+    OUTPUT fakedepend.cpp
+    DEPENDS flang-lazy
+    COMMAND "${CMAKE_COMMAND}" -E touch fakedepend.cpp
+  )
+  target_sources(flang PRIVATE
+    fakedepend.cpp
+  )
+endif ()
diff --git a/llvm/cmake/modules/LLVMExternalProjectUtils.cmake b/llvm/cmake/modules/LLVMExternalProjectUtils.cmake
index 47b12dc9a3d8c..ee270d70a778d 100644
--- a/llvm/cmake/modules/LLVMExternalProjectUtils.cmake
+++ b/llvm/cmake/modules/LLVMExternalProjectUtils.cmake
@@ -100,6 +100,9 @@ function(llvm_ExternalProject_Add name source_dir)
 
   if(NOT ARG_TOOLCHAIN_TOOLS)
     set(ARG_TOOLCHAIN_TOOLS clang)
+    if (ARG_ENABLE_FORTRAN)
+      list(APPEND ARG_TOOLCHAIN_TOOLS flang-lazy)
+    endif ()
     # AIX 64-bit XCOFF and big AR format is not yet supported in some of these tools.
     if(NOT _cmake_system_name STREQUAL "AIX")
       list(APPEND ARG_TOOLCHAIN_TOOLS lld llvm-ar llvm-ranlib llvm-nm llvm-objdump)
@@ -150,7 +153,7 @@ function(llvm_ExternalProject_Add name source_dir)
     set(CLANG_IN_TOOLCHAIN On)
   endif()
 
-  if(ARG_ENABLE_FORTRAN AND TARGET flang)
+  if(flang-lazy IN_LIST TOOLCHAIN_TOOLS)
     set(FLANG_IN_TOOLCHAIN On)
   endif()
 
diff --git a/llvm/runtimes/CMakeLists.txt b/llvm/runtimes/CMakeLists.txt
index bbc91a709c553..6d81b26d2d416 100644
--- a/llvm/runtimes/CMakeLists.txt
+++ b/llvm/runtimes/CMakeLists.txt
@@ -120,18 +120,20 @@ endmacro()
 # flang dependency to the main build target and any fortran module file builds,
 # like 'install-libomp-mod' or `install-flang-rt-mod'.
 function(add_flang_mod_deps build_target)
-  if(TARGET flang)
-    if(TARGET ${build_target})
-      add_dependencies(${build_target} flang)
-    endif()
-    foreach(tgt IN LISTS ARGN)
-      if(tgt MATCHES "-mod($|-)")
-        if(TARGET ${tgt})
-          add_dependencies(${tgt} flang)
-        endif()
-      endif()
-    endforeach()
+  if(NOT TARGET flang)
+    return ()
+  endif ()
+
+  if(TARGET ${build_target})
+    add_dependencies(${build_target} flang)
   endif()
+  foreach(tgt IN LISTS ARGN)
+    if(tgt MATCHES "-mod($|-)" OR tgt MATCHES "check-(flang-rt|openmp)($|-)")
+      if(TARGET ${tgt})
+        add_dependencies(${tgt} flang)
+      endif()
+    endif()
+  endforeach()
 endfunction()
 
 function(builtin_default_target compiler_rt_path)
@@ -364,7 +366,7 @@ function(runtime_default_target)
                            FOLDER "Runtimes"
                            ${EXTRA_ARGS} ${ARG_EXTRA_ARGS})
 
-  add_flang_mod_deps(runtimes-build ${extra_targets})
+  add_flang_mod_deps(runtimes-build ${extra_targets} ${test_targets})
 endfunction()
 
 # runtime_register_target(name)
@@ -544,6 +546,7 @@ if(build_runtimes)
   if(LLVM_INCLUDE_TESTS)
     foreach(dep FileCheck
                 clang
+                flang-lazy
                 count
                 lld
                 lli
diff --git a/runtimes/cmake/config-Fortran.cmake b/runtimes/cmake/config-Fortran.cmake
index e9ac8e7b7eac3..74fd15021f0a0 100644
--- a/runtimes/cmake/config-Fortran.cmake
+++ b/runtimes/cmake/config-Fortran.cmake
@@ -88,27 +88,15 @@ if (CMAKE_Fortran_COMPILER)
   # cannot use CMAKE_Fortran_COMPILER_ID.
   cmake_path(GET CMAKE_Fortran_COMPILER STEM _Fortran_COMPILER_STEM)
   if (_Fortran_COMPILER_STEM STREQUAL "flang-new" OR _Fortran_COMPILER_STEM STREQUAL "flang")
-    # Force the compiler ID so CMake does not try to run the compiler for
-    # identification. In a bootstrapping build the Flang binary may not be
-    # built yet at configure time (only CMAKE_Fortran_COMPILER_WORKS is set).
-    # FIXME: flang has no equivalent to clang-cl, so
-    # CMAKE_Fortran_SIMULATE_ID=GNU should be the only correct value. CMake may
-    # imply that supports different toolchains for each language but in
-    # practice is doesn't. In particular, the last enabled
-    # language will overwrite global variables such as CMAKE_LINK_LIBRARY_FLAG
-    # depending on CMAKE_<lang>_SIMULATE_ID, i.e. they cannot be different.
-    set(CMAKE_Fortran_COMPILER_ID "LLVMFlang")
-    set(CMAKE_Fortran_COMPILER_ID_RUN TRUE)
-    set(CMAKE_Fortran_COMPILER_FORCED TRUE)
-    set(CMAKE_Fortran_COMPILER_VERSION "${LLVM_VERSION_MAJOR}.${LLVM_VERSION_MINOR}.${LLVM_VERSION_PATCH}")
-    set(CMAKE_Fortran_SIMULATE_ID "${CMAKE_CXX_SIMULATE_ID}")
-    set(CMAKE_Fortran_SIMULATE_VERSION "${CMAKE_CXX_SIMULATE_VERSION}")
-    set(CMAKE_Fortran_COMPILER_SUPPORTS_F90 1)
-    set(CMAKE_Fortran_PLATFORM_ID "${CMAKE_CXX_PLATFORM_ID}")
-
     # CMake 3.24 is the first version of CMake that directly recognizes Flang.
     # LLVM's requirement is only CMake 3.20, teach CMake 3.20-3.23 how to use Flang, if used.
     if (CMAKE_VERSION VERSION_LESS "3.24")
+      include(CMakeForceCompiler)
+      CMAKE_FORCE_Fortran_COMPILER("${CMAKE_Fortran_COMPILER}" "LLVMFlang")
+
+      set(CMAKE_Fortran_COMPILER_ID "LLVMFlang")
+      set(CMAKE_Fortran_COMPILER_VERSION "${LLVM_VERSION_MAJOR}.${LLVM_VERSION_MINOR}")
+
       set(CMAKE_Fortran_SUBMODULE_SEP "-")
       set(CMAKE_Fortran_SUBMODULE_EXT ".mod")
 
@@ -159,21 +147,6 @@ else ()
   return ()
 endif ()
 
-# In a bootstrapping build the Fortran compiler may not have been built yet.
-# Create a placeholder so CMake's enable_language() existence check passes.
-# The build-order dependency in add_flang_mod_deps ensures the real binary is
-# built before anything tries to invoke this placeholder.
-if (CMAKE_Fortran_COMPILER_FORCED AND NOT EXISTS "${CMAKE_Fortran_COMPILER}")
-  get_filename_component(_compiler_dir "${CMAKE_Fortran_COMPILER}" DIRECTORY)
-  file(MAKE_DIRECTORY "${_compiler_dir}")
-  file(WRITE "${CMAKE_Fortran_COMPILER}" "stub")
-  # Ninja uses file mtimes to decide whether build outputs are up-to-date.
-  # If this placeholder's mtime is recent it may match what is recorded in
-  # .ninja_log, causing ninja to skip building the real compiler binary.
-  # Set it so that any subsequent real build always has a newer mtime.
-  execute_process(COMMAND touch -t 197001020000 "${CMAKE_Fortran_COMPILER}"
-                  ERROR_QUIET)
-endif ()
 
 include(CheckLanguage)
 check_language(Fortran)



More information about the flang-commits mailing list