我正在开发一个使用 Clang 库来解析 C++ 源代码并进一步静态分析其 AST 的项目。 在 Linux 上一切都很好(该项目特别支持 Ubuntu 发行版),但是最近也将支持 macOS 添加到了开发计划中。
但是,在 macOS 上构建我们的工具并将其链接到 Clang 库(从 HomeBrew 安装)后,由于解决标准库头的包含语句时出现一些奇怪的问题,解析 C++ 源代码失败。
作为一个简单的测试,假设我们想要解析 tinyxml2 项目,它是一个用 C++ 编写的简单 XML 解析器,仅包含 2 个翻译单元。由于它使用 CMake 作为构建系统,因此可以轻松生成
compile_commands.json
文件,内容如下:
[
{
"directory": "<path to project dir>/tinyxml2/build",
"command": "/Library/Developer/CommandLineTools/usr/bin/c++ -I<path to project dir>/tinyxml2 -arch arm64 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk -mmacosx-version-min=13.5 -fvisibility=hidden -fvisibility-inlines-hidden -o CMakeFiles/tinyxml2.dir/tinyxml2.cpp.o -c /Users/mate/Documents/cc/projects/tinyxml2/tinyxml2.cpp",
"file": "<path to project dir>/tinyxml2/tinyxml2.cpp",
"output": "CMakeFiles/tinyxml2.dir/tinyxml2.cpp.o"
},
{
"directory": "<path to project dir>/tinyxml2/build",
"command": "/Library/Developer/CommandLineTools/usr/bin/c++ -I<path to project dir>/tinyxml2 -arch arm64 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk -mmacosx-version-min=13.5 -fvisibility=hidden -fvisibility-inlines-hidden -o CMakeFiles/xmltest.dir/xmltest.cpp.o -c <path to project dir>tinyxml2/xmltest.cpp",
"file": "<path to project dir>/tinyxml2/xmltest.cpp",
"output": "CMakeFiles/xmltest.dir/xmltest.cpp.o"
}
]
直接从此文件执行命令会成功并正确构建相应的目标文件。
但是,当使用同样的
compile_commands.json
来参数化libClang时,会出现以下奇怪的错误:
In file included from <path to project dir>/tinyxml2/tinyxml2.cpp:24:
<path to project dir>/tinyxml2/tinyxml2.h:37:13: fatal error: cannot open file '<path to project dir>/tinyxml2/cctype': No such file or directory
# include <cctype>
由于某种原因,在项目的文件夹中搜索
cctype
系统标头,尽管 -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk
标志已被传递并且它包含正确的路径。
更奇怪的是,当我删除
-I<path to project dir>/tinyxml2
标志时,找到了 cctype
系统标头,但出现了另一个错误:
In file included from <path to project dir>/tinyxml2/tinyxml2.cpp:24:
In file included from <path to project dir>/tinyxml2/tinyxml2.h:37:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk/usr/include/c++/v1/cctype:37:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk/usr/include/c++/v1/__assert:13:
/Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk/usr/include/c++/v1/__config:76:21: fatal error: cannot open file '/Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk/usr/include/c++/v1/RTK_device_methods.h': No such file or directory
# if __has_include(<RTK_device_methods.h>) && defined(_LIBCPP_HAS_NO_RANDOM_DEVICE) && defined(_LIBCPP_HAS_NO_LOCALIZATION)
我在 macOS 上的工作经验有限,因此欢迎任何帮助或建议! 在 Linux 上工作时不会出现此类问题。我还尝试确保
compile_commands.json
中使用相同版本的 Clang,该版本与我们的工具链接,但仍然存在相同的问题。 (所以使用 /opt/homebrew/opt/llvm@11/bin/clang++
而不是 /Library/Developer/CommandLineTools/usr/bin/c++
。)
我已经成功在两台不同的 Mac 上重现了这个问题 - 如果这很重要的话,两台 Mac 都有 ARM 处理器。
我从我们的项目中制作了一个 MWE(见下文),它可以重现这种行为。这是一个简单的 CMake 项目,它将生成一个名为
parse_test
的可执行二进制文件。
然后你就可以像./parse_test <path>/compile_commands.json
一样测试它。
CMakeLists.txt
的内容:
cmake_minimum_required(VERSION 3.16.3)
project(mwe-clang-apple)
# Set CXX standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Find LLVM
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")
# Check whether RTTI is on for LLVM
message(STATUS "Using RTTI for LLVM: ${LLVM_ENABLE_RTTI}")
if(NOT LLVM_ENABLE_RTTI)
message(SEND_ERROR "RTTI is required for LLVM")
endif()
# Check LLVM version
if (${LLVM_PACKAGE_VERSION} VERSION_LESS "10")
message(SEND_ERROR "Loaded LLVM version must be at least 10.0, ${LLVM_PACKAGE_VERSION} found.")
endif()
# Find Clang
find_package(Clang REQUIRED CONFIG)
message(STATUS "Using ClangConfig.cmake in: ${Clang_DIR}")
include_directories(SYSTEM
${LLVM_INCLUDE_DIRS}
${CLANG_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_definitions(${LLVM_DEFINITIONS})
add_executable(parse_test
main.cpp)
target_link_libraries(parse_test
clangTooling
clangFrontend
clang)
if(APPLE)
# Use Linux-like linking behaviour, as we reference headers from Clang without implementation
set_target_properties(parse_test PROPERTIES LINK_FLAGS
"-undefined dynamic_lookup")
endif(APPLE)
main.cpp
的内容:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <filesystem>
#include <clang/Tooling/JSONCompilationDatabase.h>
#include <clang/Tooling/Tooling.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/FrontendActions.h>
namespace fs = std::filesystem;
int parseWorker(const clang::tooling::CompileCommand& command_);
int main(int argc, char* argv[])
{
if (argc < 2) {
std::cout << "Pass the path to the JSON Compilation Database as an argument!" << std::endl;
return 1;
}
std::string jsonFile_ = std::string(argv[1]);
std::string errorMsg;
std::unique_ptr<clang::tooling::JSONCompilationDatabase> compDb
= clang::tooling::JSONCompilationDatabase::loadFromFile(
jsonFile_, errorMsg,
clang::tooling::JSONCommandLineSyntax::Gnu);
if (!errorMsg.empty())
{
std::cout << "Error: " << errorMsg << std::endl;
return 1;
}
//--- Read the compilation commands compile database ---//
std::vector<clang::tooling::CompileCommand> compileCommands =
compDb->getAllCompileCommands();
for (const auto& command : compileCommands)
{
std::cout << " Parsing " << command.Filename << std::endl;
int error = parseWorker(command);
if (error)
std::cout << " Parsing " << command.Filename << " has been failed." << std::endl;
}
return 0;
}
int parseWorker(const clang::tooling::CompileCommand& command_)
{
//--- Assemble compiler command line ---//
std::vector<const char*> commandLine;
commandLine.reserve(command_.CommandLine.size());
commandLine.push_back("--");
std::transform(
command_.CommandLine.begin() + 1, // Skip compiler name
command_.CommandLine.end(),
std::back_inserter(commandLine),
[](const std::string& s){ return s.c_str(); });
int argc = commandLine.size();
std::string compilationDbLoadError;
std::unique_ptr<clang::tooling::FixedCompilationDatabase> compilationDb(
clang::tooling::FixedCompilationDatabase::loadFromCommandLine(
argc,
commandLine.data(),
compilationDbLoadError));
if (!compilationDb)
{
std::cout
<< "Failed to create compilation database from command-line. "
<< compilationDbLoadError
<< std::endl;
return 1;
}
//--- Start the tool ---//
fs::path sourceFullPath(command_.Filename);
if (!sourceFullPath.is_absolute())
sourceFullPath = fs::path(command_.Directory) / command_.Filename;
clang::tooling::ClangTool tool(*compilationDb, sourceFullPath.string());
int error = tool.run(clang::tooling::newFrontendActionFactory<clang::SyntaxOnlyAction>().get());
return error;
}
不幸的是,在我看来,苹果已经开始与任何想要使用 XCode 和他们提供的东西以外的东西进行开发的人进行全面战争。
我已经成为苹果用户近 30 年了,他们以所谓的安全性为名,系统性地让在普通 *nix 系统上做起来如此简单的事情变得越来越困难。
我最终用 -v 编译了一个源文件...
clang++ -v -g3 -std=c++20 foo.cpp -c
然后获取该输出并将所有包含文件添加到我的命令行中,然后系统地删除它们,直到它满足我需要的最低限度。
我最终将其作为传递给应用程序的额外参数,然后将它们作为参数转发给 libclang 函数来解析文件。
xtra_incs=-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1 \
-isystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/include