diff --git a/modules/gmake2/_manifest.lua b/modules/gmake2/_manifest.lua new file mode 100644 index 00000000..b71a00b0 --- /dev/null +++ b/modules/gmake2/_manifest.lua @@ -0,0 +1,9 @@ +return { + "_preload.lua", + "gmake2.lua", + "gmake2_cpp.lua", + "gmake2_csharp.lua", + "gmake2_makefile.lua", + "gmake2_utility.lua", + "gmake2_workspace.lua", +} diff --git a/modules/gmake2/_preload.lua b/modules/gmake2/_preload.lua new file mode 100644 index 00000000..3be06ec9 --- /dev/null +++ b/modules/gmake2/_preload.lua @@ -0,0 +1,67 @@ +-- +-- Name: gmake2/_preload.lua +-- Purpose: Define the gmake2 action. +-- Author: Blizzard Entertainment (Tom van Dijck) +-- Modified by: Aleksi Juvani +-- Vlad Ivanov +-- Created: 2016/01/01 +-- Copyright: (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + + newaction { + trigger = "gmake2", + shortname = "Alternative GNU Make", + description = "Generate GNU makefiles for POSIX, MinGW, and Cygwin", + + valid_kinds = { "ConsoleApp", "WindowedApp", "StaticLib", "SharedLib", "Utility", "Makefile" }, + + valid_languages = { "C", "C++", "C#" }, + + valid_tools = { + cc = { "clang", "gcc" }, + dotnet = { "mono", "msnet", "pnet" } + }, + + onInitialize = function() + p.modules.gmake2.cpp.initialize() + end, + + onWorkspace = function(wks) + p.escaper(p.modules.gmake2.esc) + p.generate(wks, p.modules.gmake2.getmakefilename(wks, false), p.modules.gmake2.generate_workspace) + end, + + onProject = function(prj) + p.escaper(p.modules.gmake2.esc) + local makefile = p.modules.gmake2.getmakefilename(prj, true) + if prj.kind == p.UTILITY then + p.generate(prj, makefile, p.modules.gmake2.utility.generate) + elseif prj.kind == p.MAKEFILE then + p.generate(prj, makefile, p.modules.gmake2.makefile.generate) + else + if project.isdotnet(prj) then + p.generate(prj, makefile, p.modules.gmake2.cs.generate) + elseif project.iscpp(prj) then + p.generate(prj, makefile, p.modules.gmake2.cpp.generate) + end + end + end, + + onCleanWorkspace = function(wks) + p.clean.file(wks, p.modules.gmake2.getmakefilename(wks, false)) + end, + + onCleanProject = function(prj) + p.clean.file(prj, p.modules.gmake2.getmakefilename(prj, true)) + end + } + +-- +-- Decide when the full module should be loaded. +-- + + return function(cfg) + return (_ACTION == "gmake2") + end diff --git a/modules/gmake2/gmake2.lua b/modules/gmake2/gmake2.lua new file mode 100644 index 00000000..523693e4 --- /dev/null +++ b/modules/gmake2/gmake2.lua @@ -0,0 +1,360 @@ +-- +-- gmake2.lua +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local project = p.project + + p.modules.gmake2 = {} + p.modules.gmake2._VERSION = p._VERSION + local gmake2 = p.modules.gmake2 + +-- +-- Write out the default configuration rule for a workspace or project. +-- +-- @param target +-- The workspace or project object for which a makefile is being generated. +-- + + function gmake2.defaultconfig(target) + -- find the right configuration iterator function for this object + local eachconfig = iif(target.project, project.eachconfig, p.workspace.eachconfig) + local iter = eachconfig(target) + + -- grab the first configuration and write the block + local cfg = iter() + if cfg then + _p('ifndef config') + _x(' config=%s', cfg.shortname) + _p('endif') + _p('') + end + end + + +--- +-- Escape a string so it can be written to a makefile. +--- + + function gmake2.esc(value) + result = value:gsub("\\", "\\\\") + result = result:gsub("\"", "\\\"") + result = result:gsub(" ", "\\ ") + result = result:gsub("%(", "\\(") + result = result:gsub("%)", "\\)") + + -- leave $(...) shell replacement sequences alone + result = result:gsub("$\\%((.-)\\%)", "$(%1)") + return result + end + + +-- +-- Get the makefile file name for a workspace or a project. If this object is the +-- only one writing to a location then I can use "Makefile". If more than one object +-- writes to the same location I use name + ".make" to keep it unique. +-- + + function gmake2.getmakefilename(this, searchprjs) + local count = 0 + for wks in p.global.eachWorkspace() do + if wks.location == this.location then + count = count + 1 + end + + if searchprjs then + for _, prj in ipairs(wks.projects) do + if prj.location == this.location then + count = count + 1 + end + end + end + end + + if count == 1 then + return "Makefile" + else + return ".make" + end + end + + +-- +-- Output a makefile header. +-- +-- @param target +-- The workspace or project object for which the makefile is being generated. +-- + + function gmake2.header(target) + local kind = iif(target.project, "project", "workspace") + + _p('# %s %s makefile autogenerated by Premake', p.action.current().shortname, kind) + _p('') + + if kind == "workspace" then + local haspch = false + for _, prj in ipairs(target.projects) do + for cfg in project.eachconfig(prj) do + if cfg.pchheader then + haspch = true + end + end + end + + if haspch then + _p('.NOTPARALLEL:') + _p('') + end + end + + gmake2.defaultconfig(target) + + _p('ifndef verbose') + _p(' SILENT = @') + _p('endif') + _p('') + end + + +-- +-- Rules for file ops based on the shell type. Can't use defines and $@ because +-- it screws up the escaping of spaces and parethesis (anyone know a fix?) +-- + + function gmake2.mkdir(dirname) + _p('ifeq (posix,$(SHELLTYPE))') + _p('\t$(SILENT) mkdir -p %s', dirname) + _p('else') + _p('\t$(SILENT) mkdir $(subst /,\\\\,%s)', dirname) + _p('endif') + end + + function gmake2.mkdirRules(dirname) + _p('%s:', dirname) + _p('\t@echo Creating %s', dirname) + gmake2.mkdir(dirname) + _p('') + end + +-- +-- Format a list of values to be safely written as part of a variable assignment. +-- + + function gmake2.list(value, quoted) + quoted = false + if #value > 0 then + if quoted then + local result = "" + for _, v in ipairs (value) do + if #result then + result = result .. " " + end + result = result .. p.quoted(v) + end + return result + else + return " " .. table.concat(value, " ") + end + else + return "" + end + end + + +-- +-- Convert an arbitrary string (project name) to a make variable name. +-- + + function gmake2.tovar(value) + value = value:gsub("[ -]", "_") + value = value:gsub("[()]", "") + return value + end + + + + function gmake2.path(cfg, value) + cfg = cfg.project or cfg + local dirs = path.translate(project.getrelative(cfg, value)) + + if type(dirs) == 'table' then + dirs = table.filterempty(dirs) + end + + return dirs + end + + + function gmake2.getToolSet(cfg) + local default = iif(cfg.system == p.MACOSX, "clang", "gcc") + local toolset = p.tools[_OPTIONS.cc or cfg.toolset or default] + if not toolset then + error("Invalid toolset '" .. cfg.toolset .. "'") + end + return toolset + end + + + function gmake2.outputSection(prj, callback) + local root = {} + + for cfg in project.eachconfig(prj) do + -- identify the toolset used by this configurations (would be nicer if + -- this were computed and stored with the configuration up front) + + local toolset = gmake2.getToolSet(cfg) + + local settings = {} + local funcs = callback(cfg) + for i = 1, #funcs do + local c = p.capture(function () + funcs[i](cfg, toolset) + end) + if #c > 0 then + table.insert(settings, c) + end + end + + if not root.settings then + root.settings = table.arraycopy(settings) + else + root.settings = table.intersect(root.settings, settings) + end + + root[cfg] = settings + end + + if #root.settings > 0 then + for _, v in ipairs(root.settings) do + p.outln(v) + end + p.outln('') + end + + for cfg in project.eachconfig(prj) do + local settings = table.difference(root[cfg], root.settings) + if #settings > 0 then + _x('ifeq ($(config),%s)', cfg.shortname) + for k, v in ipairs(settings) do + p.outln(v) + end + p.outln('endif') + p.outln('') + end + end + end + + +--------------------------------------------------------------------------- +-- +-- Handlers for the individual makefile elements that can be shared +-- between the different language projects. +-- +--------------------------------------------------------------------------- + + function gmake2.phonyRules(prj) + _p('.PHONY: clean prebuild prelink') + _p('') + end + + + function gmake2.shellType() + _p('SHELLTYPE := msdos') + _p('ifeq (,$(ComSpec)$(COMSPEC))') + _p(' SHELLTYPE := posix') + _p('endif') + _p('ifeq (/bin,$(findstring /bin,$(SHELL)))') + _p(' SHELLTYPE := posix') + _p('endif') + _p('') + end + + + function gmake2.target(cfg, toolset) + p.outln('TARGETDIR = ' .. project.getrelative(cfg.project, cfg.buildtarget.directory)) + p.outln('TARGET = $(TARGETDIR)/' .. cfg.buildtarget.name) + end + + + function gmake2.objdir(cfg, toolset) + p.outln('OBJDIR = ' .. project.getrelative(cfg.project, cfg.objdir)) + end + + + function gmake2.settings(cfg, toolset) + if #cfg.makesettings > 0 then + for _, value in ipairs(cfg.makesettings) do + p.outln(value) + end + end + + local value = toolset.getmakesettings(cfg) + if value then + p.outln(value) + end + end + + + function gmake2.buildCmds(cfg, event) + _p('define %sCMDS', event:upper()) + local steps = cfg[event .. "commands"] + local msg = cfg[event .. "message"] + if #steps > 0 then + steps = os.translateCommandsAndPaths(steps, cfg.project.basedir, cfg.project.location) + msg = msg or string.format("Running %s commands", event) + _p('\t@echo %s', msg) + _p('\t%s', table.implode(steps, "", "", "\n\t")) + end + _p('endef') + end + + + function gmake2.preBuildCmds(cfg, toolset) + gmake2.buildCmds(cfg, "prebuild") + end + + + function gmake2.preLinkCmds(cfg, toolset) + gmake2.buildCmds(cfg, "prelink") + end + + + function gmake2.postBuildCmds(cfg, toolset) + gmake2.buildCmds(cfg, "postbuild") + end + + + function gmake2.targetDirRules(cfg, toolset) + gmake2.mkdirRules("$(TARGETDIR)") + end + + + function gmake2.objDirRules(cfg, toolset) + gmake2.mkdirRules("$(OBJDIR)") + end + + + function gmake2.preBuildRules(cfg, toolset) + _p('prebuild:') + _p('\t$(PREBUILDCMDS)') + _p('') + end + + + function gmake2.preLinkRules(cfg, toolset) + _p('prelink:') + _p('\t$(PRELINKCMDS)') + _p('') + end + + + + include("gmake2_cpp.lua") + include("gmake2_csharp.lua") + include("gmake2_makefile.lua") + include("gmake2_utility.lua") + include("gmake2_workspace.lua") + + return gmake2 diff --git a/modules/gmake2/gmake2_cpp.lua b/modules/gmake2/gmake2_cpp.lua new file mode 100644 index 00000000..7826a59e --- /dev/null +++ b/modules/gmake2/gmake2_cpp.lua @@ -0,0 +1,792 @@ +-- +-- gmake2_cpp.lua +-- Generate a C/C++ project makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local gmake2 = p.modules.gmake2 + + gmake2.cpp = {} + local cpp = gmake2.cpp + + local project = p.project + local config = p.config + local fileconfig = p.fileconfig + + +--- +-- Add namespace for element definition lists for premake.callarray() +--- + + cpp.elements = {} + + +-- +-- Generate a GNU make C++ project makefile, with support for the new platforms API. +-- + + cpp.elements.makefile = function(prj) + return { + gmake2.header, + gmake2.phonyRules, + gmake2.shellType, + cpp.createRuleTable, + cpp.outputConfigurationSection, + cpp.outputPerFileConfigurationSection, + cpp.createFileTable, + cpp.outputFilesSection, + cpp.outputRulesSection, + cpp.outputFileRuleSection, + cpp.dependencies, + } + end + + + function cpp.generate(prj) + p.eol("\n") + p.callArray(cpp.elements.makefile, prj) + + -- allow the garbage collector to clean things up. + for cfg in project.eachconfig(prj) do + cfg._gmake = nil + end + prj._gmake = nil + end + + + function cpp.initialize() + rule 'cpp' + fileExtension {".cc", ".cpp", ".cxx", ".mm"} + buildoutputs {'$(OBJDIR)/%{file.objname}.o'} + buildmessage '$(notdir $<)' + buildcommands {'$(CXX) $(%{premake.modules.gmake2.cpp.fileFlags(cfg, file)}) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"'} + + rule 'cc' + fileExtension {".c", ".s", ".m"} + buildoutputs {'$(OBJDIR)/%{file.objname}.o'} + buildmessage '$(notdir $<)' + buildcommands {'$(CC) $(%{premake.modules.gmake2.cpp.fileFlags(cfg, file)}) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"'} + + rule 'resource' + fileExtension ".rc" + buildoutputs {'$(OBJDIR)/%{file.objname}.res'} + buildmessage '$(notdir $<)' + buildcommands {'$(RESCOMP) $< -O coff -o "$@" $(ALL_RESFLAGS)'} + + global(nil) + end + + + function cpp.createRuleTable(prj) + local rules = {} + + local function addRule(extension, rule) + if type(extension) == 'table' then + for _, value in ipairs(extension) do + addRule(value, rule) + end + else + rules[extension] = rule + end + end + + -- add all rules. + local usedRules = table.join({'cpp', 'cc', 'resource'}, prj.rules) + for _, name in ipairs(usedRules) do + local rule = p.global.getRule(name) + addRule(rule.fileExtension, rule) + end + + -- create fileset categories. + local filesets = { + ['.o'] = 'OBJECTS', + ['.obj'] = 'OBJECTS', + ['.cc'] = 'SOURCES', + ['.cpp'] = 'SOURCES', + ['.cxx'] = 'SOURCES', + ['.mm'] = 'SOURCES', + ['.c'] = 'SOURCES', + ['.s'] = 'SOURCES', + ['.m'] = 'SOURCES', + ['.rc'] = 'RESOURCES', + } + + -- cache the result. + prj._gmake = prj._gmake or {} + prj._gmake.rules = rules + prj._gmake.filesets = filesets + end + + + function cpp.createFileTable(prj) + for cfg in project.eachconfig(prj) do + cfg._gmake = cfg._gmake or {} + cfg._gmake.filesets = {} + cfg._gmake.fileRules = {} + + local files = table.shallowcopy(prj._.files) + table.foreachi(files, function(node) + cpp.addFile(cfg, node) + end) + + for _, f in pairs(cfg._gmake.filesets) do + table.sort(f) + end + + cfg._gmake.kinds = table.keys(cfg._gmake.filesets) + table.sort(cfg._gmake.kinds) + + prj._gmake.kinds = table.join(prj._gmake.kinds or {}, cfg._gmake.kinds) + end + + -- we need to reassign object sequences if we generated any files. + if prj.hasGeneratedFiles and p.project.iscpp(prj) then + p.oven.assignObjectSequences(prj) + end + + prj._gmake.kinds = table.unique(prj._gmake.kinds) + table.sort(prj._gmake.kinds) + end + + + function cpp.addFile(cfg, node) + local filecfg = fileconfig.getconfig(node, cfg) + if not filecfg or filecfg.flags.ExcludeFromBuild then + return + end + + -- skip generated files, since we try to figure it out manually below. + if node.generated then + return + end + + -- process custom build commands. + if fileconfig.hasCustomBuildRule(filecfg) then + local env = table.shallowcopy(filecfg.environ) + env.PathVars = { + ["file.basename"] = { absolute = false, token = node.basename }, + ["file.abspath"] = { absolute = true, token = node.abspath }, + ["file.relpath"] = { absolute = false, token = node.relpath }, + ["file.name"] = { absolute = false, token = node.name }, + ["file.objname"] = { absolute = false, token = node.objname }, + ["file.path"] = { absolute = true, token = node.path }, + ["file.directory"] = { absolute = true, token = path.getdirectory(node.abspath) }, + ["file.reldirectory"] = { absolute = false, token = path.getdirectory(node.relpath) }, + } + + local shadowContext = p.context.extent(filecfg, env) + + local buildoutputs = p.project.getrelative(cfg.project, shadowContext.buildoutputs) + if buildoutputs and #buildoutputs > 0 then + local file = { + buildoutputs = buildoutputs, + source = node.relpath, + buildmessage = shadowContext.buildmessage, + buildcommands = shadowContext.buildcommands, + buildinputs = p.project.getrelative(cfg.project, shadowContext.buildinputs) + } + table.insert(cfg._gmake.fileRules, file) + + for _, output in ipairs(buildoutputs) do + cpp.addGeneratedFile(cfg, node, output) + end + end + else + cpp.addRuleFile(cfg, node) + end + end + + function cpp.addGeneratedFile(cfg, source, filename) + -- mark that we have generated files. + cfg.project.hasGeneratedFiles = true + + -- add generated file to the project. + local files = cfg.project._.files + local node = files[filename] + if not node then + node = fileconfig.new(filename, cfg.project) + files[filename] = node + table.insert(files, node) + end + + -- always overwrite the dependency information. + node.dependsOn = source + node.generated = true + + -- add to config if not already added. + if not fileconfig.getconfig(node, cfg) then + fileconfig.addconfig(node, cfg) + end + + -- add file to the fileset. + local filesets = cfg.project._gmake.filesets + local kind = filesets[path.getextension(filename):lower()] or "CUSTOM" + + -- don't link generated object files automatically if it's explicitly + -- disabled. + if path.isobjectfile(filename) and source.linkbuildoutputs == false then + kind = "CUSTOM" + end + + local fileset = cfg._gmake.filesets[kind] or {} + table.insert(fileset, filename) + cfg._gmake.filesets[kind] = fileset + + -- recursively setup rules. + cpp.addRuleFile(cfg, node) + end + + function cpp.addRuleFile(cfg, node) + local rules = cfg.project._gmake.rules + local rule = rules[path.getextension(node.abspath):lower()] + if rule then + + local filecfg = fileconfig.getconfig(node, cfg) + local environ = table.shallowcopy(filecfg.environ) + + if rule.propertydefinition then + p.rule.prepareEnvironment(rule, environ, "$(%s)") + end + + local shadowContext = p.context.extent(rule, environ) + + local buildoutputs = shadowContext.buildoutputs + local buildmessage = shadowContext.buildmessage + local buildcommands = shadowContext.buildcommands + local buildinputs = shadowContext.buildinputs + + buildoutputs = p.project.getrelative(cfg.project, buildoutputs) + if buildoutputs and #buildoutputs > 0 then + local file = { + buildoutputs = buildoutputs, + source = node.relpath, + buildmessage = buildmessage, + buildcommands = buildcommands, + buildinputs = buildinputs + } + table.insert(cfg._gmake.fileRules, file) + + for _, output in ipairs(buildoutputs) do + cpp.addGeneratedFile(cfg, node, output) + end + end + end + end + + +-- +-- Write out the settings for a particular configuration. +-- + + cpp.elements.configuration = function(cfg) + return { + cpp.tools, + gmake2.target, + gmake2.objdir, + cpp.pch, + cpp.defines, + cpp.includes, + cpp.forceInclude, + cpp.cppFlags, + cpp.cFlags, + cpp.cxxFlags, + cpp.resFlags, + cpp.libs, + cpp.ldDeps, + cpp.ldFlags, + cpp.linkCmd, + cpp.bindirs, + cpp.exepaths, + cpp.ruleProperties, + gmake2.settings, + gmake2.preBuildCmds, + gmake2.preLinkCmds, + gmake2.postBuildCmds, + } + end + + + function cpp.outputConfigurationSection(prj) + _p('# Configurations') + _p('# #############################################') + _p('') + gmake2.outputSection(prj, cpp.elements.configuration) + end + + + function cpp.tools(cfg, toolset) + local tool = toolset.gettoolname(cfg, "cc") + if tool then + _p('ifeq ($(origin CC), default)') + _p(' CC = %s', tool) + _p('endif' ) + end + + tool = toolset.gettoolname(cfg, "cxx") + if tool then + _p('ifeq ($(origin CXX), default)') + _p(' CXX = %s', tool) + _p('endif' ) + end + + tool = toolset.gettoolname(cfg, "ar") + if tool then + _p('ifeq ($(origin AR), default)') + _p(' AR = %s', tool) + _p('endif' ) + end + + tool = toolset.gettoolname(cfg, "rc") + if tool then + _p('RESCOMP = %s', tool) + end + end + + + function cpp.pch(cfg, toolset) + -- If there is no header, or if PCH has been disabled, I can early out + if not cfg.pchheader or cfg.flags.NoPCH then + return + end + + -- Visual Studio requires the PCH header to be specified in the same way + -- it appears in the #include statements used in the source code; the PCH + -- source actual handles the compilation of the header. GCC compiles the + -- header file directly, and needs the file's actual file system path in + -- order to locate it. + + -- To maximize the compatibility between the two approaches, see if I can + -- locate the specified PCH header on one of the include file search paths + -- and, if so, adjust the path automatically so the user doesn't have + -- add a conditional configuration to the project script. + + local pch = cfg.pchheader + local found = false + + -- test locally in the project folder first (this is the most likely location) + local testname = path.join(cfg.project.basedir, pch) + if os.isfile(testname) then + pch = project.getrelative(cfg.project, testname) + found = true + else + -- else scan in all include dirs. + for _, incdir in ipairs(cfg.includedirs) do + testname = path.join(incdir, pch) + if os.isfile(testname) then + pch = project.getrelative(cfg.project, testname) + found = true + break + end + end + end + + if not found then + pch = project.getrelative(cfg.project, path.getabsolute(pch)) + end + + p.outln('PCH = ' .. pch) + p.outln('PCH_PLACEHOLDER = $(OBJDIR)/$(notdir $(PCH))') + p.outln('GCH = $(PCH_PLACEHOLDER).gch') + end + + + function cpp.defines(cfg, toolset) + p.outln('DEFINES +=' .. gmake2.list(table.join(toolset.getdefines(cfg.defines, cfg), toolset.getundefines(cfg.undefines)))) + end + + + function cpp.includes(cfg, toolset) + local includes = toolset.getincludedirs(cfg, cfg.includedirs, cfg.sysincludedirs) + p.outln('INCLUDES +=' .. gmake2.list(includes)) + end + + + function cpp.forceInclude(cfg, toolset) + local includes = toolset.getforceincludes(cfg) + if not cfg.flags.NoPCH and cfg.pchheader then + table.insert(includes, "-include $(PCH_PLACEHOLDER)") + end + p.outln('FORCE_INCLUDE +=' .. gmake2.list(includes)) + end + + + function cpp.cppFlags(cfg, toolset) + local flags = gmake2.list(toolset.getcppflags(cfg)) + p.outln('ALL_CPPFLAGS += $(CPPFLAGS)' .. flags .. ' $(DEFINES) $(INCLUDES)') + end + + + function cpp.cFlags(cfg, toolset) + local flags = gmake2.list(table.join(toolset.getcflags(cfg), cfg.buildoptions)) + p.outln('ALL_CFLAGS += $(CFLAGS) $(ALL_CPPFLAGS)' .. flags) + end + + + function cpp.cxxFlags(cfg, toolset) + local flags = gmake2.list(table.join(toolset.getcxxflags(cfg), cfg.buildoptions)) + p.outln('ALL_CXXFLAGS += $(CXXFLAGS) $(ALL_CPPFLAGS)' .. flags) + end + + + function cpp.resFlags(cfg, toolset) + local resflags = table.join(toolset.getdefines(cfg.resdefines), toolset.getincludedirs(cfg, cfg.resincludedirs), cfg.resoptions) + p.outln('ALL_RESFLAGS += $(RESFLAGS) $(DEFINES) $(INCLUDES)' .. gmake2.list(resflags)) + end + + + function cpp.libs(cfg, toolset) + local flags = toolset.getlinks(cfg) + p.outln('LIBS +=' .. gmake2.list(flags, true)) + end + + + function cpp.ldDeps(cfg, toolset) + local deps = config.getlinks(cfg, "siblings", "fullpath") + p.outln('LDDEPS +=' .. gmake2.list(p.esc(deps))) + end + + + function cpp.ldFlags(cfg, toolset) + local flags = table.join(toolset.getLibraryDirectories(cfg), toolset.getrunpathdirs(cfg, cfg.runpathdirs), toolset.getldflags(cfg), cfg.linkoptions) + p.outln('ALL_LDFLAGS += $(LDFLAGS)' .. gmake2.list(flags)) + end + + + function cpp.linkCmd(cfg, toolset) + if cfg.kind == p.STATICLIB then + if cfg.architecture == p.UNIVERSAL then + p.outln('LINKCMD = libtool -o "$@" $(OBJECTS)') + else + p.outln('LINKCMD = $(AR) -rcs "$@" $(OBJECTS)') + end + elseif cfg.kind == p.UTILITY then + -- Empty LINKCMD for Utility (only custom build rules) + p.outln('LINKCMD =') + else + -- this was $(TARGET) $(LDFLAGS) $(OBJECTS) + -- but had trouble linking to certain static libs; $(OBJECTS) moved up + -- $(LDFLAGS) moved to end (http://sourceforge.net/p/premake/patches/107/) + -- $(LIBS) moved to end (http://sourceforge.net/p/premake/bugs/279/) + + local cc = iif(p.languages.isc(cfg.language), "CC", "CXX") + p.outln('LINKCMD = $(' .. cc .. ') -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS)') + end + end + + + function cpp.bindirs(cfg, toolset) + local dirs = project.getrelative(cfg.project, cfg.bindirs) + if #dirs > 0 then + p.outln('EXECUTABLE_PATHS = "' .. table.concat(dirs, ":") .. '"') + end + end + + + function cpp.exepaths(cfg, toolset) + local dirs = project.getrelative(cfg.project, cfg.bindirs) + if #dirs > 0 then + p.outln('EXE_PATHS = export PATH=$(EXECUTABLE_PATHS):$$PATH;') + end + end + + + function cpp.ruleProperties(cfg, toolset) + for i = 1, #cfg.rules do + local rule = p.global.getRule(cfg.rules[i]) + + for prop in p.rule.eachProperty(rule) do + local fld = p.rule.getPropertyField(rule, prop) + local value = cfg[fld.name] + if value ~= nil then + if fld.kind == "path" then + value = gmake2.path(cfg, value) + elseif fld.kind == "list:path" then + value = gmake2.path(cfg, value) + end + + value = p.rule.expandString(rule, prop, value) + if value ~= nil and #value > 0 then + p.outln(prop.name .. ' = ' .. p.esc(value)) + end + end + end + end + end + +-- +-- Write out the per file configurations. +-- + function cpp.outputPerFileConfigurationSection(prj) + _p('# Per File Configurations') + _p('# #############################################') + _p('') + for cfg in project.eachconfig(prj) do + table.foreachi(cfg.project._.files, function(node) + local fcfg = fileconfig.getconfig(node, cfg) + if fcfg then + cpp.perFileFlags(cfg, fcfg) + end + end) + end + _p('') + end + + local function makeVarName(prj, value, saltValue) + prj._gmake.varlist = prj._gmake.varlist or {} + prj._gmake.varlistlength = prj._gmake.varlistlength or 0 + local cache = prj._gmake.varlist + local length = prj._gmake.varlistlength + + local key = value .. saltValue + + if (cache[key] ~= nil) then + return cache[key], false + end + + local var = string.format("PERFILE_FLAGS_%d", length) + cache[key] = var + + prj._gmake.varlistlength = length + 1 + + return var, true + end + + function cpp.perFileFlags(cfg, fcfg) + local toolset = gmake2.getToolSet(cfg) + + local value = gmake2.list(table.join(toolset.getcflags(fcfg), fcfg.buildoptions)) + + if fcfg.defines or fcfg.undefines then + local defs = table.join(toolset.getdefines(fcfg.defines, cfg), toolset.getundefines(fcfg.undefines)) + if #defs > 0 then + value = value .. gmake2.list(defs) + end + end + + if fcfg.includedirs or fcfg.sysincludedirs then + local includes = toolset.getincludedirs(cfg, fcfg.includedirs, fcfg.sysincludedirs) + if #includes > 0 then + value = value .. gmake2.list(includes) + end + end + + if #value > 0 then + local newPerFileFlag = false + fcfg.flagsVariable, newPerFileFlag = makeVarName(cfg.project, value, iif(path.iscfile(fcfg.name), '_C', '_CPP')) + if newPerFileFlag then + if path.iscfile(fcfg.name) then + _p('%s = $(ALL_CFLAGS)%s', fcfg.flagsVariable, value) + else + _p('%s = $(ALL_CXXFLAGS)%s', fcfg.flagsVariable, value) + end + end + end + end + + function cpp.fileFlags(cfg, file) + local fcfg = fileconfig.getconfig(file, cfg) + if fcfg and fcfg.flagsVariable then + return fcfg.flagsVariable + end + + if path.iscfile(file.name) then + return 'ALL_CFLAGS' + else + return 'ALL_CXXFLAGS' + end + end + +-- +-- Write out the file sets. +-- + + cpp.elements.filesets = function(cfg) + local result = {} + for _, kind in ipairs(cfg._gmake.kinds) do + for _, f in ipairs(cfg._gmake.filesets[kind]) do + table.insert(result, function(cfg, toolset) + cpp.outputFileset(cfg, kind, f) + end) + end + end + return result + end + + function cpp.outputFilesSection(prj) + _p('# File sets') + _p('# #############################################') + _p('') + + for _, kind in ipairs(prj._gmake.kinds) do + _x('%s :=', kind) + end + _x('') + + gmake2.outputSection(prj, cpp.elements.filesets) + end + + function cpp.outputFileset(cfg, kind, file) + _x('%s += %s', kind, file) + end + + +-- +-- Write out the targets. +-- + + cpp.elements.rules = function(cfg) + return { + cpp.allRules, + cpp.targetRules, + gmake2.targetDirRules, + gmake2.objDirRules, + cpp.cleanRules, + gmake2.preBuildRules, + gmake2.preLinkRules, + cpp.pchRules, + } + end + + + function cpp.outputRulesSection(prj) + _p('# Rules') + _p('# #############################################') + _p('') + gmake2.outputSection(prj, cpp.elements.rules) + end + + + function cpp.allRules(cfg, toolset) + if cfg.system == p.MACOSX and cfg.kind == p.WINDOWEDAPP then + _p('all: prebuild prelink $(TARGET) $(dir $(TARGETDIR))PkgInfo $(dir $(TARGETDIR))Info.plist | $(TARGETDIR) $(OBJDIR)') + _p('\t@:') + _p('') + _p('$(dir $(TARGETDIR))PkgInfo:') + _p('$(dir $(TARGETDIR))Info.plist:') + else + _p('all: prebuild prelink $(TARGET) | $(TARGETDIR) $(OBJDIR)') + _p('\t@:') + end + _p('') + end + + + function cpp.targetRules(cfg, toolset) + local targets = '$(GCH) ' + + for _, kind in ipairs(cfg._gmake.kinds) do + if kind ~= 'OBJECTS' and kind ~= 'RESOURCES' then + targets = targets .. '$(' .. kind .. ') ' + end + end + + targets = targets .. '$(OBJECTS) $(LDDEPS)' + if cfg._gmake.filesets['RESOURCES'] then + targets = targets .. ' $(RESOURCES)' + end + + _p('$(TARGET): %s | $(TARGETDIR)', targets) + _p('\t@echo Linking %s', cfg.project.name) + _p('\t$(SILENT) $(LINKCMD)') + _p('\t$(POSTBUILDCMDS)') + _p('') + end + + + function cpp.cleanRules(cfg, toolset) + _p('clean:') + _p('\t@echo Cleaning %s', cfg.project.name) + _p('ifeq (posix,$(SHELLTYPE))') + _p('\t$(SILENT) rm -f $(TARGET)') + _p('\t$(SILENT) rm -rf $(OBJDIR)') + _p('else') + _p('\t$(SILENT) if exist $(subst /,\\\\,$(TARGET)) del $(subst /,\\\\,$(TARGET))') + _p('\t$(SILENT) if exist $(subst /,\\\\,$(OBJDIR)) rmdir /s /q $(subst /,\\\\,$(OBJDIR))') + _p('endif') + _p('') + end + + + function cpp.pchRules(cfg, toolset) + _p('ifneq (,$(PCH))') + _p('$(OBJECTS): $(GCH) $(PCH) | $(OBJDIR) $(PCH_PLACEHOLDER)') + _p('$(GCH): $(PCH) | $(OBJDIR)') + _p('\t@echo $(notdir $<)') + local cmd = iif(p.languages.isc(cfg.language), "$(CC) -x c-header $(ALL_CFLAGS)", "$(CXX) -x c++-header $(ALL_CXXFLAGS)") + _p('\t$(SILENT) %s -o "$@" -MF "$(@:%%.gch=%%.d)" -c "$<"', cmd) + _p('$(PCH_PLACEHOLDER): $(GCH) | $(OBJDIR)') + _p('\t$(SILENT) touch "$@"') + _p('else') + _p('$(OBJECTS): | $(OBJDIR)') + _p('endif') + _p('') + end + +-- +-- Output the file compile targets. +-- + + cpp.elements.fileRules = function(cfg) + local funcs = {} + for _, fileRule in ipairs(cfg._gmake.fileRules) do + table.insert(funcs, function(cfg, toolset) + cpp.outputFileRules(cfg, fileRule) + end) + end + return funcs + end + + + function cpp.outputFileRuleSection(prj) + _p('# File Rules') + _p('# #############################################') + _p('') + gmake2.outputSection(prj, cpp.elements.fileRules) + end + + + function cpp.outputFileRules(cfg, file) + local outputs = table.concat(file.buildoutputs, ' ') + + local dependencies = p.esc(file.source) + if file.buildinputs and #file.buildinputs > 0 then + dependencies = dependencies .. " " .. table.concat(p.esc(file.buildinputs), " ") + end + + _p('%s: %s', outputs, dependencies) + + if file.buildmessage then + _p('\t@echo %s', file.buildmessage) + end + + if file.buildcommands then + local cmds = os.translateCommandsAndPaths(file.buildcommands, cfg.project.basedir, cfg.project.location) + for _, cmd in ipairs(cmds) do + if cfg.bindirs and #cfg.bindirs > 0 then + _p('\t$(SILENT) $(EXE_PATHS) %s', cmd) + else + _p('\t$(SILENT) %s', cmd) + end + end + end + end + + +--------------------------------------------------------------------------- +-- +-- Handlers for individual makefile elements +-- +--------------------------------------------------------------------------- + + + function cpp.dependencies(prj) + -- include the dependencies, built by GCC (with the -MMD flag) + _p('-include $(OBJECTS:%%.o=%%.d)') + _p('ifneq (,$(PCH))') + _p(' -include "$(PCH_PLACEHOLDER).d"') + _p('endif') + end + + + diff --git a/modules/gmake2/gmake2_csharp.lua b/modules/gmake2/gmake2_csharp.lua new file mode 100644 index 00000000..812d3b66 --- /dev/null +++ b/modules/gmake2/gmake2_csharp.lua @@ -0,0 +1,317 @@ +-- +-- gmake2_csharp.lua +-- Generate a C# project makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local gmake2 = p.modules.gmake2 + + gmake2.cs = {} + local cs = gmake2.cs + + local project = p.project + local config = p.config + local fileconfig = p.fileconfig + + +-- +-- Add namespace for element definition lists for p.callarray() +-- + + cs.elements = {} + + +-- +-- Generate a GNU make C++ project makefile, with support for the new platforms API. +-- + + cs.elements.makefile = function(prj) + return { + gmake2.header, + gmake2.phonyRules, + gmake2.csConfigs, + gmake2.csProjectConfig, + gmake2.csSources, + gmake2.csEmbedFiles, + gmake2.csCopyFiles, + gmake2.csResponseFile, + gmake2.shellType, + gmake2.csAllRules, + gmake2.csTargetRules, + gmake2.targetDirRules, + gmake2.csResponseRules, + gmake2.objDirRules, + gmake2.csCleanRules, + gmake2.preBuildRules, + gmake2.preLinkRules, + gmake2.csFileRules, + } + end + + +-- +-- Generate a GNU make C# project makefile, with support for the new platforms API. +-- + + function cs.generate(prj) + p.eol("\n") + local toolset = p.tools.dotnet + p.callArray(cs.elements.makefile, prj, toolset) + end + + +-- +-- Write out the settings for a particular configuration. +-- + + cs.elements.configuration = function(cfg) + return { + gmake2.csTools, + gmake2.target, + gmake2.objdir, + gmake2.csFlags, + gmake2.csLinkCmd, + gmake2.preBuildCmds, + gmake2.preLinkCmds, + gmake2.postBuildCmds, + gmake2.settings, + } + end + + function gmake2.csConfigs(prj, toolset) + for cfg in project.eachconfig(prj) do + _x('ifeq ($(config),%s)', cfg.shortname) + p.callArray(cs.elements.configuration, cfg, toolset) + _p('endif') + _p('') + end + end + + +-- +-- Given a .resx resource file, builds the path to corresponding .resource +-- file, matching the behavior and naming of Visual Studio. +-- + + function cs.getresourcefilename(cfg, fname) + if path.getextension(fname) == ".resx" then + local name = cfg.buildtarget.basename .. "." + local dir = path.getdirectory(fname) + if dir ~= "." then + name = name .. path.translate(dir, ".") .. "." + end + return "$(OBJDIR)/" .. p.esc(name .. path.getbasename(fname)) .. ".resources" + else + return fname + end + end + + +-- +-- Iterate and output some selection of the source code files. +-- + + function cs.listsources(prj, selector) + local tr = project.getsourcetree(prj) + p.tree.traverse(tr, { + onleaf = function(node, depth) + local value = selector(node) + if value then + _x('\t%s \\', value) + end + end + }) + end + + + + + +--------------------------------------------------------------------------- +-- +-- Handlers for individual makefile elements +-- +--------------------------------------------------------------------------- + + function gmake2.csAllRules(prj, toolset) + _p('all: $(TARGETDIR) $(OBJDIR) prebuild $(EMBEDFILES) $(COPYFILES) prelink $(TARGET)') + _p('') + end + + + function gmake2.csCleanRules(prj, toolset) + --[[ + -- porting from 4.x + _p('clean:') + _p('\t@echo Cleaning %s', prj.name) + _p('ifeq (posix,$(SHELLTYPE))') + _p('\t$(SILENT) rm -f $(TARGETDIR)/%s.* $(COPYFILES)', target.basename) + _p('\t$(SILENT) rm -rf $(OBJDIR)') + _p('else') + _p('\t$(SILENT) if exist $(subst /,\\\\,$(TARGETDIR)/%s) del $(subst /,\\\\,$(TARGETDIR)/%s.*)', target.name, target.basename) + for target, source in pairs(cfgpairs[anycfg]) do + _p('\t$(SILENT) if exist $(subst /,\\\\,%s) del $(subst /,\\\\,%s)', target, target) + end + for target, source in pairs(copypairs) do + _p('\t$(SILENT) if exist $(subst /,\\\\,%s) del $(subst /,\\\\,%s)', target, target) + end + _p('\t$(SILENT) if exist $(subst /,\\\\,$(OBJDIR)) rmdir /s /q $(subst /,\\\\,$(OBJDIR))') + _p('endif') + _p('') + --]] + end + + + function gmake2.csCopyFiles(prj, toolset) + --[[ + -- copied from 4.x; needs more porting + _p('COPYFILES += \\') + for target, source in pairs(cfgpairs[anycfg]) do + _p('\t%s \\', target) + end + for target, source in pairs(copypairs) do + _p('\t%s \\', target) + end + _p('') + --]] + end + + + function cs.getresponsefilename(prj) + return '$(OBJDIR)/' .. prj.filename .. '.rsp' + end + + + function gmake2.csResponseFile(prj, toolset) + _x('RESPONSE += ' .. gmake2.cs.getresponsefilename(prj)) + end + + + function gmake2.csResponseRules(prj) + local toolset = p.tools.dotnet + local ext = gmake2.getmakefilename(prj, true) + local makefile = path.getname(p.filename(prj, ext)) + local response = gmake2.cs.getresponsefilename(prj) + + _p('$(RESPONSE): %s', makefile) + _p('\t@echo Generating response file', prj.name) + + _p('ifeq (posix,$(SHELLTYPE))') + _x('\t$(SILENT) rm -f $(RESPONSE)') + _p('else') + _x('\t$(SILENT) if exist $(RESPONSE) del %s', path.translate(response, '\\')) + _p('endif') + + local sep = os.istarget("windows") and "\\" or "/" + local tr = project.getsourcetree(prj) + p.tree.traverse(tr, { + onleaf = function(node, depth) + if toolset.fileinfo(node).action == "Compile" then + _x('\t@echo %s >> $(RESPONSE)', path.translate(node.relpath, sep)) + end + end + }) + _p('') + end + + + function gmake2.csEmbedFiles(prj, toolset) + local cfg = project.getfirstconfig(prj) + + _p('EMBEDFILES += \\') + cs.listsources(prj, function(node) + local fcfg = fileconfig.getconfig(node, cfg) + local info = toolset.fileinfo(fcfg) + if info.action == "EmbeddedResource" then + return cs.getresourcefilename(cfg, node.relpath) + end + end) + _p('') + end + + + function gmake2.csFileRules(prj, toolset) + --[[ + -- porting from 4.x + _p('# Per-configuration copied file rules') + for cfg in p.eachconfig(prj) do + _x('ifneq (,$(findstring %s,$(config)))', cfg.name:lower()) + for target, source in pairs(cfgpairs[cfg]) do + p.make_copyrule(source, target) + end + _p('endif') + _p('') + end + + _p('# Copied file rules') + for target, source in pairs(copypairs) do + p.make_copyrule(source, target) + end + + _p('# Embedded file rules') + for _, fname in ipairs(embedded) do + if path.getextension(fname) == ".resx" then + _x('%s: %s', getresourcefilename(prj, fname), fname) + _p('\t$(SILENT) $(RESGEN) $^ $@') + end + _p('') + end + --]] + end + + + function gmake2.csFlags(cfg, toolset) + _p(' FLAGS =%s', gmake2.list(toolset.getflags(cfg))) + end + + + function gmake2.csLinkCmd(cfg, toolset) + local deps = p.esc(config.getlinks(cfg, "dependencies", "fullpath")) + _p(' DEPENDS =%s', gmake2.list(deps)) + _p(' REFERENCES = %s', table.implode(deps, "/r:", "", " ")) + end + + + function gmake2.csProjectConfig(prj, toolset) + -- To maintain compatibility with Visual Studio, these values must + -- be set on the project level, and not per-configuration. + local cfg = project.getfirstconfig(prj) + + local kindflag = "/t:" .. toolset.getkind(cfg):lower() + local libdirs = table.implode(p.esc(cfg.libdirs), "/lib:", "", " ") + _p('FLAGS += %s', table.concat(table.join(kindflag, libdirs), " ")) + + local refs = p.esc(config.getlinks(cfg, "system", "fullpath")) + _p('REFERENCES += %s', table.implode(refs, "/r:", "", " ")) + _p('') + end + + + function gmake2.csSources(prj, toolset) + local cfg = project.getfirstconfig(prj) + + _p('SOURCES += \\') + cs.listsources(prj, function(node) + local fcfg = fileconfig.getconfig(node, cfg) + local info = toolset.fileinfo(fcfg) + if info.action == "Compile" then + return node.relpath + end + end) + _p('') + end + + + function gmake2.csTargetRules(prj, toolset) + _p('$(TARGET): $(SOURCES) $(EMBEDFILES) $(DEPENDS) $(RESPONSE)') + _p('\t$(SILENT) $(CSC) /nologo /out:$@ $(FLAGS) $(REFERENCES) @$(RESPONSE) $(patsubst %%,/resource:%%,$(EMBEDFILES))') + _p('\t$(POSTBUILDCMDS)') + _p('') + end + + + function gmake2.csTools(cfg, toolset) + _p(' CSC = %s', toolset.gettoolname(cfg, "csc")) + _p(' RESGEN = %s', toolset.gettoolname(cfg, "resgen")) + end diff --git a/modules/gmake2/gmake2_makefile.lua b/modules/gmake2/gmake2_makefile.lua new file mode 100644 index 00000000..f6f53e9e --- /dev/null +++ b/modules/gmake2/gmake2_makefile.lua @@ -0,0 +1,98 @@ +-- +-- gmake2_makefile.lua +-- Generate a C/C++ project makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local gmake2 = p.modules.gmake2 + + gmake2.makefile = {} + local makefile = gmake2.makefile + + local project = p.project + local config = p.config + local fileconfig = p.fileconfig + +--- +-- Add namespace for element definition lists for p.callarray() +--- + makefile.elements = {} + +-- +-- Generate a GNU make makefile project makefile. +-- + + makefile.elements.makefile = function(prj) + return { + gmake2.header, + gmake2.phonyRules, + makefile.configs, + makefile.targetRules + } + end + + function makefile.generate(prj) + p.eol("\n") + p.callarray(make, makefile.elements.makefile, prj) + end + + + makefile.elements.configuration = function(cfg) + return { + gmake2.target, + gmake2.buildCommands, + gmake2.cleanCommands, + } + end + + function makefile.configs(prj) + for cfg in project.eachconfig(prj) do + -- identify the toolset used by this configurations (would be nicer if + -- this were computed and stored with the configuration up front) + + local toolset = p.tools[cfg.toolset or "gcc"] + if not toolset then + error("Invalid toolset '" .. cfg.toolset .. "'") + end + + _x('ifeq ($(config),%s)', cfg.shortname) + p.callarray(make, makefile.elements.configuration, cfg, toolset) + _p('endif') + _p('') + end + end + + function makefile.targetRules(prj) + _p('$(TARGET):') + _p('\t$(BUILDCMDS)') + _p('') + _p('clean:') + _p('\t$(CLEANCMDS)') + _p('') + end + + + function gmake2.buildCommands(cfg) + _p(' define BUILDCMDS') + local steps = cfg.buildcommands + if #steps > 0 then + steps = os.translateCommandsAndPaths(steps, cfg.project.basedir, cfg.project.location) + _p('\t@echo Running build commands') + _p('\t%s', table.implode(steps, "", "", "\n\t")) + end + _p(' endef') + end + + + function gmake2.cleanCommands(cfg) + _p(' define CLEANCMDS') + local steps = cfg.cleancommands + if #steps > 0 then + steps = os.translateCommandsAndPaths(steps, cfg.project.basedir, cfg.project.location) + _p('\t@echo Running clean commands') + _p('\t%s', table.implode(steps, "", "", "\n\t")) + end + _p(' endef') + end + diff --git a/modules/gmake2/gmake2_utility.lua b/modules/gmake2/gmake2_utility.lua new file mode 100644 index 00000000..8cf430cb --- /dev/null +++ b/modules/gmake2/gmake2_utility.lua @@ -0,0 +1,380 @@ +-- +-- gmake2_utility.lua +-- Generate a C/C++ project makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local gmake2 = p.modules.gmake2 + + gmake2.utility = {} + local utility = gmake2.utility + + local project = p.project + local config = p.config + local fileconfig = p.fileconfig + +--- +-- Add namespace for element definition lists for premake.callarray() +--- + + utility.elements = {} + + +-- +-- Generate a GNU make utility project makefile +-- + + utility.elements.makefile = function(prj) + return { + gmake2.header, + gmake2.phonyRules, + gmake2.shellType, + utility.initialize, + utility.createFileTable, + utility.outputFilesSection, + utility.outputRulesSection, + utility.outputFileRuleSection, + } + end + + + function utility.generate(prj) + p.eol("\n") + p.callArray(utility.elements.makefile, prj) + + -- allow the garbage collector to clean things up. + for cfg in project.eachconfig(prj) do + cfg._gmake = nil + end + prj._gmake = nil + + end + + + function utility.initialize(prj) + prj._gmake = prj._gmake or {} + prj._gmake.rules = prj.rules + prj._gmake.filesets = { } + end + + + function utility.createFileTable(prj) + for cfg in project.eachconfig(prj) do + cfg._gmake = cfg._gmake or {} + cfg._gmake.filesets = {} + cfg._gmake.fileRules = {} + + local files = table.shallowcopy(prj._.files) + table.foreachi(files, function(node) + utility.addFile(cfg, node, prj) + end) + + for _, f in pairs(cfg._gmake.filesets) do + table.sort(f) + end + + cfg._gmake.kinds = table.keys(cfg._gmake.filesets) + table.sort(cfg._gmake.kinds) + + prj._gmake.kinds = table.join(prj._gmake.kinds or {}, cfg._gmake.kinds) + end + + prj._gmake.kinds = table.unique(prj._gmake.kinds) + table.sort(prj._gmake.kinds) + end + + + function utility.addFile(cfg, node, prj) + local filecfg = fileconfig.getconfig(node, cfg) + if not filecfg or filecfg.flags.ExcludeFromBuild then + return + end + + -- skip generated files, since we try to figure it out manually below. + if node.generated then + return + end + + -- process custom build commands. + if fileconfig.hasCustomBuildRule(filecfg) then + local env = table.shallowcopy(filecfg.environ) + env.PathVars = { + ["file.basename"] = { absolute = false, token = node.basename }, + ["file.abspath"] = { absolute = true, token = node.abspath }, + ["file.relpath"] = { absolute = false, token = node.relpath }, + ["file.name"] = { absolute = false, token = node.name }, + ["file.path"] = { absolute = true, token = node.path }, + } + + local shadowContext = p.context.extent(filecfg, env) + + local buildoutputs = p.project.getrelative(cfg.project, shadowContext.buildoutputs) + if buildoutputs and #buildoutputs > 0 then + local file = { + buildoutputs = buildoutputs, + source = node.relpath, + buildmessage = shadowContext.buildmessage, + buildcommands = shadowContext.buildcommands, + buildinputs = p.project.getrelative(cfg.project, shadowContext.buildinputs) + } + table.insert(cfg._gmake.fileRules, file) + + for _, output in ipairs(buildoutputs) do + utility.addGeneratedFile(cfg, node, output) + end + end + else + utility.addRuleFile(cfg, node) + end + end + + + function utility.addGeneratedFile(cfg, source, filename) + -- mark that we have generated files. + cfg.project.hasGeneratedFiles = true + + -- add generated file to the project. + local files = cfg.project._.files + local node = files[filename] + if not node then + node = fileconfig.new(filename, cfg.project) + files[filename] = node + table.insert(files, node) + end + + -- always overwrite the dependency information. + node.dependsOn = source + node.generated = true + + -- add to config if not already added. + if not fileconfig.getconfig(node, cfg) then + fileconfig.addconfig(node, cfg) + end + + -- add file to the fileset. + local filesets = cfg.project._gmake.filesets + local kind = "CUSTOM" + + local fileset = cfg._gmake.filesets[kind] or {} + table.insert(fileset, filename) + cfg._gmake.filesets[kind] = fileset + + -- recursively setup rules. + utility.addRuleFile(cfg, node) + end + + + function utility.addRuleFile(cfg, node) + local rules = cfg.project._gmake.rules + local rule = rules[path.getextension(node.abspath):lower()] + if rule then + + local filecfg = fileconfig.getconfig(node, cfg) + local environ = table.shallowcopy(filecfg.environ) + + if rule.propertydefinition then + p.rule.prepareEnvironment(rule, environ, "$(%s)") + end + + local shadowContext = p.context.extent(rule, environ) + + local buildoutputs = shadowContext.buildoutputs + local buildmessage = shadowContext.buildmessage + local buildcommands = shadowContext.buildcommands + local buildinputs = shadowContext.buildinputs + + buildoutputs = p.project.getrelative(cfg.project, buildoutputs) + if buildoutputs and #buildoutputs > 0 then + local file = { + buildoutputs = buildoutputs, + source = node.relpath, + buildmessage = buildmessage, + buildcommands = buildcommands, + buildinputs = buildinputs + } + table.insert(cfg._gmake.fileRules, file) + + for _, output in ipairs(buildoutputs) do + utility.addGeneratedFile(cfg, node, output) + end + end + end + end + + +-- +-- Write out the settings for a particular configuration. +-- + + utility.elements.configuration = function(cfg) + return { + utility.ruleProperties, + gmake2.settings, + gmake2.preBuildCmds, + gmake2.preLinkCmds, + gmake2.postBuildCmds, + } + end + + + function utility.ruleProperties(cfg, toolset) + for i = 1, #cfg.rules do + local rule = p.global.getRule(cfg.rules[i]) + + for prop in p.rule.eachProperty(rule) do + local fld = p.rule.getPropertyField(rule, prop) + local value = cfg[fld.name] + if value ~= nil then + if fld.kind == "path" then + value = gmake2.path(cfg, value) + elseif fld.kind == "list:path" then + value = gmake2.path(cfg, value) + end + + value = p.rule.expandString(rule, prop, value) + if value ~= nil and #value > 0 then + p.outln(prop.name .. ' = ' .. p.esc(value)) + end + end + end + end + end + + + +-- +-- Write out the file sets. +-- + + utility.elements.filesets = function(cfg) + local result = {} + for _, kind in ipairs(cfg._gmake.kinds) do + for _, f in ipairs(cfg._gmake.filesets[kind]) do + table.insert(result, function(cfg, toolset) + utility.outputFileset(cfg, kind, f) + end) + end + end + return result + end + + + function utility.outputFilesSection(prj) + _p('# File sets') + _p('# #############################################') + _p('') + + for _, kind in ipairs(prj._gmake.kinds) do + _x('%s :=', kind) + end + _x('') + + gmake2.outputSection(prj, utility.elements.filesets) + end + + + function utility.outputFileset(cfg, kind, file) + _x('%s += %s', kind, file) + end + + +-- +-- Write out the targets. +-- + + utility.elements.rules = function(cfg) + return { + utility.allRules, + utility.targetRules, + gmake2.targetDirRules, + utility.cleanRules, + gmake2.preBuildRules, + gmake2.preLinkRules, + } + end + + + function utility.outputRulesSection(prj) + _p('# Rules') + _p('# #############################################') + _p('') + gmake2.outputSection(prj, utility.elements.rules) + end + + + function utility.allRules(cfg, toolset) + local allTargets = 'all: $(TARGETDIR) prebuild prelink $(TARGET)' + for _, kind in ipairs(cfg._gmake.kinds) do + allTargets = allTargets .. ' $(' .. kind .. ')' + end + _p(allTargets) + _p('\t@:') + _p('') + end + + + function utility.targetRules(cfg, toolset) + local targets = '' + + for _, kind in ipairs(cfg._gmake.kinds) do + targets = targets .. '$(' .. kind .. ') ' + end + + _p('$(TARGET): %s', targets) + _p('\t$(POSTBUILDCMDS)') + _p('') + end + + + function utility.cleanRules(cfg, toolset) + _p('clean:') + _p('\t@echo Cleaning %s', cfg.project.name) + _p('') + end + + +-- +-- Output the file compile targets. +-- + + utility.elements.fileRules = function(cfg) + local funcs = {} + for _, fileRule in ipairs(cfg._gmake.fileRules) do + table.insert(funcs, function(cfg, toolset) + utility.outputFileRules(cfg, fileRule) + end) + end + return funcs + end + + + function utility.outputFileRuleSection(prj) + _p('# File Rules') + _p('# #############################################') + _p('') + gmake2.outputSection(prj, utility.elements.fileRules) + end + + + function utility.outputFileRules(cfg, file) + local outputs = table.concat(file.buildoutputs, ' ') + + local dependencies = file.source + if file.buildinputs and #file.buildinputs > 0 then + dependencies = dependencies .. " " .. table.concat(p.esc(file.buildinputs), " ") + end + + _p('%s: %s', outputs, dependencies) + + if file.buildmessage then + _p('\t@echo %s', file.buildmessage) + end + + if file.buildcommands then + local cmds = os.translateCommandsAndPaths(file.buildcommands, cfg.project.basedir, cfg.project.location) + for _, cmd in ipairs(cmds) do + _p('\t$(SILENT) %s', cmd) + end + end + end diff --git a/modules/gmake2/gmake2_workspace.lua b/modules/gmake2/gmake2_workspace.lua new file mode 100644 index 00000000..7b310a09 --- /dev/null +++ b/modules/gmake2/gmake2_workspace.lua @@ -0,0 +1,188 @@ +-- +-- gmake2_workspace.lua +-- Generate a workspace-level makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local gmake2 = p.modules.gmake2 + + local tree = p.tree + local project = p.project + +-- +-- Generate a GNU make "workspace" makefile, with support for the new platforms API. +-- + + function gmake2.generate_workspace(wks) + p.eol("\n") + + gmake2.header(wks) + + gmake2.configmap(wks) + gmake2.projects(wks) + + gmake2.workspacePhonyRule(wks) + gmake2.groupRules(wks) + + gmake2.projectrules(wks) + gmake2.cleanrules(wks) + gmake2.helprule(wks) + end + + +-- +-- Write out the workspace's configuration map, which maps workspace +-- level configurations to the project level equivalents. +-- + + function gmake2.configmap(wks) + for cfg in p.workspace.eachconfig(wks) do + _p('ifeq ($(config),%s)', cfg.shortname) + for prj in p.workspace.eachproject(wks) do + local prjcfg = project.getconfig(prj, cfg.buildcfg, cfg.platform) + if prjcfg then + _p(' %s_config = %s', gmake2.tovar(prj.name), prjcfg.shortname) + end + end + _p('endif') + end + _p('') + end + + +-- +-- Write out the rules for the `make clean` action. +-- + + function gmake2.cleanrules(wks) + _p('clean:') + for prj in p.workspace.eachproject(wks) do + local prjpath = p.filename(prj, gmake2.getmakefilename(prj, true)) + local prjdir = path.getdirectory(path.getrelative(wks.location, prjpath)) + local prjname = path.getname(prjpath) + _x(1,'@${MAKE} --no-print-directory -C %s -f %s clean', prjdir, prjname) + end + _p('') + end + + +-- +-- Write out the make file help rule and configurations list. +-- + + function gmake2.helprule(wks) + _p('help:') + _p(1,'@echo "Usage: make [config=name] [target]"') + _p(1,'@echo ""') + _p(1,'@echo "CONFIGURATIONS:"') + + for cfg in p.workspace.eachconfig(wks) do + _x(1, '@echo " %s"', cfg.shortname) + end + + _p(1,'@echo ""') + + _p(1,'@echo "TARGETS:"') + _p(1,'@echo " all (default)"') + _p(1,'@echo " clean"') + + for prj in p.workspace.eachproject(wks) do + _p(1,'@echo " %s"', prj.name) + end + + _p(1,'@echo ""') + _p(1,'@echo "For more information, see http://industriousone.com/premake/quick-start"') + end + + +-- +-- Write out the list of projects that comprise the workspace. +-- + + function gmake2.projects(wks) + _p('PROJECTS := %s', table.concat(p.esc(table.extract(wks.projects, "name")), " ")) + _p('') + end + +-- +-- Write out the workspace PHONY rule +-- + + function gmake2.workspacePhonyRule(wks) + local groups = {} + local tr = p.workspace.grouptree(wks) + tree.traverse(tr, { + onbranch = function(n) + table.insert(groups, n.path) + end + }) + + _p('.PHONY: all clean help $(PROJECTS) ' .. table.implode(groups, '', '', ' ')) + _p('') + _p('all: $(PROJECTS)') + _p('') + end + +-- +-- Write out the phony rules representing project groups +-- + function gmake2.groupRules(wks) + -- Transform workspace groups into target aggregate + local tr = p.workspace.grouptree(wks) + tree.traverse(tr, { + onbranch = function(n) + local rule = n.path .. ":" + local projectTargets = {} + local groupTargets = {} + for i, c in pairs(n.children) + do + if type(i) == "string" + then + if c.project + then + table.insert(projectTargets, c.name) + else + table.insert(groupTargets, c.path) + end + end + end + if #groupTargets > 0 then + table.sort(groupTargets) + rule = rule .. " " .. table.concat(groupTargets, " ") + end + if #projectTargets > 0 then + table.sort(projectTargets) + rule = rule .. " " .. table.concat(projectTargets, " ") + end + _p(rule) + _p('') + end + }) + end + +-- +-- Write out the rules to build each of the workspace's projects. +-- + + function gmake2.projectrules(wks) + for prj in p.workspace.eachproject(wks) do + local deps = project.getdependencies(prj) + deps = table.extract(deps, "name") + _p('%s:%s', p.esc(prj.name), gmake2.list(deps)) + + local cfgvar = gmake2.tovar(prj.name) + _p('ifneq (,$(%s_config))', cfgvar) + + _p(1,'@echo "==== Building %s ($(%s_config)) ===="', prj.name, cfgvar) + + local prjpath = p.filename(prj, gmake2.getmakefilename(prj, true)) + local prjdir = path.getdirectory(path.getrelative(wks.location, prjpath)) + local prjname = path.getname(prjpath) + + _x(1,'@${MAKE} --no-print-directory -C %s -f %s config=$(%s_config)', prjdir, prjname, cfgvar) + + _p('endif') + _p('') + end + end diff --git a/modules/gmake2/tests/_tests.lua b/modules/gmake2/tests/_tests.lua new file mode 100644 index 00000000..f8e86134 --- /dev/null +++ b/modules/gmake2/tests/_tests.lua @@ -0,0 +1,13 @@ +require ("gmake2") + +return { + "test_gmake2_clang.lua", + "test_gmake2_file_rules.lua", + "test_gmake2_flags.lua", + "test_gmake2_ldflags.lua", + "test_gmake2_linking.lua", + "test_gmake2_objects.lua", + "test_gmake2_pch.lua", + "test_gmake2_target_rules.lua", + "test_gmake2_tools.lua", +} diff --git a/modules/gmake2/tests/test_gmake2_clang.lua b/modules/gmake2/tests/test_gmake2_clang.lua new file mode 100644 index 00000000..4c66d368 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_clang.lua @@ -0,0 +1,46 @@ +-- +-- test_gmake2_clang.lua +-- Test Clang support in Makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_clang") + + local p = premake + local gmake2 = p.modules.gmake2 + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + wks = test.createWorkspace() + toolset "clang" + prj = p.workspace.getproject(wks, 1) + end + + +-- +-- Make sure that the correct compilers are used. +-- + + function suite.usesCorrectCompilers() + gmake2.cpp.outputConfigurationSection(prj) + test.capture [[ +# Configurations +# ############################################# + +ifeq ($(origin CC), default) + CC = clang +endif +ifeq ($(origin CXX), default) + CXX = clang++ +endif +ifeq ($(origin AR), default) + AR = ar +endif +]] + end + diff --git a/modules/gmake2/tests/test_gmake2_file_rules.lua b/modules/gmake2/tests/test_gmake2_file_rules.lua new file mode 100644 index 00000000..947cd859 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_file_rules.lua @@ -0,0 +1,141 @@ +-- +-- test_gmake2_file_rules.lua +-- Validate the makefile source building rules. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_file_rules") + + local p = premake + local gmake2 = p.modules.gmake2 + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + p.escaper(gmake2.esc) + gmake2.cpp.initialize() + wks = test.createWorkspace() + end + + local function prepare() + prj = p.workspace.getproject(wks, 1) + p.oven.bake() + + gmake2.cpp.createRuleTable(prj) + gmake2.cpp.createFileTable(prj) + gmake2.cpp.outputFileRuleSection(prj) + end + + +-- +-- Two files with the same base name should have different object files. +-- + + function suite.uniqueObjNames_onBaseNameCollision() + files { "src/hello.cpp", "src/greetings/hello.cpp" } + prepare() + test.capture [[ +# File Rules +# ############################################# + +$(OBJDIR)/hello.o: src/greetings/hello.cpp + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/hello1.o: src/hello.cpp + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" + + ]] + end + + +-- +-- C files in C++ projects should been compiled as c +-- + + function suite.cFilesGetsCompiledWithCCWhileInCppProject() + files { "src/hello.c", "src/test.cpp" } + prepare() + test.capture [[ +# File Rules +# ############################################# + +$(OBJDIR)/hello.o: src/hello.c + @echo $(notdir $<) + $(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/test.o: src/test.cpp + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" + + ]] + end + + +-- +-- If a custom build rule is supplied, it should be used. +-- + + function suite.customBuildRule() + files { "hello.x" } + filter "files:**.x" + buildmessage "Compiling %{file.name}" + buildcommands { + 'cxc -c "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.xo"', + 'c2o -c "%{cfg.objdir}/%{file.basename}.xo" -o "%{cfg.objdir}/%{file.basename}.obj"' + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.obj" } + prepare() + test.capture [[ +# File Rules +# ############################################# + +ifeq ($(config),debug) +obj/Debug/hello.obj: hello.x + @echo Compiling hello.x + $(SILENT) cxc -c "hello.x" -o "obj/Debug/hello.xo" + $(SILENT) c2o -c "obj/Debug/hello.xo" -o "obj/Debug/hello.obj" +endif + +ifeq ($(config),release) +obj/Release/hello.obj: hello.x + @echo Compiling hello.x + $(SILENT) cxc -c "hello.x" -o "obj/Release/hello.xo" + $(SILENT) c2o -c "obj/Release/hello.xo" -o "obj/Release/hello.obj" +endif + ]] + end + + function suite.customBuildRuleWithAdditionalInputs() + files { "hello.x" } + filter "files:**.x" + buildmessage "Compiling %{file.name}" + buildcommands { + 'cxc -c "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.xo"', + 'c2o -c "%{cfg.objdir}/%{file.basename}.xo" -o "%{cfg.objdir}/%{file.basename}.obj"' + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.obj" } + buildinputs { "%{file.path}.inc", "%{file.path}.inc2" } + prepare() + test.capture [[ +# File Rules +# ############################################# + +ifeq ($(config),debug) +obj/Debug/hello.obj: hello.x hello.x.inc hello.x.inc2 + @echo Compiling hello.x + $(SILENT) cxc -c "hello.x" -o "obj/Debug/hello.xo" + $(SILENT) c2o -c "obj/Debug/hello.xo" -o "obj/Debug/hello.obj" +endif + +ifeq ($(config),release) +obj/Release/hello.obj: hello.x hello.x.inc hello.x.inc2 + @echo Compiling hello.x + $(SILENT) cxc -c "hello.x" -o "obj/Release/hello.xo" + $(SILENT) c2o -c "obj/Release/hello.xo" -o "obj/Release/hello.obj" +endif + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_flags.lua b/modules/gmake2/tests/test_gmake2_flags.lua new file mode 100644 index 00000000..00cf14b5 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_flags.lua @@ -0,0 +1,42 @@ +-- +-- test_gmake2_flags.lua +-- Tests compiler and linker flags for Makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_flags") + + local p = premake + local gmake2 = p.modules.gmake2 + + local project = p.project + + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + wks, prj = test.createWorkspace() + end + + local function prepare(calls) + local cfg = test.getconfig(prj, "Debug") + local toolset = p.tools.gcc + p.callarray(gmake2.cpp, calls, cfg, toolset) + end + + +-- +-- Include directories should be relative and space separated. +-- + + function suite.includeDirs() + includedirs { "src/include", "../include" } + prepare { "includes" } + test.capture [[ +INCLUDES += -Isrc/include -I../include + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_ldflags.lua b/modules/gmake2/tests/test_gmake2_ldflags.lua new file mode 100644 index 00000000..b3a81eb8 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_ldflags.lua @@ -0,0 +1,79 @@ +-- +-- test_gmake2_ldflags.lua +-- Tests compiler and linker flags for Makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_ldflags") + + local p = premake + local gmake2 = p.modules.gmake2 + + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + wks, prj = test.createWorkspace() + symbols "On" + end + + local function prepare(calls) + local cfg = test.getconfig(prj, "Debug") + local toolset = p.tools.gcc + gmake2.cpp.ldFlags(cfg, toolset) + end + + +-- +-- Check the output from default project values. +-- + + function suite.checkDefaultValues() + prepare() + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) + ]] + end + +-- +-- Check addition of library search directores. +-- + + function suite.checkLibDirs() + libdirs { "../libs", "libs" } + prepare() + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -L../libs -Llibs + ]] + end + + function suite.checkLibDirs_X86_64() + architecture ("x86_64") + system (p.LINUX) + prepare() + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -L/usr/lib64 -m64 + ]] + end + + function suite.checkLibDirs_X86() + architecture ("x86") + system (p.LINUX) + prepare() + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -L/usr/lib32 -m32 + ]] + end + + function suite.checkLibDirs_X86_64_MacOSX() + architecture ("x86_64") + system (p.MACOSX) + prepare() + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -m64 + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_linking.lua b/modules/gmake2/tests/test_gmake2_linking.lua new file mode 100644 index 00000000..62cb2334 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_linking.lua @@ -0,0 +1,272 @@ +-- +-- test_gmake2_linking.lua +-- Validate the link step generation for makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_linking") + + local p = premake + local gmake2 = p.modules.gmake2 + + local project = p.project + + +-- +-- Setup and teardown +-- + + local wks, prj + + function suite.setup() + _OS = "linux" + wks, prj = test.createWorkspace() + end + + local function prepare(calls) + local cfg = test.getconfig(prj, "Debug") + local toolset = p.tools.gcc + p.callarray(gmake2.cpp, calls, cfg, toolset) + end + + +-- +-- Check link command for a shared C++ library. +-- + + function suite.links_onCppSharedLib() + kind "SharedLib" + prepare { "ldFlags", "linkCmd" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -shared -Wl,-soname=libMyProject.so -s +LINKCMD = $(CXX) -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS) + ]] + end + + function suite.links_onMacOSXCppSharedLib() + _OS = "macosx" + kind "SharedLib" + prepare { "ldFlags", "linkCmd" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -dynamiclib -Wl,-install_name,@rpath/libMyProject.dylib -Wl,-x +LINKCMD = $(CXX) -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS) + ]] + end + +-- +-- Check link command for a shared C library. +-- + + function suite.links_onCSharedLib() + language "C" + kind "SharedLib" + prepare { "ldFlags", "linkCmd" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -shared -Wl,-soname=libMyProject.so -s +LINKCMD = $(CC) -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS) + ]] + end + + +-- +-- Check link command for a static library. +-- + + function suite.links_onStaticLib() + kind "StaticLib" + prepare { "ldFlags", "linkCmd" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LINKCMD = $(AR) -rcs "$@" $(OBJECTS) + ]] + end + + +-- +-- Check link command for the Utility kind. +-- +-- Utility projects should only run custom commands, and perform no linking. +-- + + function suite.links_onUtility() + kind "Utility" + prepare { "linkCmd" } + test.capture [[ +LINKCMD = + ]] + end + + +-- +-- Check link command for a Mac OS X universal static library. +-- + + function suite.links_onMacUniversalStaticLib() + architecture "universal" + kind "StaticLib" + prepare { "ldFlags", "linkCmd" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LINKCMD = libtool -o "$@" $(OBJECTS) + ]] + end + + +-- +-- Check a linking to a sibling static library. +-- + + function suite.links_onSiblingStaticLib() + links "MyProject2" + + test.createproject(wks) + kind "StaticLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LIBS += build/bin/Debug/libMyProject2.a +LDDEPS += build/bin/Debug/libMyProject2.a + ]] + end + + +-- +-- Check a linking to a sibling shared library. +-- + + function suite.links_onSiblingSharedLib() + links "MyProject2" + + test.createproject(wks) + kind "SharedLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LIBS += build/bin/Debug/libMyProject2.so +LDDEPS += build/bin/Debug/libMyProject2.so + ]] + end + +-- +-- Check a linking to a sibling shared library using -l and -L. +-- + + function suite.links_onSiblingSharedLib() + links "MyProject2" + flags { "RelativeLinks" } + + test.createproject(wks) + kind "SharedLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -Lbuild/bin/Debug -Wl,-rpath,'$$ORIGIN/../../build/bin/Debug' -s +LIBS += -lMyProject2 +LDDEPS += build/bin/Debug/libMyProject2.so + ]] + end + + function suite.links_onMacOSXSiblingSharedLib() + _OS = "macosx" + links "MyProject2" + flags { "RelativeLinks" } + + test.createproject(wks) + kind "SharedLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -Lbuild/bin/Debug -Wl,-rpath,'@loader_path/../../build/bin/Debug' -Wl,-x +LIBS += -lMyProject2 +LDDEPS += build/bin/Debug/libMyProject2.dylib + ]] + end + +-- +-- Check a linking multiple siblings. +-- + + function suite.links_onMultipleSiblingStaticLib() + links "MyProject2" + links "MyProject3" + + test.createproject(wks) + kind "StaticLib" + location "build" + + test.createproject(wks) + kind "StaticLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LIBS += build/bin/Debug/libMyProject2.a build/bin/Debug/libMyProject3.a +LDDEPS += build/bin/Debug/libMyProject2.a build/bin/Debug/libMyProject3.a + ]] + end + +-- +-- Check a linking multiple siblings with link groups enabled. +-- + + function suite.links_onSiblingStaticLibWithLinkGroups() + links "MyProject2" + links "MyProject3" + linkgroups "On" + + test.createproject(wks) + kind "StaticLib" + location "build" + + test.createproject(wks) + kind "StaticLib" + location "build" + + prepare { "ldFlags", "libs", "ldDeps" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -s +LIBS += -Wl,--start-group build/bin/Debug/libMyProject2.a build/bin/Debug/libMyProject3.a -Wl,--end-group +LDDEPS += build/bin/Debug/libMyProject2.a build/bin/Debug/libMyProject3.a + ]] + end + +-- +-- When referencing an external library via a path, the directory +-- should be added to the library search paths, and the library +-- itself included via an -l flag. +-- + + function suite.onExternalLibraryWithPath() + location "MyProject" + links { "libs/SomeLib" } + prepare { "ldFlags", "libs" } + test.capture [[ +ALL_LDFLAGS += $(LDFLAGS) -L../libs -s +LIBS += -lSomeLib + ]] + end + + + +-- +-- When referencing an external library with a period in the +-- file name make sure it appears correctly in the LIBS +-- directive. Currently the period and everything after it +-- is stripped +-- + + function suite.onExternalLibraryWithPath() + location "MyProject" + links { "libs/SomeLib-1.1" } + prepare { "libs", } + test.capture [[ +LIBS += -lSomeLib-1.1 + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_objects.lua b/modules/gmake2/tests/test_gmake2_objects.lua new file mode 100644 index 00000000..44b2d3b5 --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_objects.lua @@ -0,0 +1,286 @@ +-- +-- test_gmake2_objects.lua +-- Validate the list of objects for a makefile. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_objects") + + local p = premake + local gmake2 = p.modules.gmake2 + + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + gmake2.cpp.initialize() + wks = test.createWorkspace() + end + + local function prepare() + prj = test.getproject(wks, 1) + gmake2.cpp.createRuleTable(prj) + gmake2.cpp.createFileTable(prj) + gmake2.cpp.outputFilesSection(prj) + end + + +-- +-- If a file is listed at the project level, it should get listed in +-- the project level objects list. +-- + + function suite.listFileInProjectObjects() + files { "src/hello.cpp" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +OBJECTS += $(OBJDIR)/hello.o + + ]] + end + + +-- +-- Only buildable files should be listed. +-- + + function suite.onlyListBuildableFiles() + files { "include/gl.h", "src/hello.cpp" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +OBJECTS += $(OBJDIR)/hello.o + + ]] + end + + +-- +-- A file should only be listed in the configurations to which it belongs. +-- + + function suite.configFilesAreConditioned() + filter "Debug" + files { "src/hello_debug.cpp" } + filter "Release" + files { "src/hello_release.cpp" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +ifeq ($(config),debug) +OBJECTS += $(OBJDIR)/hello_debug.o +endif + +ifeq ($(config),release) +OBJECTS += $(OBJDIR)/hello_release.o +endif + + ]] + end + + +-- +-- Two files with the same base name should have different object files. +-- + + function suite.uniqueObjNames_onBaseNameCollision() + files { "src/hello.cpp", "src/greetings/hello.cpp" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +OBJECTS += $(OBJDIR)/hello.o +OBJECTS += $(OBJDIR)/hello1.o + + ]] + end + + +-- +-- If there's a custom rule for a non-C++ file extension, make sure that those +-- files are included in the build. +-- + + function suite.customBuildCommand_onCustomFileType() + files { "hello.lua" } + filter "files:**.lua" + buildmessage "Compiling %{file.name}" + buildcommands { + 'luac "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.luac"', + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.luac" } + prepare() + test.capture [[ +# File sets +# ############################################# + +CUSTOM := + +ifeq ($(config),debug) +CUSTOM += obj/Debug/hello.luac +endif + +ifeq ($(config),release) +CUSTOM += obj/Release/hello.luac +endif + ]] + end + + +-- +-- If a custom rule builds to an object file, include it in the +-- link automatically to match the behavior of Visual Studio +-- + + function suite.linkBuildOutputs_onNotSpecified() + files { "hello.x" } + filter "files:**.x" + buildmessage "Compiling %{file.name}" + buildcommands { + 'cxc -c "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.xo"', + 'c2o -c "%{cfg.objdir}/%{file.basename}.xo" -o "%{cfg.objdir}/%{file.basename}.obj"' + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.obj" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +ifeq ($(config),debug) +OBJECTS += obj/Debug/hello.obj +endif + +ifeq ($(config),release) +OBJECTS += obj/Release/hello.obj +endif + ]] + end + + +-- +-- Also include it in the link step if we explicitly specified so with +-- linkbuildoutputs. +-- + + function suite.linkBuildOutputs_onOn() + files { "hello.x" } + filter "files:**.x" + buildmessage "Compiling %{file.name}" + buildcommands { + 'cxc -c "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.xo"', + 'c2o -c "%{cfg.objdir}/%{file.basename}.xo" -o "%{cfg.objdir}/%{file.basename}.obj"' + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.obj" } + linkbuildoutputs "On" + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +ifeq ($(config),debug) +OBJECTS += obj/Debug/hello.obj +endif + +ifeq ($(config),release) +OBJECTS += obj/Release/hello.obj +endif + ]] + end + + +-- +-- If linkbuildoutputs says that we shouldn't include it in the link however, +-- don't do it. +-- + + function suite.linkBuildOutputs_onOff() + files { "hello.x" } + filter "files:**.x" + buildmessage "Compiling %{file.name}" + buildcommands { + 'cxc -c "%{file.path}" -o "%{cfg.objdir}/%{file.basename}.xo"', + 'c2o -c "%{cfg.objdir}/%{file.basename}.xo" -o "%{cfg.objdir}/%{file.basename}.obj"' + } + buildoutputs { "%{cfg.objdir}/%{file.basename}.obj" } + linkbuildoutputs "Off" + prepare() + test.capture [[ +# File sets +# ############################################# + +CUSTOM := + +ifeq ($(config),debug) +CUSTOM += obj/Debug/hello.obj +endif + +ifeq ($(config),release) +CUSTOM += obj/Release/hello.obj +endif + ]] + end + + +-- +-- If a file is excluded from a configuration, it should not be listed. +-- + + function suite.excludedFromBuild_onExcludedFile() + files { "hello.cpp" } + filter "Debug" + removefiles { "hello.cpp" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +ifeq ($(config),release) +OBJECTS += $(OBJDIR)/hello.o +endif + + ]] + end + + function suite.excludedFromBuild_onExcludeFlag() + files { "hello.cpp" } + filter { "Debug", "files:hello.cpp" } + flags { "ExcludeFromBuild" } + prepare() + test.capture [[ +# File sets +# ############################################# + +OBJECTS := + +ifeq ($(config),release) +OBJECTS += $(OBJDIR)/hello.o +endif + + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_pch.lua b/modules/gmake2/tests/test_gmake2_pch.lua new file mode 100644 index 00000000..0eeec8cb --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_pch.lua @@ -0,0 +1,150 @@ +-- +-- test_gmake2_pch.lua +-- Validate the setup for precompiled headers in makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local suite = test.declare("gmake2_pch") + + local p = premake + local gmake2 = p.modules.gmake2 + + local project = p.project + + + +-- +-- Setup and teardown +-- + + local wks, prj + function suite.setup() + os.chdir(_TESTS_DIR) + wks, prj = test.createWorkspace() + end + + local function prepareVars() + local cfg = test.getconfig(prj, "Debug") + gmake2.cpp.pch(cfg) + end + + local function prepareRules() + local cfg = test.getconfig(prj, "Debug") + gmake2.cpp.pchRules(cfg.project) + end + + +-- +-- If no header has been set, nothing should be output. +-- + + function suite.noConfig_onNoHeaderSet() + prepareVars() + test.isemptycapture() + end + + +-- +-- If a header is set, but the NoPCH flag is also set, then +-- nothing should be output. +-- + + function suite.noConfig_onHeaderAndNoPCHFlag() + pchheader "include/myproject.h" + flags "NoPCH" + prepareVars() + test.isemptycapture() + end + + +-- +-- If a header is specified and the NoPCH flag is not set, then +-- the header can be used. +-- + + function suite.config_onPchEnabled() + pchheader "include/myproject.h" + prepareVars() + test.capture [[ +PCH = include/myproject.h +PCH_PLACEHOLDER = $(OBJDIR)/$(notdir $(PCH)) +GCH = $(PCH_PLACEHOLDER).gch + ]] + end + + +-- +-- The PCH can be specified relative the an includes search path. +-- + + function suite.pch_searchesIncludeDirs() + pchheader "premake.h" + includedirs { "../../../src/host" } + prepareVars() + test.capture [[ +PCH = ../../../src/host/premake.h + ]] + end + + +-- +-- Verify the format of the PCH rules block for a C++ file. +-- + + function suite.buildRules_onCpp() + pchheader "include/myproject.h" + prepareRules() + test.capture [[ +ifneq (,$(PCH)) +$(OBJECTS): $(GCH) $(PCH) | $(OBJDIR) $(PCH_PLACEHOLDER) +$(GCH): $(PCH) | $(OBJDIR) + @echo $(notdir $<) + $(SILENT) $(CXX) -x c++-header $(ALL_CXXFLAGS) -o "$@" -MF "$(@:%.gch=%.d)" -c "$<" +$(PCH_PLACEHOLDER): $(GCH) | $(OBJDIR) + $(SILENT) touch "$@" +else +$(OBJECTS): | $(OBJDIR) +endif + ]] + end + + +-- +-- Verify the format of the PCH rules block for a C file. +-- + + function suite.buildRules_onC() + language "C" + pchheader "include/myproject.h" + prepareRules() + test.capture [[ +ifneq (,$(PCH)) +$(OBJECTS): $(GCH) $(PCH) | $(OBJDIR) $(PCH_PLACEHOLDER) +$(GCH): $(PCH) | $(OBJDIR) + @echo $(notdir $<) + $(SILENT) $(CC) -x c-header $(ALL_CFLAGS) -o "$@" -MF "$(@:%.gch=%.d)" -c "$<" +$(PCH_PLACEHOLDER): $(GCH) | $(OBJDIR) + $(SILENT) touch "$@" +else +$(OBJECTS): | $(OBJDIR) +endif + ]] + end + + + + -- + -- If the header is located on one of the include file + -- search directories, it should get found automatically. + -- + + function suite.findsPCH_onIncludeDirs() + location "MyProject" + pchheader "premake.h" + includedirs { "../../../src/host" } + prepareVars() + test.capture [[ +PCH = ../../../../src/host/premake.h + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_target_rules.lua b/modules/gmake2/tests/test_gmake2_target_rules.lua new file mode 100644 index 00000000..8f28769b --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_target_rules.lua @@ -0,0 +1,60 @@ +-- +-- test_gmake2_target_rules.lua +-- Validate the makefile target building rules. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local p = premake + local suite = test.declare("gmake2_target_rules") + + local p = premake + local gmake2 = p.modules.gmake2 + + local project = p.project + + +-- +-- Setup +-- + + local wks, prj + + function suite.setup() + wks, prj = test.createWorkspace() + end + + local function prepare() + local cfg = test.getconfig(prj, "Debug") + gmake2.cpp.allRules(cfg) + end + + +-- +-- Check the default, normal format of the rules. +-- + + function suite.defaultRules() + prepare() + test.capture [[ +all: prebuild prelink $(TARGET) | $(TARGETDIR) $(OBJDIR) + @: + ]] + end + + +-- +-- Check rules for an OS X Cocoa application. +-- + + function suite.osxWindowedAppRules() + system "MacOSX" + kind "WindowedApp" + prepare() + test.capture [[ +all: prebuild prelink $(TARGET) $(dir $(TARGETDIR))PkgInfo $(dir $(TARGETDIR))Info.plist | $(TARGETDIR) $(OBJDIR) + @: + +$(dir $(TARGETDIR))PkgInfo: +$(dir $(TARGETDIR))Info.plist: + ]] + end diff --git a/modules/gmake2/tests/test_gmake2_tools.lua b/modules/gmake2/tests/test_gmake2_tools.lua new file mode 100644 index 00000000..9474176d --- /dev/null +++ b/modules/gmake2/tests/test_gmake2_tools.lua @@ -0,0 +1,36 @@ +-- +-- test_gmake2_tools.lua +-- Tests for tools support in makefiles. +-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project +-- + + local suite = test.declare("gmake2_tools") + + local p = premake + local gmake2 = p.modules.gmake2 + + local project = premake.project + + +-- +-- Setup +-- + + local cfg + + function suite.setup() + local wks, prj = test.createWorkspace() + cfg = test.getconfig(prj, "Debug") + end + + +-- +-- Make sure that the correct tools are used. +-- + + function suite.usesCorrectTools() + gmake2.cpp.tools(cfg, p.tools.gcc) + test.capture [[ +RESCOMP = windres + ]] + end diff --git a/src/_modules.lua b/src/_modules.lua index f809c097..12aa402a 100644 --- a/src/_modules.lua +++ b/src/_modules.lua @@ -7,5 +7,6 @@ return { "xcode", "codelite", + "gmake2", "d", } diff --git a/src/base/rule.lua b/src/base/rule.lua index 27580589..b1ad1add 100644 --- a/src/base/rule.lua +++ b/src/base/rule.lua @@ -141,6 +141,44 @@ +--- +-- Given the value for a particular property, returns a expanded string with switches embedded. +-- +-- @param prop +-- The property definition. +-- @param value +-- The value of the property to be formatted. +-- @returns +-- A string value. +--- + + function rule.expandString(self, prop, value) + if not prop.switch then + return rule.getPropertyString(self, prop, value) + end + + -- list? + if type(value) == "table" then + return prop.switch .. table.concat(value, " " .. prop.switch) + end + + -- enum? + if prop.values then + local i = table.indexof(prop.values, value) + return prop.switch .. tostring(i) + end + + -- primitive + value = tostring(value) + if #value > 0 then + return prop.switch .. value + else + return nil + end + end + + + --- -- Set one or more rule variables in the current configuration scope. -- diff --git a/src/base/table.lua b/src/base/table.lua index 58e7d7fa..191be825 100644 --- a/src/base/table.lua +++ b/src/base/table.lua @@ -554,3 +554,30 @@ end end end + + +-- +-- Intersect two arrays and return a new array +-- + function table.intersect(a, b) + local result = {} + for _, v in ipairs(b) do + if table.indexof(a, v) then + table.insert(result, v) + end + end + return result + end + +-- +-- The difference of A and B is the set containing those elements that are in A but not in B +-- + function table.difference(a, b) + local result = {} + for _, v in ipairs(a) do + if not table.indexof(b, v) then + table.insert(result, v) + end + end + return result + end