Issues when building JUCE CMake project with homebrew clang on mac

I’d love to make use of C++ 20 concepts in our codebase. Since Apple clang only has very limited support for concepts, I wanted to try out a homebred-installed vanilla clang 13.

After fiddling around for approx a day long, I finally managed to build our in-house unit test project which is a JUCE command line app. But there are some problems I encountered on the way and I’d love to get some input about what’s the right approach to them and for the JUCE-related ones, if the JUCE team could do anything about them.

A few words regarding my test configuration: I’m running macOS 12 on an Intel mac, have Xcode 13 and the Xcode command line tools installed and do all my cmake stuff via CLion. The project I built uses a slightly older JUCE 6 version that should be updated to the latest release in the near future (so sorry if any issue I’m addressing here should have been fixed in the meantime). I’m compiling with C++ 20.

Part 1: Pass the CMake configure step

My first naive approach was to just pass -DCMAKE_C_COMPILER=/usr/local/opt/llvm/bin/clang -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm/bin/clang++ to the cmake command.

Pass the CMake compiler check

Turns out that this fails early with

The C compiler

    "/usr/local/opt/llvm/bin/clang"

  is not able to compile a simple test program.

  It fails with the following output:

    Change Dir: /Users/me/Projects/testProject/build/CLion/Debug/CMakeFiles/CMakeTmp
    
    Run Build Command(s):/usr/local/bin/ninja cmTC_d2d4b && [1/2] Building C object CMakeFiles/cmTC_d2d4b.dir/testCCompiler.c.o
    [2/2] Linking C executable cmTC_d2d4b
    FAILED: cmTC_d2d4b 
    : && /usr/local/opt/llvm/bin/clang -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX11.0.sdk -Wl,-search_paths_first -Wl,-headerpad_max_install_names  CMakeFiles/cmTC_d2d4b.dir/testCCompiler.c.o -o cmTC_d2d4b   && :
    ld: library not found for -lSystem
    clang-13: error: linker command failed with exit code 1 (use -v to see invocation)
    ninja: build stopped: subcommand failed.

Okay, so the home-brew clang won’t find the system libraries. After trying out a few different options, I ended up supplying the library search paths via the LDFLAGS environment variable like -L/Library/Developer/CommandLineTools/SDKs/MacOSX11.0.sdk/usr/lib -L/usr/local/opt/llvm/lib. The first search path seems to be the one needed for all the Apple system libraries, (the C compiler check passes with this one only) while the second one supplies C++ libraries. This makes the CMake compiler check pass. Note that I explicitly passed the MacOSX11.0.sdk since CMake obviously automatically choses that SDK as sysroot when attempting to compile the test files. Not sure if that’s a good choice but more on that later.

Build juceaide

After I got the compiler check passing, configuration failed during the compilation of juceaide. While compilation succeeded, I got linker errors regarding multiple ObjectiveC symbols like undefined symbols _objc_opt_class. Since that one looks like it does not link to the ObjectiveC library, I added -lobjc to LDFLAGS and voila: juceaide builds successfully!

Part 2: Actually build the project

The first error I encountered here was here:

The compiler cannot find std::result_of which has been removed in C++ 20. Looking at the code it seems that this tried to work around an Apple clang specific issue. Removing the || (JUCE_CLANG && ! JUCE_WINDOWS) condition fixes this one. Sidenote: We use std::invoke_result here and there in our codebase and see no issues using Apple clang 12. Not sure which Apple clang version caused that problem, but it might be needed to rework the condition in a way that it’s only taken for the problematic version of Apple clang. Not sure if there is any way to determine wether Apple clang is used from a define?

The rest of the code builds without issues. The next error is a linker error again, complaining that the Accelerate framework cannot be found. So it seems that the compiler also misses the framework search path. Adding -F/Library/Developer/CommandLineTools/SDKs/MacOSX11.0.sdk/System/Library/Frameworks to the LDFLAGS also solves this one and finally the build succeeds and I’m able to run my unit test executable!

Open Questions

Although the build now succeeded, I’m still not sure if the approaches chosen here are the optimal ones.

How to correctly pass the search paths?

Is supplying -L/Library/Developer/CommandLineTools/SDKs/MacOSX11.0.sdk/usr/lib -L/usr/local/opt/llvm/lib -F/Library/Developer/CommandLineTools/SDKs/MacOSX11.0.sdk/System/Library/Frameworks -lobjc via LDFLAGS really the way to go? Usually using a quite puristic CMake workflow to specify search paths etc. this seems a bit wrong. On the other hand, specifying the search paths in my CMakeLists does not get me past the CMake configuration phase. Since Apple clang does not need those argument, it feels like the Apple toolchain has some default settings somewhere. Is there any system-wide way to specify those basic settings for my homebrew-based toolchain and if so, would it be a good way to go? Or might e.g. a CMake toolchain file be a better solution?

How to link against libraries matching my deployment target?

The default deployment target for our projects is 10.9 until now, this is set via CMAKE_OSX_DEPLOYMENT_TARGET. To be honest, I don’t know in detail what this flag causes. But specifying a 11.0 SDK as sysroot and as library search path seems a bit wrong to me? However, since those libraries all seem to be dynamic libraries, I’m not sure if this choice will even affect where the final executable will look for its libraries? At least I get no linker warnings here. Indeed, looking into Library/Developer/CommandLineTools/SDKs, I only see MacOSX10.14.sdk, MacOSX10.15.sdk and MacOSX11.0.sdk. Looking into the Xcode bundle I furthermore find MacOSX12.1.sdk.

However, I get a linker warning here:
ld: warning: dylib (/usr/local/opt/llvm/lib/libunwind.dylib) was built for newer macOS version (12.0) than being linked (10.9)
Seems like the homebrew supplied libraries are built for the target operating system of the build machine. Again, this is a dynamic library, so I expect this one to reference a matching library found on the target machine the executable will run on – right? Can I safely ignore that warning? Or does the homebrew clang reference any libraries or symbols that might not even be present on the target system?

Conclusion

I’m pretty happy that I made it until here :wink: And I’m quite confident that the remaining problems and warnings can be resolved in some way. For now I need some clarification on the open questions, but maybe someone round here knows a thing or two. Does anyone else here successfully use a non-Apple clang to build for macOS?

Do you also run into this problem?

You’ll need to include the juce_gui_extra module to hit it…

No, I don’t run into that problem, but probably because my command line app test did not include juce_gui_extra, neither was it on JUCE 6.1.3. But I’ll quickly set up a test project to try that…

In the meantime, I thought about a potential fix for the WaveShaper compile error issue. What if you added JUCE_CXX20_IS_AVAILABLE (which would be handy anyway) and only pick std::invoke_result if C++ 20 is available? Assuming that the problematic older apple clang versions mentioned in the comment, which is 4 years old, are ones that don’t support C++ 20 anyway this could be a straightforward solution. At least I can confirm that std::invoke_result is available with apple clang 12

Okay, I just set up a simple test project with a CMakeLists like this:

cmake_minimum_required(VERSION 3.20)
project(HomebrewClangTests)

set(CMAKE_CXX_STANDARD 17)

add_subdirectory(JUCE)

juce_add_gui_app(HomebrewClangTests VERSION 0.0.1)

target_sources(HomebrewClangTests PRIVATE  main.cpp)

target_link_libraries(HomebrewClangTests PRIVATE juce::juce_gui_extra)

And an main.cpp like this:

#include <juce_gui_extra/juce_gui_extra.h>

class AppleRemoteApp : public juce::JUCEApplication
{
public:
    class MainWindow : public juce::DocumentWindow, public juce::AppleRemoteDevice
    {
    public:
        MainWindow (juce::String name)  : DocumentWindow (name,
                                                          juce::Colours::lightgrey,
                                                          DocumentWindow::allButtons)
        {
            centreWithSize (300, 200);
            setUsingNativeTitleBar (true);
            setVisible (true);
            start (false);
        }

        void closeButtonPressed() override
        {
            juce::JUCEApplication::getInstance()->systemRequestedQuit();
            stop();
        }

        void buttonPressed(ButtonType, bool) override {}

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };

    void initialise (const juce::String& commandLine) override
    {
        mainWindow.reset (new MainWindow (getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr;
    }

    const juce::String getApplicationName() override { return "clang test"; }

    const juce::String getApplicationVersion() override { return "0.0.1"; }

private:
    std::unique_ptr<MainWindow> mainWindow;
};

START_JUCE_APPLICATION (AppleRemoteApp)

It compiles and runs without any issues with the setup as described above. I deliberately made it inherit AppleRemoteDevice since the error reported in the topic you linked refers to that and checked out JUCE 6.1.3

Thanks for the data point.

For std::invoke_result I can’t see where we’d run into problems just removing the JUCE_CLANG clause. I’ve been through all the vanilla clang versions on Compiler Explorer and they all behave themselves, and Xcode 9 doesn’t understand -std=c++17 whereas Xcode 10 with -std=c++17 can find std::invoke_result in the standard library. Perhaps something else has changed, but it’s not clear what.

1 Like