--!A cross-platform build utility based on Lua -- -- 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. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki, Arthapz -- @file clang/support.lua -- -- imports import("core.base.semver") import("core.base.option") import("core.base.json") import("lib.detect.find_tool") import("lib.detect.find_file") import(".support", {inherit = true}) -- get includedirs for stl headers -- -- $ echo '#include ' | clang -x c++ -E - | grep '/vector"' -- # 1 "/usr/include/c++/11/vector" 1 3 -- # 58 "/usr/include/c++/11/vector" 3 -- # 59 "/usr/include/c++/11/vector" 3 -- function _get_toolchain_includedirs_for_stlheaders(target, includedirs, clang) local tmpfile = os.tmpfile() .. ".cc" io.writefile(tmpfile, "#include ") local argv = {"-E", "-x", "c++", tmpfile} local cpplib = get_cpplibrary_name(target) if cpplib then if cpplib == "c++" then table.insert(argv, 1, "-stdlib=libc++") elseif cpplib == "stdc++" then table.insert(argv, 1, "-stdlib=libstdc++") end end local result = try {function () return os.iorunv(clang, argv, {envs = compinst:runenvs()}) end} if result then for _, line in ipairs(result:split("\n", {plain = true})) do local line = line:trim() if line:startswith("#") and line:find("/vector\"", 1, true) then local includedir = line:match("\"(.+)/vector\"") if includedir and os.isdir(includedir) then table.insert(includedirs, path.normalize(includedir)) break end end end end os.tryrm(tmpfile) end function _get_std_module_manifest_path(target) local print_module_manifest_flag = get_print_library_module_manifest_path_flag(target) local clang_path = path.directory(get_clang_path(target)) if print_module_manifest_flag then local compinst = target:compiler("cxx") local outdata, _ = try { function() return os.iorunv(compinst:program(), {"-std=c++23", "-stdlib=libc++", "--sysroot=" .. path.join(clang_path, ".."), print_module_manifest_flag}, {envs = compinst:runenvs()}) end } if outdata and not outdata:startswith("") then return outdata:trim() end end -- fallback on custom detection -- manifest can be found in /lib subdirectory (i.e on debian it should be /lib/x86_64-unknown-linux-gnu/) local clang_lib_path = path.join(clang_path, "..", "lib") local modules_json_path = path.join(clang_lib_path, "libc++.modules.json") if not os.isfile(modules_json_path) then modules_json_path = find_file("*/libc++.modules.json", clang_lib_path) end return modules_json_path end -- load module support for the current target function load(target) local _, modulestsflag, withoutflag = get_modulesflag(target) -- add module flags if not withoutflag then target:add("cxxflags", modulestsflag) end -- fix default visibility for functions and variables [-fvisibility] differs in PCH file vs. current file -- module.pcm cannot be loaded due to a configuration mismatch with the current compilation. -- -- it will happen in binary target depend on library target with modules, and enable release mode at same time. -- -- @see https://github.com/xmake-io/xmake/issues/3358#issuecomment-1432586767 local dep_symbols local has_library_deps = false for _, dep in ipairs(target:orderdeps()) do if dep:is_shared() or dep:is_static() or dep:is_object() then dep_symbols = dep:get("symbols") has_library_deps = true break end end if has_library_deps then target:set("symbols", dep_symbols and dep_symbols or "none") end -- on Windows before llvm18 we need to disable delayed-template-parsing because it's incompatible with modules, from llvm >= 18, it's disabled by default local clang_version = get_clang_version(target) if semver.compare(clang_version, "18") < 0 then target:add("cxxflags", "-fno-delayed-template-parsing") end end function has_two_phase_compilation_support(target) return target:policy("build.c++.modules.two_phases") end -- flags that doesn't affect bmi generation function strippeable_flags() -- speculative list as there is no resource that list flags that prevent reusability, this list will likely be improve over time -- @see https://clang.llvm.org/docs/StandardCPlusPlusModules.html#consistency-requirement local strippable_flags = { "g", "O", "W", "w", "Q", "fmodule-file", "fPIC", "fsanitize", "embed-dir" } local splitted_strippeable_flags = { "I", "isystem", "cxx-isystem", "framework" } return strippable_flags, splitted_strippeable_flags end -- provide toolchain include directories for stl headerunit when p1689 is not supported function toolchain_includedirs(target) local includedirs = _g.includedirs if includedirs == nil then includedirs = {} local clang, toolname = target:tool("cxx") assert(toolname:startswith("clang")) _get_toolchain_includedirs_for_stlheaders(target, includedirs, clang) local cpplib = get_cpplibrary_name(target) local runtime_flag if cpplib then if cpplib == "c++" then runtime_flag = "-stdlib=libc++" elseif cpplib == "stdc++" then runtime_flag = "-stdlib=libstdc++" end end local _, result = try {function () return os.iorunv(clang, table.join({"-E", "-Wp,-v", "-xc++", os.nuldev()}, runtime_flag or {})) end} if result then for _, line in ipairs(result:split("\n", {plain = true})) do local line = line:trim() if os.isdir(line) then table.insert(includedirs, path.normalize(line)) elseif line:startswith("End") then break end end end _g.includedirs = includedirs end return includedirs end -- get clang path function get_clang_path(target) local clang_path = _g.clang_path if not clang_path then local program, toolname = target:tool("cxx") if program and toolname:startswith("clang") then local clang = find_tool(toolname, {program = program, envs = os.getenvs(), cachekey = "modules_support_clang_" .. toolname}) if clang then clang_path = clang.program end end clang_path = clang_path or false _g.clang_path = clang_path end return clang_path or nil end -- get clang version function get_clang_version(target) local clang_version = _g.clang_version if not clang_version then local program, toolname = target:tool("cxx") if program and toolname:startswith("clang") then local clang = find_tool(toolname, {program = program, version = true, envs = os.getenvs(), cachekey = "modules_support_clang_" .. toolname}) if clang then clang_version = clang.version end end clang_version = clang_version or false _g.clang_version = clang_version end return clang_version or nil end -- get clang-scan-deps function get_clang_scan_deps(target) local clang_scan_deps = _g.clang_scan_deps if not clang_scan_deps then local program, toolname = target:tool("cxx") if program and toolname:startswith("clang") then local dir = path.directory(program) local basename = path.basename(program) if basename == "clang-cl" then basename = "clang" end local extension = path.extension(program) program = (basename:gsub("%+%+", ""):gsub("clang", "clang-scan-deps")) .. extension if dir and dir ~= "." and os.isdir(dir) then program = path.join(dir, program) end local result = find_tool("clang-scan-deps", {program = program, version = true}) if not result then -- find a system wide alternative result = find_tool("clang-scan-deps", {version = true}) end if result then clang_scan_deps = result.program end end clang_scan_deps = clang_scan_deps or false _g.clang_scan_deps = clang_scan_deps end return clang_scan_deps or nil end function parse_link_files(filepath) if not os.islink(filepath) then return filepath end local target = os.readlink(filepath) if path.is_absolute(target) then return target end return path.join(path.directory(filepath), target) end function get_original_file(filepath) while os.islink(filepath) do filepath = parse_link_files(filepath) end return filepath end function get_stdmodules(target) local cpplib = get_cpplibrary_name(target) if cpplib then if cpplib == "c++" then -- libc++ module is found by parsing libc++.modules.json local modules_json_path = _get_std_module_manifest_path(target) if not modules_json_path then wprint("libc++.modules.json not found! maybe try to add --sdk= or install libc++") return end local modules_json = json.decode(io.readfile(modules_json_path)) if not (modules_json and modules_json.modules and #modules_json.modules > 0) then wprint("libc++.modules.json is invalid! path: %s", path.normalize(modules_json_path)) return end local std_module_directory = path.directory(modules_json.modules[1]["source-path"]) -- check absolute path first if path.is_absolute(std_module_directory) then if os.isdir(std_module_directory) then return {path.normalize(path.join(std_module_directory, "std.cppm")), path.normalize(path.join(std_module_directory, "std.compat.cppm"))} else wprint("std module directory not found: %s which defined in %s", path.normalize(std_module_directory), path.normalize(modules_json_path)) return end end -- otherwise try to resolve relative path local try_std_module_directory -- first try the directory relative to libc++.modules.json try_std_module_directory = path.join(path.directory(modules_json_path), std_module_directory) if os.isdir(try_std_module_directory) then return {path.normalize(path.join(try_std_module_directory, "std.cppm")), path.normalize(path.join(try_std_module_directory, "std.compat.cppm"))} end -- then try the directory relative to clang bin directory try_std_module_directory = path.join(path.directory(get_original_file(get_clang_path(target))), std_module_directory) if os.isdir(try_std_module_directory) then return {path.normalize(path.join(try_std_module_directory, "std.cppm")), path.normalize(path.join(try_std_module_directory, "std.compat.cppm"))} end elseif cpplib == "stdc++" then -- dont be greedy and don't enable stdc++ std module support for llvm < 19 local clang_version = get_clang_version(target) if clang_version and semver.compare(clang_version, "19.0") >= 0 then return import(".gcc.support").get_stdmodules(target) end elseif cpplib == "msstl" then -- msstl std module file is not compatible with llvm < 19 local clang_version = get_clang_version(target) if clang_version and semver.compare(clang_version, "19.0") >= 0 then local toolchain = target:toolchain("llvm") or target:toolchain("clang") or target:toolchain("clang-cl") return import(".msvc.support").get_stdmodules(target, {toolchain = toolchain}) end end end wprint("std and std.compat modules not found! maybe try to add --sdk= or install libc++") end function get_bmi_extension() return ".pcm" end function get_modulesflag(target) local clangmodulesflag = _g.clangmodulesflag local modulestsflag = _g.modulestsflag local withoutflag = _g.withoutflag if clangmodulesflag == nil and modulestsflag == nil then local compinst = target:compiler("cxx") if compinst:has_flags("-fmodules", "cxxflags", {flagskey = "clang_modules"}) then clangmodulesflag = "-fmodules" end if compinst:has_flags("-fmodules-ts", "cxxflags", {flagskey = "clang_modules_ts"}) then modulestsflag = "-fmodules-ts" end local clang_version = get_clang_version(target) withoutflag = semver.compare(clang_version, "16.0") >= 0 assert(withoutflag or modulestsflag, "compiler(clang): does not support c++ module!") _g.clangmodulesflag = clangmodulesflag or false _g.modulestsflag = modulestsflag or false _g.withoutflag = withoutflag or false end return clangmodulesflag or nil, modulestsflag or nil, withoutflag or nil end function get_modulefileflag(target) local modulefileflag = _g.modulefileflag if modulefileflag == nil then local compinst = target:compiler("cxx") if compinst:has_flags("-fmodule-file=" .. os.tmpfile() .. get_bmi_extension(), "cxxflags", {flagskey = "clang_module_file"}) then modulefileflag = "-fmodule-file=" end assert(modulefileflag, "compiler(clang): does not support c++ module!") _g.modulefileflag = modulefileflag or false end return modulefileflag or nil end function get_moduleheaderflag(target) local moduleheaderflag = _g.moduleheaderflag if moduleheaderflag == nil then local compinst = target:compiler("cxx") if compinst:has_flags("-fmodule-header=system", "cxxflags", {flagskey = "clang_module_header"}) then moduleheaderflag = "-fmodule-header=" end _g.moduleheaderflag = moduleheaderflag or false end return moduleheaderflag or nil end function get_modulesreducedbmiflag(target) local modulesreducedbmiflag = _g.modulesreducedbmiflag if modulesreducedbmiflag == nil then local compinst = target:compiler("cxx") if compinst:has_flags("-fmodules-reduced-bmi", "cxxflags", {flagskey = "clang_modules_reduced_bmi"}) then modulesreducedbmiflag = "-fmodules-reduced-bmi" elseif compinst:has_flags("-fexperimental-modules-reduced-bmi", "cxxflags", {flagskey = "clang_modules_reduced_bmi"}) then modulesreducedbmiflag = "-fexperimental-modules-reduced-bmi" end _g.modulesreducedbmiflag = modulesreducedbmiflag or false end return modulesreducedbmiflag or nil end function has_clangscandepssupport(target) local support_clangscandeps = _g.support_clangscandeps if support_clangscandeps == nil then local clangscandeps = get_clang_scan_deps(target) local clang_version = get_clang_version(target) if clangscandeps and clang_version and semver.compare(clang_version, "16.0") >= 0 then support_clangscandeps = true end _g.support_clangscandeps = support_clangscandeps or false end return support_clangscandeps or nil end function get_keepsystemincludesflag(target) local keepsystemincludesflag = _g.keepsystemincludesflag if keepsystemincludesflag == nil then local compinst = target:compiler("cxx") local clang_version = get_clang_version(target) if compinst:has_flags("-E -fkeep-system-includes", "cxxflags", {flagskey = "clang_keep_system_includes", tryrun = true}) and semver.compare(clang_version, "18.0") >= 0 then keepsystemincludesflag = "-fkeep-system-includes" end _g.keepsystemincludesflag = keepsystemincludesflag or false end return keepsystemincludesflag or nil end function get_moduleoutputflag(target) local moduleoutputflag = _g.moduleoutputflag if moduleoutputflag == nil then local compinst = target:compiler("cxx") local clang_version = get_clang_version(target) if compinst:has_flags("-fmodule-output=", "cxxflags", {flagskey = "clang_module_output", tryrun = true}) and semver.compare(clang_version, "16.0") >= 0 then moduleoutputflag = "-fmodule-output=" end _g.moduleoutputflag = moduleoutputflag or false end return moduleoutputflag or nil end function get_print_library_module_manifest_path_flag(target) local print_library_module_manifest_path_flag = _g.print_library_module_manifest_path_flag if print_library_module_manifest_path_flag == nil then local compinst = target:compiler("cxx") if compinst:has_flags("-print-library-module-manifest-path", "cxxflags", {flagskey = "clang_print_library_module_manifest_path", tryrun = true}) then print_library_module_manifest_path_flag = "-print-library-module-manifest-path" end _g.print_library_module_manifest_path_flag = print_library_module_manifest_path_flag or false end return print_library_module_manifest_path_flag or nil end