Changes to C++ headers not causing Swift code to recompile

Hi,

I am facing an issue where changes to definitions in C++ headers don't cause the Swift code that uses them to recompile when building with CMake using Ninja.

Should CMake/Ninja be able to detect changes to definitions in C++ headers used in Swift and recompile when necessary? What can be done to make the example below work so that changes take effect when doing incremental builds?

Example

include/module.modulemap

module Foo {
  header "Foo.hpp"
}

include/Foo.hpp

#pragma once

inline int hpp() {
  return 1; // <--
}

int cpp();

Foo.cpp

#include "Foo.hpp"

int cpp() {
  return 1; // <--
}

main.swift

import Foo

print("hpp: \(hpp()), cpp: \(cpp())") // <--

CMakeLists.txt

cmake_minimum_required(VERSION 3.29)
project(example LANGUAGES CXX Swift)

add_executable(example main.swift Foo.cpp)
target_compile_options(example
  PRIVATE
    $<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default>
)
target_include_directories(example PRIVATE include)

Output after building for the first time

./example
hpp: 1, cpp: 1

Output after changing both cpp() and foo() to return 2 instead of 1 and building again without cleanup

./example
hpp: 1, cpp: 2

Oh, this is fun.

So for C/C++, Ninja reads the dependency file emitted by clang and adds it to Ninja's internal dependency graph so that when it sees that file change, it implicitly knows which targets to rebuild. It only accepts one dependency file per target though.

The Swift driver emits one dependency file per frontend invocation, so in most cases, there are multiple dependency files for a Swift target. This isn't fed into Ninja, so Ninja doesn't know about the dependency. This isn't really something that CMake knows about either since it's the driver discovering the dependency.

Hand-writing a quick deps file and feeding it into Ninja seems to support this. Please file a bug report on the Driver asking for single dep-file output per invocation and I can wire that into CMake pretty easily once we have that. Thanks.

CC @ArtemC

3 Likes

Fun indeed... :-/

So each C++ source file / translation unit is its own Ninja target?

Like:

Target "Foo"
Foo.cpp + referenced headers -> clang -> Foo.cpp.o, Foo.cpp.o.d

Target "Bar"
Bar.cpp + referenced headers -> clang -> Bar.cpp.o, Bar.cpp.o.d

?

What does frontend invocation mean exactly? If I pass 2 Swift files in one swiftc invocation, does it count as 2 frontend invocations?

Each Swift file produces 1 object file and 1 dependency file, but as multiple Swift files are passed within a single swiftc invocation, they are treated as parts of the same Ninja target?

Like:

Target "FooBar"
= (Foo.swift + referenced headers) + (Bar.swift + referenced headers)
-> swiftc
-> Bar.swift.o, Bar.swift.o.d, Foo.swift.o, Foo.swift.o.d

?

So Ninja needs a single dependency file (*.d) that has the combined dependencies of all the Swift files included in the swiftc invocation? And there isn't an option in swiftc to generate that file yet?

Yeah

Yep, though it would be Bar.swift.o, Bar.swift.o.d, Foo.swift.o and Foo.swift.o.d.

Ninja needs one per target, which in this case is at the module-level so that swiftc can manage things like incremental builds within the sources.

Yes, exactly.

1 Like

Thanks for the clarification! I can file an issue tomorrow on the swift-driver repository if someone hasn't already done so by then.

Out of curiosity, how are projects that make use of Swift C++ interoperability with CMake handling this issue currently? I feel like this could cause some major problems during development, and I find it odd that it hasn't been resolved yet. I assume that the fix will take some time to appear in official releases of Swift and CMake, so I wonder what would be the best solution to keep the headers in check in the meantime?

Oops, typo.

:+1:

I don't know.

Yeah, I don't know when it will land. If you notice it, you can probably touch one of the Swift sources in the target importing the C/C++ header. Ninja will see that the Swift source is dirty and rebuild that target.

If you're feeling adventurous, you could scoop up the generated *.swift.o.d for the target, merge them, and feed that into Ninja. Ninja will consume that file unless you pass -d keepdepfile. The hand-crafted deps file won't stay in sync with the Swift source or the module maps, so if you add an import or modify the module map that the target is importing, you'll need to update it to reflect the dependencies.

1 Like

An issue has been filed: