From 76e6340bc1650c0c4d17e43d0122f9b5a2c57880 Mon Sep 17 00:00:00 2001 From: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:43:28 +0000 Subject: [PATCH] fix: link Fortran executables with --coverage under code coverage Bazel enables the "coverage" feature on every action when code coverage is being collected, but rules_cc's legacy gcc_coverage_map_format feature only attaches --coverage to the C/C++ link actions. A fortran_binary that statically links coverage-instrumented C/C++ objects therefore failed to link: libxwin.a(Xwin2.pic.o): undefined reference to `__gcov_init' libxwin.a(Xwin2.pic.o): undefined reference to `__gcov_exit' libxwin.a(Xwin2.pic.o): undefined reference to `__gcov_merge_add' Add a fortran_coverage feature that mirrors --coverage onto fortran-link-executable, gated on the coverage feature so non-coverage builds are unchanged. Only the link action is covered. Instrumenting fortran-compile would also require rules_fortran to declare the .gcno outputs and report them through InstrumentedFilesInfo; without that, -ftest-coverage emits undeclared outputs that Bazel discards, so Fortran sources stay uninstrumented. The coverage CI job only exercised a C++ test, which is why this went unnoticed. It now also builds tests/coverage, where a fortran_binary links an instrumented C library; that target fails to link without this change. --- .github/workflows/default.yaml | 2 +- tests/coverage/BUILD.bazel | 37 +++++++++++++++++++++++++++++++ tests/coverage/add_one.c | 26 ++++++++++++++++++++++ tests/coverage/main.f90 | 32 ++++++++++++++++++++++++++ toolchain/cc_toolchain_config.bzl | 31 ++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/coverage/BUILD.bazel create mode 100644 tests/coverage/add_one.c create mode 100644 tests/coverage/main.f90 diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index b594392..bd94072 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -108,7 +108,7 @@ jobs: - name: Test Coverage run: | ln -s .github/workflows/.bazelrc.ci .bazelrc.ci - bazel coverage --config ${{ matrix.bzlmod_config }} //examples/hello_world_cpp:hello_world_cpp_test + bazel coverage --config ${{ matrix.bzlmod_config }} //examples/hello_world_cpp:hello_world_cpp_test //tests/coverage/... bazel9: strategy: matrix: diff --git a/tests/coverage/BUILD.bazel b/tests/coverage/BUILD.bazel new file mode 100644 index 0000000..4a2239b --- /dev/null +++ b/tests/coverage/BUILD.bazel @@ -0,0 +1,37 @@ +# Copyright (c) Thulio Ferraz Assis 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("//rules_cc:defs.bzl", "cc_library") +load("//rules_fortran:defs.bzl", "fortran_binary") + +cc_library( + name = "add_one", + srcs = ["add_one.c"], +) + +fortran_binary( + name = "fortran_links_instrumented_c", + srcs = ["main.f90"], + deps = [":add_one"], +) + +# Linking a coverage-instrumented C object into a Fortran executable only +# succeeds if the fortran_coverage feature mirrors --coverage onto the +# fortran-link-executable action. Run with --collect_code_coverage to exercise +# that; without it this is an ordinary mixed Fortran/C link. +build_test( + name = "fortran_coverage_link_test", + targets = [":fortran_links_instrumented_c"], +) diff --git a/tests/coverage/add_one.c b/tests/coverage/add_one.c new file mode 100644 index 0000000..614a11e --- /dev/null +++ b/tests/coverage/add_one.c @@ -0,0 +1,26 @@ +// Copyright (c) Thulio Ferraz Assis 2026 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// When Bazel collects code coverage this translation unit is compiled with +// -fprofile-arcs -ftest-coverage, so its object file gains constructors and +// destructors that reference __gcov_init, __gcov_exit and __gcov_merge_add. +// Those symbols live in libgcov, which the linker only pulls in when the link +// action receives --coverage. The branch below exists so that there is more +// than one arc to instrument. +int add_one(int value) { + if (value < 0) { + return value; + } + return value + 1; +} diff --git a/tests/coverage/main.f90 b/tests/coverage/main.f90 new file mode 100644 index 0000000..1aad961 --- /dev/null +++ b/tests/coverage/main.f90 @@ -0,0 +1,32 @@ +! Copyright (c) Thulio Ferraz Assis 2026 +! +! Licensed under the Apache License, Version 2.0 (the "License"); +! you may not use this file except in compliance with the License. +! You may obtain a copy of the License at +! +! http://www.apache.org/licenses/LICENSE-2.0 +! +! Unless required by applicable law or agreed to in writing, software +! distributed under the License is distributed on an "AS IS" BASIS, +! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +! See the License for the specific language governing permissions and +! limitations under the License. + +program fortran_links_instrumented_c + use, intrinsic :: iso_c_binding, only: c_int + implicit none + + interface + function add_one(value) bind(C, name='add_one') + import :: c_int + integer(c_int), value :: value + integer(c_int) :: add_one + end function add_one + end interface + + if (add_one(1_c_int) /= 2_c_int) then + error stop 'add_one(1) did not return 2' + end if + + write(*,'(a)') adjustl('OK') +end program fortran_links_instrumented_c diff --git a/toolchain/cc_toolchain_config.bzl b/toolchain/cc_toolchain_config.bzl index fbd5dd9..3ebae6f 100644 --- a/toolchain/cc_toolchain_config.bzl +++ b/toolchain/cc_toolchain_config.bzl @@ -220,6 +220,36 @@ def _impl(ctx): ], ) + # Bazel enables the "coverage" feature on every action whenever code coverage is being + # collected, but rules_cc's legacy gcc_coverage_map_format feature only attaches --coverage to + # the C/C++ link actions. A fortran_binary that links coverage-instrumented C/C++ objects + # therefore fails to link with undefined references to __gcov_init, __gcov_exit and + # __gcov_merge_add, so the flag has to be mirrored onto the Fortran link action. + # + # Only the link action is covered. Instrumenting fortran-compile would additionally need + # rules_fortran to declare the .gcno outputs and report them through InstrumentedFilesInfo; + # without that, -ftest-coverage would emit undeclared outputs that Bazel discards, so the + # Fortran sources themselves stay uninstrumented. + fortran_coverage_feature = feature( + name = "fortran_coverage", + enabled = True, + flag_sets = [ + flag_set( + actions = [FORTRAN_ACTION_NAMES.fortran_link_executable], + flag_groups = [ + flag_group( + flags = ["--coverage"], + ), + ], + with_features = [ + with_feature_set( + features = ["coverage"], + ), + ], + ), + ], + ) + if enable_fortran: action_configs.append(action_config( action_name = FORTRAN_ACTION_NAMES.fortran_compile, @@ -545,6 +575,7 @@ def _impl(ctx): fortran_compile_flags_feature, static_libgfortran_feature, fortran_link_flags_feature, + fortran_coverage_feature, extra_fflags_feature, ) + sanitizers_features