--!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 cl.lua -- -- imports import("core.base.option") import("core.base.global") import("core.base.hashset") import("core.project.project") import("core.project.policy") import("core.language.language") import("private.utils.toolchain", {alias = "toolchain_utils"}) import("private.tools.vstool") import("core.tools.cl.parse_include") import("private.cache.build_cache") import("utils.checker") import("private.service.distcc_build.client", {alias = "distcc_build_client"}) import("utils.progress") -- init it function init(self) -- init cxflags self:set("cxflags", "-nologo") -- init flags map self:set("mapflags", { -- optimize ["-O0"] = "-Od" , ["-Os"] = "-O1" , ["-O3"] = "-Ox" , ["-Ofast"] = "-Ox -fp:fast" , ["-fomit-frame-pointer"] = "-Oy" -- symbols , ["-g"] = "-Z7" , ["-fvisibility=.*"] = "" -- warnings , ["-Weverything"] = "-Wall" , ["-Wextra"] = "-W4" , ["-Wall"] = "-W3" -- = "-Wall" will enable too more warnings , ["-W1"] = "-W1" , ["-W2"] = "-W2" , ["-W3"] = "-W3" , ["-Werror"] = "-WX" , ["-Wswitch"] = "-we4062" , ["-Wswitch-enum"] = "-we4061" , ["%-Wno%-error=.*"] = "" , ["%-fno%-.*"] = "" -- vectorexts , ["-mmmx"] = "-arch:MMX" , ["-msse"] = "-arch:SSE" , ["-msse2"] = "-arch:SSE2" , ["-msse3"] = "-arch:SSE3" , ["-mssse3"] = "-arch:SSSE3" , ["-mavx"] = "-arch:AVX" , ["-mavx2"] = "-arch:AVX2" , ["-mfpu=.*"] = "" -- language , ["-ansi"] = "" , ["-std=c99"] = "-TP" -- compile as c++ files because msvc doesn't support c99 , ["-std=c11"] = "-std:c11" , ["-std=c17"] = "-std:c17" , ["-std=c2x"] = "-std:clatest" , ["-std=c23"] = "-std:clatest" , ["-std=gnu99"] = "-TP" -- compile as c++ files because msvc doesn't support c99 , ["-std=gnu11"] = "-std:c11" , ["-std=gnu17"] = "-std:c17" , ["-std=gnu2x"] = "-std:clatest" , ["-std=gnu23"] = "-std:clatest" , ["-std=.*"] = "" -- others , ["-ftrapv"] = "" }) end -- is syntax check enabled? function _is_syntax_check() return checker.is_running("syntax") end -- make the symbol flags function nf_symbols(self, levels, opt) local flags = nil local values = hashset.from(levels) if values:has("debug") then flags = {} if values:has("edit") then table.insert(flags, "-ZI") elseif values:has("embed") then table.insert(flags, "-Z7") else table.insert(flags, "-Zi") end -- generate *.pdb file local symbolfile = nil local target = opt.target if target and target.symbolfile and not values:has("embed") then symbolfile = target:symbolfile() end if symbolfile then -- ensure the object directory local symboldir = path.directory(symbolfile) if not os.isdir(symboldir) then os.mkdir(symboldir) end -- check and add symbol output file -- -- @note we need to use `{}` to wrap it to avoid expand it -- https://github.com/xmake-io/xmake/issues/2061#issuecomment-1042590085 local pdbflags = {"-Fd" .. (target:is_static() and symbolfile or path.join(symboldir, "compile." .. path.filename(symbolfile)))} if self:has_flags({"-FS", "-Fd" .. os.nuldev() .. ".pdb"}, "cxflags", { flagskey = "-FS -Fd" }) then table.insert(pdbflags, 1, "-FS") end table.insert(flags, pdbflags) end end return flags end -- make the fp-model flag function nf_fpmodel(self, level) local maps = { precise = "-fp:precise" -- default , fast = "-fp:fast" , strict = "-fp:strict" , except = "-fp:except" , noexcept = "-fp:except-" } return maps[level] end -- make the warning flag function nf_warnings(self, levels) local flags = {} local values = hashset.from(levels) if values:has("all") and values:has("extra") then table.insert(flags, "-W4") values:remove("all") values:remove("extra") end local maps = { none = "-w" , less = "-W1" , more = "-W3" , all = "-W3" -- = "-Wall" will enable too more warnings , allextra = "-W4" , everything = "-Wall" , error = "-WX" } for _, level in values:orderkeys() do local flag = maps[level] if flag then table.insert(flags, flag) end end if #flags > 0 then return flags end end -- make the optimize flag function nf_optimize(self, level) local maps = { none = "-Od" , faster = "-Ox" , fastest = "-O2" , smallest = "-O1 -GL" -- /GL and (/OPT:REF is on by default in linker), we need to enable /ltcg , aggressive = "-O2" } return maps[level] end -- make the runtime flag function nf_runtime(self, runtime) if runtime then local maps = { MT = "-MT", MD = "-MD", MTd = "-MTd", MDd = "-MDd" } return maps[runtime] end end -- make the vector extension flag function nf_vectorext(self, extension) local maps = { sse = "-arch:SSE" , sse2 = "-arch:SSE2" , ["sse4.2"] = "/d2archSSE42" -- the only undocumented way works, @see https://github.com/xmake-io/xmake/issues/3786 , avx = "-arch:AVX" , avx2 = "-arch:AVX2" , avx512 = "-arch:AVX512" -- for msvc 2019+ avx512 support , fma = "-arch:AVX2" , all = {"-arch:SSE", "-arch:SSE2", "/d2archSSE42", "-arch:AVX", "-arch:AVX2", "-arch:AVX512"} } local flags = maps[extension] if flags then -- @see https://github.com/xmake-io/xmake/issues/5499 if type(flags) == "string" then return flags else local result = {} for _, flag in ipairs(flags) do if self:has_flags(flag, "cxflags") then table.insert(result, flag) end end if #result > 0 then return table.unwrap(result) end end end end -- make the language flag -- clang-cl should also use it, @see https://github.com/xmake-io/xmake/issues/2211#issuecomment-1083322178 function nf_language(self, stdname) -- the stdc maps if _g.cmaps == nil then _g.cmaps = { -- stdc c99 = "-TP" -- compile as c++ files because older msvc only support c89 , gnu99 = "-TP" , c11 = {"-std:c11", "-std:clatest", "-TP"} , gnu11 = {"-std:c11", "-std:clatest", "-TP"} , c17 = {"-std:c17", "-std:clatest", "-TP"} , gnu17 = {"-std:c17", "-std:clatest", "-TP"} , c2x = {"-std:c23", "-std:clatest", "-TP"} , gnu2x = {"-std:c23", "-std:clatest", "-TP"} , c23 = {"-std:c23", "-std:clatest", "-TP"} , gnu23 = {"-std:c23", "-std:clatest", "-TP"} , clatest = {"-std:clatest", "-std:c23", "-std:c17", "-std:c11", "-TP"} , gnulatest = {"-std:clatest", "-std:c23", "-std:c17", "-std:c11", "-TP"} } end -- the stdc++ maps if _g.cxxmaps == nil then -- clang-cl with c++23preview does not work for c++ modules -- https://github.com/xmake-io/xmake/issues/7169 local cxx23 = {"-std:c++23", "-std:c++23preview", "-std:c++latest"} if self:name() == "clang_cl" then cxx23 = {"-std:c++23", "-std:c++latest"} end local cxx2b = cxx23 _g.cxxmaps = { cxx11 = "-std:c++11" , gnuxx11 = "-std:c++11" , cxx14 = "-std:c++14" , gnuxx14 = "-std:c++14" , cxx17 = "-std:c++17" , gnuxx17 = "-std:c++17" , cxx1z = "-std:c++17" , gnuxx1z = "-std:c++17" , cxx20 = {"-std:c++20", "-std:c++latest"} , gnuxx20 = {"-std:c++20", "-std:c++latest"} , cxx2a = {"-std:c++20", "-std:c++latest"} , gnuxx2a = {"-std:c++20", "-std:c++latest"} , cxx23 = cxx23 , gnuxx23 = cxx23 , cxx2b = cxx2b , gnuxx2b = cxx2b , cxx26 = {"-std:c++26", "-std:c++latest"} , gnuxx26 = {"-std:c++26", "-std:c++latest"} , cxxlatest = "-std:c++latest" , gnuxxlatest = "-std:c++latest" } local cxxmaps2 = {} for k, v in pairs(_g.cxxmaps) do cxxmaps2[k:gsub("xx", "++")] = v end table.join2(_g.cxxmaps, cxxmaps2) end -- select maps local maps = _g.cmaps if self:kind() == "cxx" or self:kind() == "mxx" then maps = _g.cxxmaps end -- map it local result = maps[stdname] if type(result) == "table" then for _, v in ipairs(result) do if self:has_flags(v, "cxflags") then result = v maps[stdname] = result return result end end else return result end end -- make the define flag function nf_define(self, macro) return {"-D" .. macro} end -- make the undefine flag function nf_undefine(self, macro) return "-U" .. macro end -- make the includedir flag function nf_includedir(self, dir) return {"-I" .. path.translate(dir)} end -- make the force include flag function nf_forceinclude(self, headerfile, opt) local target = opt.target local sourcekinds = target and target:extraconf("forceincludes", headerfile, "sourcekinds") if not sourcekinds or table.contains(table.wrap(sourcekinds), self:kind()) then return {"-FI", headerfile} end end -- make the sysincludedir flag function nf_sysincludedir(self, dir) local has_external_includedir = _g._HAS_EXTERNAL_INCLUDEDIR if has_external_includedir == nil then if self:has_flags({"-external:W0", "-external:I" .. os.args(path.translate(dir))}, "cxflags", {flagskey = "cl_external_includedir"}) then has_external_includedir = 2 -- full support elseif self:has_flags({"-experimental:external", "-external:W0", "-external:I" .. os.args(path.translate(dir))}, "cxflags", {flagskey = "cl_external_includedir_experimental"}) then has_external_includedir = 1 -- experimental support end has_external_includedir = has_external_includedir or 0 _g._HAS_EXTERNAL_INCLUDEDIR = has_external_includedir end if has_external_includedir >= 2 then return {"-external:W0", "-external:I" .. path.translate(dir)} elseif has_external_includedir >= 1 then return {"-experimental:external", "-external:W0", "-external:I" .. path.translate(dir)} else return nf_includedir(self, dir) end end -- make the exception flag -- -- e.g. -- set_exceptions("cxx") -- set_exceptions("no-cxx") function nf_exception(self, exp) local maps = { cxx = "/EHsc", ["no-cxx"] = "/EHs-c-" } return maps[exp] end -- make the encoding flag -- @see https://github.com/xmake-io/xmake/issues/2471 -- -- e.g. -- set_encodings("utf-8") -- set_encodings("source:utf-8", "target:utf-8") function nf_encoding(self, encoding) local kind local charset local splitinfo = encoding:split(":") if #splitinfo > 1 then kind = splitinfo[1] charset = splitinfo[2] else charset = encoding end local charsets = { ["utf-8"] = "utf-8", utf8 = "utf-8", gb2312 = "gb2312" } local flags = {} charset = charsets[charset:lower()] if charset then if not kind and charset == "utf-8" then table.insert(flags, "/utf-8") else if kind == "source" or not kind then table.insert(flags, "-source-charset:" .. charset) end if kind == "target" or not kind then table.insert(flags, "-execution-charset:" .. charset) end end end if #flags > 0 then return flags end end -- make the c precompiled header flag function nf_pcheader(self, pcheaderfile, opt) if self:kind() == "cc" then local target = opt.target local objectfiles = target:objectfiles() if objectfiles then table.insert(objectfiles, target:pcoutputfile("c") .. ".obj") end return {"-Yu" .. path.absolute(pcheaderfile), "-FI" .. path.absolute(pcheaderfile), "-Fp" .. target:pcoutputfile("c")} end end -- make the c++ precompiled header flag function nf_pcxxheader(self, pcheaderfile, opt) if self:kind() == "cxx" then local target = opt.target local objectfiles = target:objectfiles() if objectfiles then table.insert(objectfiles, target:pcoutputfile("cxx") .. ".obj") end return {"-Yu" .. path.absolute(pcheaderfile), "-FI" .. path.absolute(pcheaderfile), "-Fp" .. target:pcoutputfile("cxx")} end end -- add the special flags for the given source file of target -- -- @note only it called when fileconfig is set -- function add_sourceflags(self, sourcefile, fileconfig, target, targetkind) -- add language type flags explicitly if the sourcekind is changed. -- -- because compiler maybe compile `.c` as c++. -- e.g. -- add_files("*.c", {sourcekind = "cxx"}) -- local sourcekind = fileconfig.sourcekind if sourcekind then local maps = {cc = "-TC", cxx = "-TP"} return maps[sourcekind] end end -- make the compile arguments list for the precompiled header function _compargv_pch(self, pcheaderfile, pcoutputfile, flags) -- remove "-Yuxxx.h" and "-Fpxxx.pch" local pchflags = {} for _, flag in ipairs(flags) do if not flag:find("-Yu", 1, true) and not flag:find("-Fp", 1, true) then table.insert(pchflags, flag) end end -- compile as c/c++ source file if self:kind() == "cc" then table.insert(pchflags, "-TC") elseif self:kind() == "cxx" then table.insert(pchflags, "-TP") end -- make the compile arguments list return self:program(), table.join("-c", "-Yc", pchflags, "-Fp" .. pcoutputfile, "-Fo" .. pcoutputfile .. ".obj", pcheaderfile) end -- has /sourceDependencies xxx.json @see https://github.com/xmake-io/xmake/issues/868? function _has_source_dependencies(self) local has_source_dependencies = _g._HAS_SOURCE_DEPENDENCIES if has_source_dependencies == nil then local source_dependencies_jsonfile = os.tmpfile() .. ".json" if self:has_flags("/sourceDependencies " .. source_dependencies_jsonfile, "cxflags", {flagskey = "cl_sourceDependencies", on_check = function (ok, errors) -- even if cl does not support /sourceDependencies, it will not interrupt compilation if ok and not os.isfile(source_dependencies_jsonfile) then ok = false end return ok, errors end}) then has_source_dependencies = true end has_source_dependencies = has_source_dependencies or false _g._HAS_SOURCE_DEPENDENCIES = has_source_dependencies end return has_source_dependencies end function _is_in_vstudio() local is_in_vstudio = _g._IS_IN_VSTUDIO if is_in_vstudio == nil then is_in_vstudio = os.getenv("XMAKE_IN_VSTUDIO") or false _g._IS_IN_VSTUDIO = is_in_vstudio end return is_in_vstudio end -- get preprocess file path function _get_cppfile(sourcefile, objectfile) return path.join(path.directory(objectfile), "__cpp_" .. path.basename(objectfile) .. path.extension(sourcefile)) end -- do preprocess function _preprocess(program, argv, opt) -- get flags and source file local flags = {} local cppflags = {} local skipped = 0 local objectfile local pdbfile local sourcefile = argv[#argv] local extension = path.extension(sourcefile) for _, flag in ipairs(argv) do local flag = flag if flag:startswith("-Fo") or flag:startswith("/Fo") then objectfile = flag:sub(4) break end -- get preprocessor flags -- TODO fix precompiled header bug for /P + /FI local target = opt.target if target and (flag:startswith("-FI") or flag:startswith("/FI")) then local pcheaderfile = target:get("pcheader") or target:get("pcxxheader") if pcheaderfile then flag = "-FI" .. path.absolute(pcheaderfile) end end table.insert(cppflags, flag) -- get compiler flags if flag == "-showIncludes" or flag == "/showIncludes" or (flag:startswith("-I") and #flag > 2) or (flag:startswith("/I") and #flag > 2) or flag:startswith("-external:") or flag:startswith("/external:") then skipped = 1 -- @note we cannot ignore precompiled flags when compiling pch, @see https://github.com/xmake-io/xmake/issues/2885 elseif not toolchain_utils.is_cxx_headerext(extension) and ( flag:startswith("-Yu") or flag:startswith("/Yu") or flag:startswith("-FI") or flag:startswith("/FI") or flag:startswith("-Fp") or flag:startswith("/Fp")) then skipped = 1 elseif flag == "-I" or flag == "-sourceDependencies" or flag == "/sourceDependencies" then skipped = 2 elseif opt.remote and flag:startswith("-Fd") or flag:startswith("/Fd") then skipped = 1 pdbfile = flag:sub(4) --TODO handle remote pdb end if skipped > 0 then skipped = skipped - 1 else table.insert(flags, flag) end end assert(objectfile and sourcefile, "%s: iorunv(%s): invalid arguments!", self, program) -- is precompiled header? if objectfile:endswith(".pch") then return false end -- disable linemarkers? local linemarkers = _g.linemarkers if linemarkers == nil then if os.isfile(os.projectfile()) and project.policy("preprocessor.linemarkers") == false then linemarkers = false else linemarkers = true end _g.linemarkers = linemarkers end -- do preprocess local cppfile = _get_cppfile(sourcefile, objectfile) local cppfiledir = path.directory(cppfile) if not os.isdir(cppfiledir) then os.mkdir(cppfiledir) end if linemarkers == false then table.insert(cppflags, "-EP") else -- we cannot use `/P`, @see https://github.com/xmake-io/xmake/issues/2445 table.insert(cppflags, "-E") end table.insert(cppflags, sourcefile) return try{ function() -- https://github.com/xmake-io/xmake/issues/2902#issuecomment-1326934902 local outfile = cppfile local errfile = os.tmpfile() .. ".i.err" local inherit_handles_safely = true if not winos.inherit_handles_safely() then outfile = os.tmpfile() .. ".i.out" inherit_handles_safely = false end os.execv(program, winos.cmdargv(cppflags), table.join(opt, {stdout = outfile, stderr = errfile})) local errdata if os.isfile(errfile) then errdata = io.readfile(errfile) end os.tryrm(errfile) if not inherit_handles_safely then os.cp(outfile, cppfile) os.tryrm(outfile) end -- includes information will be output to stderr instead of stdout now return {outdata = errdata, errdata = errdata, sourcefile = sourcefile, objectfile = objectfile, cppfile = cppfile, cppflags = flags, pdbfile = pdbfile} end} end -- compile preprocessed file function _compile_preprocessed_file(program, cppinfo, opt) local outdata, errdata = vstool.iorunv(program, winos.cmdargv(table.join(cppinfo.cppflags, "-Fo" .. cppinfo.objectfile, cppinfo.cppfile)), opt) -- we need to get warning information from output cppinfo.outdata = outdata cppinfo.errdata = errdata end -- do compile function _compile(self, sourcefile, objectfile, compflags, opt) opt = opt or {} local function _compile_fallback() local program, argv = compargv(self, sourcefile, objectfile, compflags, opt) return vstool.iorunv(program, argv, {envs = self:runenvs()}) end local cppinfo if distcc_build_client.is_distccjob() and distcc_build_client.singleton():has_freejobs() then local program, argv = compargv(self, sourcefile, objectfile, compflags, table.join(opt, {rawargs = true})) cppinfo = distcc_build_client.singleton():compile(program, argv, {envs = self:runenvs(), preprocess = _preprocess, compile = _compile_preprocessed_file, compile_fallback = _compile_fallback, target = opt.target, tool = self, remote = true}) elseif build_cache.is_enabled(opt.target) and build_cache.is_supported(self:kind()) then local program, argv = compargv(self, sourcefile, objectfile, compflags, table.join(opt, {rawargs = true})) cppinfo = build_cache.build(program, argv, {envs = self:runenvs(), preprocess = _preprocess, compile = _compile_preprocessed_file, compile_fallback = _compile_fallback, target = opt.target, tool = self}) end if cppinfo then return cppinfo.outdata, cppinfo.errdata else return _compile_fallback() end end -- make the compile arguments list function compargv(self, sourcefile, objectfile, flags, opt) opt = opt or {} -- precompiled header? local extension = path.extension(sourcefile) if toolchain_utils.is_cxx_headerext(extension) then return _compargv_pch(self, sourcefile, objectfile, flags) end -- if syntax-only, add /Zs and skip -c and -Fo if _is_syntax_check() then table.insert(flags, "/Zs") local argv = table.join(flags, sourcefile) return self:program(), (opt and opt.rawargs) and argv or winos.cmdargv(argv) end -- suppress clang-cl warnings -- clang-cl: warning: argument unused during compilation: '-c' [-Wunused-command-line-argument] -- -- @see https://github.com/xmake-io/xmake/issues/7049 local argv = {} if not (self:name() == "clang_cl" and objectfile:endswith(".pcm")) then table.insert(argv, "-c") end -- make the compile arguments list table.join2(argv, flags, "-Fo" .. objectfile, sourcefile) return self:program(), (opt and opt.rawargs) and argv or winos.cmdargv(argv) end -- show warnings function _show_warnings(self, output, sourcefile) local lines = {} local has_warnings = false local has_source_dependencies = _has_source_dependencies(self) for _, line in ipairs(output:split("\n", {plain = true})) do local line = line:rtrim() if #line > 0 then local skip = false -- filter includes notes: "Note: including file: xxx.h" if not has_source_dependencies and parse_include.has_include_note(line) then skip = true end -- filter source filename echo -- -- e.g. -- main.cpp <-- skip it -- src\main.cpp(2): warning C5295: #warning xxx -- if not skip then -- we don't need use isfile() to check it, because it's too slow if sourcefile:endswith(line) and not line:find(":", 1, true) then skip = true end end if not skip then table.insert(lines, line) if line:find("warning", 1, true) then has_warnings = true end end end end if has_warnings and #lines > 0 then if not option.get("diagnosis") then lines = table.slice(lines, 1, (#lines > 16 and 16 or #lines)) end local warnings = table.concat(lines, "\r\n") progress.show_output("${color.warning}%s", warnings) end end -- compile the source file function compile(self, sourcefile, objectfile, dependinfo, flags, opt) -- ensure the object directory local objectdir = path.directory(objectfile) if not os.isdir(objectdir) then os.mkdir(objectdir) end -- compile it local depfile = nil local outdata = try { function () -- generate includes file local compflags = flags if dependinfo then if _has_source_dependencies(self) then depfile = os.tmpfile() .. ".json" compflags = table.join(flags, "/sourceDependencies", depfile) else compflags = table.join(flags, "-showIncludes") end end -- we need to show full file path to goto error position if xmake is called in vstudio -- https://github.com/xmake-io/xmake/issues/1049 if _is_in_vstudio() then if compflags == flags then compflags = table.join(flags, "-FC") else table.join2(compflags, "-FC") end end -- do compile return _compile(self, sourcefile, objectfile, compflags, opt) end, catch { function (errors) -- try removing the old object file for forcing to rebuild this source file os.tryrm(objectfile) -- remove preprocess file local cppfile = _get_cppfile(sourcefile, objectfile) os.tryrm(cppfile) -- use cl/stdout as errors first from vstool.iorunv() if type(errors) == "table" then local errs = errors.stdout or "" errs = errs .. (errors.stderr or "") errors = errs end local results = "" if depfile then results = tostring(errors) else -- filter includes notes: "Note: including file: xxx.h", @note maybe not english language for _, line in ipairs(tostring(errors):split("\n", {plain = true})) do local line = line:rtrim() if not parse_include.has_include_note(line) then results = results .. line .. "\r\n" end end end if not option.get("verbose") then results = results .. "\n ${yellow}> in ${bright}" .. sourcefile end raise(results) end }, finally { function (ok, outdata, errdata) if ok and policy.build_warnings(opt) then local output = outdata or "" if #output:trim() == 0 then output = errdata or "" end if #output > 0 then _show_warnings(self, output, sourcefile) end end end } } -- generate the dependent includes if dependinfo then if depfile and os.isfile(depfile) then dependinfo.depfiles_format = "cl_json" dependinfo.depfiles = io.readfile(depfile) os.tryrm(depfile) elseif outdata then dependinfo.depfiles_format = "cl" dependinfo.depfiles = outdata end end end