--!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 build_ninja.lua -- -- imports import("core.project.config") import("core.project.project") import("core.platform.platform") import("core.language.language") import("core.tool.linker") import("core.tool.compiler") import("lib.detect.find_tool") import("lib.detect.find_toolname") import("core.tools.cl.parse_include") import("plugins.project.utils.target_cmds", {rootdir = os.programdir()}) import("private.utils.target", {alias = "target_utils"}) -- 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 -- escape path function _escape_path(filepath) if is_host("windows") then filepath = filepath:gsub('\\', '/') end return filepath end -- translate path function _translate_path(filepath, outputdir) filepath = path.translate(filepath) if filepath == "" then return "" end if path.is_absolute(filepath) then if filepath:startswith(project.directory()) then return path.relative(filepath, outputdir) end return filepath else return path.relative(path.absolute(filepath), outputdir) end end -- get relative unix path function _get_relative_unix_path(filepath, outputdir) filepath = _translate_path(filepath, outputdir) filepath = _escape_path(path.translate(filepath)) return os.args(filepath) end -- translate compiler flags function _translate_compflags(compflags, outputdir) local flags = {} local last_flag = nil; for _, flag in ipairs(compflags) do if flag == "-I" or flag == "-isystem" then last_flag = flag else if last_flag == "-I" or last_flag == "-isystem" then table.insert(flags, last_flag) flag = _get_relative_unix_path(flag, outputdir) else last_flag = flag for _, pattern in ipairs({"[%-](I)(.*)", "[%-](isystem)(.*)"}) do flag = flag:gsub(pattern, function (flag, dir) dir = _get_relative_unix_path(dir, outputdir) return "-" .. flag .. dir end) end end table.insert(flags, flag) end end return flags end -- translate linker flags function _translate_linkflags(linkflags, outputdir) local flags = {} for _, flag in ipairs(linkflags) do for _, pattern in ipairs({"[%-](L)(.*)", "[%-](F)(.*)"}) do flag = flag:gsub(pattern, function (flag, dir) dir = _get_relative_unix_path(dir, outputdir) return "-" .. flag .. dir end) end table.insert(flags, flag) end return flags end -- add header function _add_header(ninjafile) ninjafile:print([[# this is the build file for project %s # it is autogenerated by the xmake build system. # do not edit by hand. ]], project.name() or "") ninjafile:print("ninja_required_version = 1.5.1") ninjafile:print("") end -- add rules for generator function _add_rules_for_generator(ninjafile, outputdir) local projectdir = _get_relative_unix_path(os.projectdir(), outputdir) ninjafile:print("rule gen") ninjafile:print(" command = xmake project -P %s -k ninja", projectdir) ninjafile:print(" description = regenerating ninja files") ninjafile:print("") end -- add rules for complier (gcc) function _add_rules_for_compiler_gcc(ninjafile, sourcekind, program) local ccache = config.get("ccache") ~= false and find_tool("ccache") ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s%s $ARGS -MMD -MF $out.d -o $out -c $in", ccache and (ccache.program .. " ") or "", program) ninjafile:print(" deps = gcc") ninjafile:print(" depfile = $out.d") ninjafile:print(" description = %scompiling.%s $in", ccache and "ccache " or "", config.mode()) ninjafile:print("") end -- add rules for complier (clang) function _add_rules_for_compiler_clang(ninjafile, sourcekind, program) return _add_rules_for_compiler_gcc(ninjafile, sourcekind, program) end -- add rules for complier (clang-cl) function _add_rules_for_compiler_clang_cl(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s -showIncludes -c $ARGS $in -Fo$out", program) ninjafile:print(" deps = msvc") ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for complier (msvc/cl) function _add_rules_for_compiler_msvc_cl(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s -showIncludes -c $ARGS $in -Fo$out", program) ninjafile:print(" deps = msvc") ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for complier (msvc/ml) function _add_rules_for_compiler_msvc_ml(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s -c $ARGS -Fo$out $in", program) ninjafile:print(" deps = msvc") ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for resource complier (msvc/rc) function _add_rules_for_compiler_msvc_rc(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s $ARGS -Fo$out $in", program) ninjafile:print(" deps = msvc") ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for resource complier (windres/rc) function _add_rules_for_compiler_windres(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s $ARGS $in $out", program) ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for complier (cuda/nvcc) function _add_rules_for_compiler_nvcc(ninjafile, sourcekind, program) ninjafile:print("rule %s", sourcekind) ninjafile:print(" command = %s -c $ARGS $in -o $out", program) ninjafile:print(" description = compiling.%s $in", config.mode()) ninjafile:print("") end -- add rules for complier function _add_rules_for_compiler(ninjafile) ninjafile:print("# rules for compiler") if is_plat("windows") then -- @see https://github.com/ninja-build/ninja/issues/613 local note_include = parse_include.probe_include_note_from_cl() if not note_include then note_include = "Note: including file:" end ninjafile:print("msvc_deps_prefix = %s", note_include:trim()) end local add_compiler_rules = { gcc = _add_rules_for_compiler_gcc, gxx = _add_rules_for_compiler_gcc, clang = _add_rules_for_compiler_clang, clangxx = _add_rules_for_compiler_clang, cl = _add_rules_for_compiler_msvc_cl, clang_cl = _add_rules_for_compiler_clang_cl, ml = _add_rules_for_compiler_msvc_ml, ml64 = _add_rules_for_compiler_msvc_ml, rc = _add_rules_for_compiler_msvc_rc, windres = _add_rules_for_compiler_windres, nvcc = _add_rules_for_compiler_nvcc } for sourcekind, _ in pairs(language.sourcekinds()) do local program, toolname = platform.tool(sourcekind) if program then local add_rule = add_compiler_rules[toolname or find_toolname(program)] -- support of unknown compiler (xmake f --cc=gcc@my-cc) if not add_rule and toolname then add_rule = add_compiler_rules[find_toolname(toolname)] end if add_rule then add_rule(ninjafile, sourcekind, program) end end end ninjafile:print("") end -- add rules for linker (ar) function _add_rules_for_linker_ar(ninjafile, linkerkind, program) ninjafile:print("rule %s", linkerkind) ninjafile:print(" command = %s $ARGS $out $in", program) ninjafile:print(" description = archiving.%s $out", config.mode()) ninjafile:print("") end -- add rules for linker (gcc) function _add_rules_for_linker_gcc(ninjafile, linkerkind, program) ninjafile:print("rule %s", linkerkind) ninjafile:print(" command = %s -o $out $in $ARGS", program) ninjafile:print(" description = linking.%s $out", config.mode()) ninjafile:print("") end -- add rules for linker (clang) function _add_rules_for_linker_clang(ninjafile, linkerkind, program) return _add_rules_for_linker_gcc(ninjafile, linkerkind, program) end -- add rules for linker (msvc) function _add_rules_for_linker_msvc(ninjafile, linkerkind, program) if linkerkind == "ar" then program = program .. " -lib" elseif linkerkind == "sh" then program = program .. " -dll" end -- @note we use rspfile to handle long command limit on windows ninjafile:print("rule %s", linkerkind) ninjafile:print(" command = %s @$out.rsp", program) ninjafile:print(" rspfile = $out.rsp") ninjafile:print(" rspfile_content = $ARGS -out:$out $in_newline") ninjafile:print(" description = linking.%s $out", config.mode()) ninjafile:print("") end -- add rules for linker function _add_rules_for_linker(ninjafile) ninjafile:print("# rules for linker") local linkerkinds = {} for _, _linkerkinds in pairs(language.targetkinds()) do table.join2(linkerkinds, _linkerkinds) end local add_linker_rules = { ar = _add_rules_for_linker_ar, gcc = _add_rules_for_linker_gcc, gxx = _add_rules_for_linker_gcc, clang = _add_rules_for_linker_clang, clangxx = _add_rules_for_linker_clang, link = _add_rules_for_linker_msvc } for _, linkerkind in ipairs(table.unique(linkerkinds)) do local program, toolname = platform.tool(linkerkind) if program then local add_rule = add_linker_rules[toolname or find_toolname(program)] -- support of unknown linker (xmake f --ld=gcc@my-ld) if not add_rule and toolname then add_rule = add_linker_rules[find_toolname(toolname)] end if add_rule then add_rule(ninjafile, linkerkind, program) end end end ninjafile:print("") end -- add rules function _add_rules(ninjafile, outputdir) -- add rules for generator _add_rules_for_generator(ninjafile, outputdir) -- add rules for complier _add_rules_for_compiler(ninjafile) -- add rules for linker _add_rules_for_linker(ninjafile) end -- add build rule for phony function _add_build_for_phony(ninjafile, target) ninjafile:print("build %s: phony", target:name()) end -- add build rule for object function _add_build_for_object(ninjafile, target, sourcekind, sourcefile, objectfile, outputdir) objectfile = _get_relative_unix_path(objectfile, outputdir) sourcefile = _get_relative_unix_path(sourcefile, outputdir) local compflags = compiler.compflags(sourcefile, {target = target}) ninjafile:print("build %s: %s %s", objectfile, sourcekind, sourcefile) ninjafile:print(" ARGS = %s", os.args(_translate_compflags(compflags, outputdir))) ninjafile:print("") end -- add build rule for objects function _add_build_for_objects(ninjafile, target, sourcebatch, outputdir) for index, objectfile in ipairs(sourcebatch.objectfiles) do _add_build_for_object(ninjafile, target, sourcebatch.sourcekind, sourcebatch.sourcefiles[index], objectfile, outputdir) end end -- add build rule for target function _add_build_for_target(ninjafile, target, outputdir) -- https://github.com/xmake-io/xmake/issues/2337 target:data_set("plugin.project.kind", "ninja") -- is phony target? if target:is_phony() or target:is_headeronly() then return _add_build_for_phony(ninjafile, target) end -- build target ninjafile:print("# build target: %s", target:name()) local targetfile = _get_relative_unix_path(target:targetfile(), outputdir) ninjafile:print("build %s: phony %s", target:name(), targetfile) -- build target file ninjafile:printf("build %s: %s", targetfile, target:linker():kind()) local objectfiles = target:objectfiles() for _, objectfile in ipairs(objectfiles) do ninjafile:write(" " .. _get_relative_unix_path(objectfile, outputdir)) end -- merge objects with rule("utils.merge.object") for _, sourcebatch in pairs(target:sourcebatches()) do if sourcebatch.rulename == "utils.merge.object" then ninjafile:write(" " .. table.concat(sourcebatch.sourcefiles, " ")) end end local deps = target:get("deps") if deps then ninjafile:print(" || $") ninjafile:write(" ") for _, dep in ipairs(deps) do local dep_target = project.target(dep, {namespace = target:namespace()}); if not dep_target:is_headeronly() then ninjafile:write(" " .. _get_relative_unix_path(dep_target:targetfile(), outputdir)) end end end ninjafile:print("") ninjafile:print(" ARGS = %s", os.args(_translate_linkflags(target:linkflags(), outputdir))) ninjafile:print("") -- build target objects local sourcebatches = target:sourcebatches() for _, sourcebatch in table.orderpairs(sourcebatches) do if _sourcebatch_is_built(sourcebatch) then _add_build_for_objects(ninjafile, target, sourcebatch, outputdir) end end end -- add build rule for generator function _add_build_for_generator(ninjafile, outputdir) ninjafile:print("# build build.ninja") ninjafile:print("build build.ninja: gen $") local allfiles = project.allfiles() for idx, projectfile in ipairs(allfiles) do if not path.is_absolute(projectfile) or projectfile:startswith(os.projectdir()) then local filepath = _get_relative_unix_path(projectfile, outputdir) ninjafile:print(" %s %s", filepath, idx < #allfiles and "$" or "") end end ninjafile:print("") end -- add build rule for targets function _add_build_for_targets(ninjafile, outputdir) -- begin ninjafile:print("# build targets\n") -- add build rule for generator _add_build_for_generator(ninjafile, outputdir) local project_targets = target_utils.get_project_targets() -- TODO -- disable precompiled header first for _, target in pairs(project_targets) do target:set("pcheader", nil) target:set("pcxxheader", nil) end -- build targets for _, target in pairs(project_targets) do _add_build_for_target(ninjafile, target, outputdir) end -- build default local default = "" for targetname, target in pairs(project_targets) do if target:is_default() then default = default .. " " .. targetname end end ninjafile:print("build default: phony%s", default) -- build all local all = "" for targetname, _ in pairs(project_targets) do all = all .. " " .. targetname end ninjafile:print("build all: phony%s\n", all) -- end ninjafile:print("default default\n") end function make(outputdir) local oldir = os.cd(os.projectdir()) -- prepare targets target_cmds.prepare_targets() -- open the build.ninja file -- -- we need to change encoding to support msvc_deps_prefix -- @see https://github.com/ninja-build/ninja/issues/613 -- -- TODO maybe we need support more encoding for other languages -- local encoding = is_subhost("windows") and "gbk" local ninjafile = io.open(path.join(outputdir, "build.ninja"), "w", {encoding = encoding}) -- add header _add_header(ninjafile) -- add rules _add_rules(ninjafile, outputdir) -- add build rules for targets _add_build_for_targets(ninjafile, outputdir) -- close the ninjafile ninjafile:close() os.cd(oldir) end