--!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 -- @file compile_commands.lua -- -- imports import("core.base.option") import("core.base.hashset") import("core.tool.compiler") import("core.project.rule") import("core.project.project") import("core.language.language") import("private.utils.batchcmds") import("private.utils.executable_path") import("private.utils.target", {alias = "target_utils"}) import("plugins.project.utils.target_cmds", {rootdir = os.programdir()}) import("actions.test.main", {rootdir = os.programdir(), alias = "test_action"}) -- escape path function _escape_path(p) return os.args(p, {escape = true, nowrap = true}) end -- this sourcebatch is built? function _sourcebatch_is_built(sourcebatch) -- we can only use rulename to filter them because sourcekind may be bound to multiple rules local rulename = sourcebatch.rulename if rulename == "c.build" or rulename == "c++.build" or rulename == "asm.build" or rulename == "cuda.build" or rulename == "objc.build" or rulename == "objc++.build" then return true end end -- Is there other supported source file, which come from custom rules? function _is_other_sourcefile(sourcefile) local extensions = _g._other_supported_exts if extensions == nil then extensions = hashset.from({".v", ".sv"}) _g._other_supported_exts = extensions end return extensions:has(path.extension(sourcefile)) end -- get LSP, clangd, ccls, ... function _get_lsp() local lsp = option.get("lsp") if lsp == nil then lsp = os.getenv("XMAKE_GENERATOR_COMPDB_LSP") end return lsp end -- specify windows sdk verison function _get_windows_sdk_arguments(target) local args = {} local msvc = target:toolchain("msvc") if msvc then local envs = msvc:runenvs() if envs then for _, dir in ipairs(path.splitenv(envs.INCLUDE)) do table.insert(args, "-imsvc") table.insert(args, dir) end end end return args end -- translate external/system include flags, because some tools (vscode) do not support them yet. -- https://github.com/xmake-io/xmake/issues/1050 function _translate_arguments(arguments) local args = {} local cc = path.basename(arguments[1]):lower() local is_include = false local lsp = _get_lsp() for idx, arg in ipairs(arguments) do local arg = arg -- convert path to string, maybe we need to convert path, but not supported now. arg = tostring(arg) -- see https://github.com/xmake-io/xmake/issues/1721 if idx == 1 and is_host("windows") and path.extension(arg) == "" then arg = arg .. ".exe" end if arg:startswith("-isystem-after", 1, true) then arg = "-I" .. arg:sub(15) elseif arg:startswith("-isystem", 1, true) then -- clangd support `-isystem`, we don't need to translate it -- @see https://github.com/xmake-io/xmake/issues/3020 if not lsp or lsp ~= "clangd" then arg = "-I" .. arg:sub(9) end elseif arg:find("[%-/]external:I") then arg = arg:gsub("[%-/]external:I", "-I") elseif arg:find("[%-/]external:W") or arg:find("[%-/]experimental:external") then arg = nil end -- @see use msvc-style flags for msvc to support language-server better -- https://github.com/xmake-io/xmake/issues/1284 if cc == "cl" and arg and arg:startswith("-") then arg = arg:gsub("^%-", "/") elseif cc == "nvcc" and arg then -- support -I path with spaces for nvcc -- https://github.com/xmake-io/xmake/issues/1726 if is_include then if arg and arg:find(' ', 1, true) then arg = "\"" .. arg .. "\"" end is_include = false elseif arg:startswith("-I") then local f = arg:sub(1, 2) local v = arg:sub(3) if v and v:find(' ', 1, true) then arg = f .. "\"" .. v .. "\"" end elseif arg:startswith("-ccbin=") then -- @see https://github.com/xmake-io/xmake/issues/4716 local f = arg:sub(1, 7) local v = arg:sub(8) if v then arg = f .. v:gsub("\\\\", "\\") end end end if arg and arg == "-I" then is_include = true end -- ignore pch flags -- https://github.com/xmake-io/xmake/issues/6710 if arg and (arg == "-include-pch" or arg:endswith(".pch")) then arg = nil end if arg then -- improve to support for "/usr/bin/xcrun -sdk macosx clang" -- @see -- https://github.com/xmake-io/xmake/issues/3159 -- https://github.com/xmake-io/xmake/issues/3286 if idx == 1 then arg = executable_path(arg) end table.insert(args, arg) end end return args end -- make command function _make_arguments(jsonfile, arguments, opt) -- attempt to get source file from arguments opt = opt or {} local sourcefile = opt.sourcefile if not sourcefile then for _, arg in ipairs(arguments) do local sourcekind = try {function () return language.sourcekind_of(path.filename(arg)) end} if sourcekind and os.isfile(arg) then sourcefile = tostring(arg) elseif _is_other_sourcefile(arg) and os.isfile(arg) then sourcefile = tostring(arg) end if sourcefile then break end end if not sourcefile then return end end -- translate some unsupported arguments arguments = _translate_arguments(arguments) -- https://github.com/xmake-io/xmake/issues/6058 local lsp = _get_lsp() local target = opt.target local cc = path.basename(arguments[1]):lower() if lsp and lsp == "clangd" and target and target:is_plat("windows") and cc ~= "nvcc" then table.join2(arguments, _get_windows_sdk_arguments(target)) end -- escape '"', '\' local arguments_escape = {} for _, arg in ipairs(arguments) do table.insert(arguments_escape, _escape_path(arg)) end -- remove repeat -- this is because some rules will repeatedly bind the same sourcekind, e.g. `rule("c++.build.modules.builder")` local key = hash.uuid(os.args(arguments_escape) .. sourcefile) local map = _g.map or {} _g.map = map if map[key] then return end -- make body local projectdir = os.projectdir() -- workaround for drive letter casing issue, can be removed when clangd resolves https://github.com/clangd/vscode-clangd/pull/747 if is_host("windows") and (lsp == nil or lsp == "clangd") then projectdir = projectdir:gsub("^[A-Z]:", string.lower) end jsonfile:printf( [[%s{ "directory": "%s", "arguments": ["%s"], "file": "%s" }]], (_g.firstline and "" or ",\n"), _escape_path(projectdir), table.concat(arguments_escape, "\", \""), _escape_path(sourcefile)) -- clear first line marks _g.firstline = false map[key] = true end -- add target custom commands function _add_target_custom_commands(jsonfile, target, suffix, cmds) for _, cmd in ipairs(cmds) do if cmd.program then _make_arguments(jsonfile, table.join(cmd.program, cmd.argv), {target = target}) end end end -- add target source commands function _add_target_source_commands(jsonfile, target) for _, sourcebatch in pairs(target:sourcebatches()) do local sourcekind = sourcebatch.sourcekind if sourcekind and _sourcebatch_is_built(sourcebatch) then for index, sourcefile in ipairs(sourcebatch.sourcefiles) do local objectfile = sourcebatch.objectfiles[index] local arguments = table.join(compiler.compargv(sourcefile, objectfile, {target = target, sourcekind = sourcekind, rawargs=true})) _make_arguments(jsonfile, arguments, {sourcefile = sourcefile, target = target}) end end end end -- add target commands function _add_target_commands(jsonfile, target) -- add before commands -- we use irpairs(groups), because the last group that should be given the highest priority. local cmds_before = target_cmds.get_target_buildcmds(target, {stages = {"before", "on"}}) _add_target_custom_commands(jsonfile, target, "before", cmds_before) -- add target source commands _add_target_source_commands(jsonfile, target) -- add after commands local cmds_after = target_cmds.get_target_buildcmds(target, {stages = {"after"}}) _add_target_custom_commands(jsonfile, target, "after", cmds_after) end -- add target function _add_target(jsonfile, target) -- https://github.com/xmake-io/xmake/issues/2337 target:data_set("plugin.project.kind", "compile_commands") -- disable compile_commands? if target:policy("generator.compile_commands") == false then return end -- enter package environments local oldenvs = os.addenvs(target:pkgenvs()) -- add target commands _add_target_commands(jsonfile, target) -- restore package environments os.setenvs(oldenvs) end -- get test targets -- https://github.com/xmake-io/xmake/issues/4750 function _get_test_targets() local test_targets = _g.test_targets if test_targets == nil then test_targets = {} for _, test in pairs(test_action.get_tests()) do local target = test.target if not target:is_phony() then table.insert(test_targets, target) end end _g.test_targets = test_targets end return test_targets end -- add targets function _add_targets(jsonfile) jsonfile:print("[") _g.firstline = true local project_targets = target_utils.get_project_targets() for _, target in pairs(project_targets) do if not target:is_phony() then _add_target(jsonfile, target) end end -- https://github.com/xmake-io/xmake/issues/4750 for _, target in ipairs(_get_test_targets()) do _add_target(jsonfile, target) end jsonfile:print("]") end -- prepare targets function _prepare_targets() target_cmds.prepare_targets() -- https://github.com/xmake-io/xmake/issues/7166 target_cmds.prepare_targets(_get_test_targets()) end -- generate compilation databases for clang-based tools(compile_commands.json) -- -- references: -- - https://clang.llvm.org/docs/JSONCompilationDatabase.html -- - https://sarcasm.github.io/notes/dev/compilation-database.html -- - http://eli.thegreenplace.net/2014/05/21/compilation-databases-for-clang-based-tools -- function make(outputdir) local oldir = os.cd(os.projectdir()) local jsonfile = io.open(path.join(outputdir, "compile_commands.json"), "w") os.setenv("XMAKE_IN_COMPILE_COMMANDS_PROJECT_GENERATOR", "true") _prepare_targets() _add_targets(jsonfile) jsonfile:close() os.setenv("XMAKE_IN_COMPILE_COMMANDS_PROJECT_GENERATOR", nil) os.cd(oldir) end