1--
2-- gmake2.lua
3-- (c) 2016-2017 Jason Perkins, Blizzard Entertainment and the Premake project
4--
5
6	local p       = premake
7	local project = p.project
8
9	p.modules.gmake2 = {}
10	p.modules.gmake2._VERSION = p._VERSION
11	local gmake2 = p.modules.gmake2
12
13--
14-- Write out the default configuration rule for a workspace or project.
15--
16-- @param target
17--    The workspace or project object for which a makefile is being generated.
18--
19
20	function gmake2.defaultconfig(target)
21		-- find the right configuration iterator function for this object
22		local eachconfig = iif(target.project, project.eachconfig, p.workspace.eachconfig)
23		local defaultconfig = nil
24
25		-- find the right default configuration platform, grab first configuration that matches
26		if target.defaultplatform then
27			for cfg in eachconfig(target) do
28				if cfg.platform == target.defaultplatform then
29					defaultconfig = cfg
30					break
31				end
32			end
33		end
34
35		-- grab the first configuration and write the block
36		if not defaultconfig then
37			local iter = eachconfig(target)
38			defaultconfig = iter()
39		end
40
41		if defaultconfig then
42			_p('ifndef config')
43			_x('  config=%s', defaultconfig.shortname)
44			_p('endif')
45			_p('')
46		end
47	end
48
49
50---
51-- Escape a string so it can be written to a makefile.
52---
53
54	function gmake2.esc(value)
55		result = value:gsub("\\", "\\\\")
56		result = result:gsub("\"", "\\\"")
57		result = result:gsub(" ", "\\ ")
58		result = result:gsub("%(", "\\(")
59		result = result:gsub("%)", "\\)")
60
61		-- leave $(...) shell replacement sequences alone
62		result = result:gsub("$\\%((.-)\\%)", "$(%1)")
63		return result
64	end
65
66
67--
68-- Get the makefile file name for a workspace or a project. If this object is the
69-- only one writing to a location then I can use "Makefile". If more than one object
70-- writes to the same location I use name + ".make" to keep it unique.
71--
72
73	function gmake2.getmakefilename(this, searchprjs)
74		local count = 0
75		for wks in p.global.eachWorkspace() do
76			if wks.location == this.location then
77				count = count + 1
78			end
79
80			if searchprjs then
81				for _, prj in ipairs(wks.projects) do
82					if prj.location == this.location then
83						count = count + 1
84					end
85				end
86			end
87		end
88
89		if count == 1 then
90			return "Makefile"
91		else
92			return ".make"
93		end
94	end
95
96
97--
98-- Output a makefile header.
99--
100-- @param target
101--    The workspace or project object for which the makefile is being generated.
102--
103
104	function gmake2.header(target)
105		local kind = iif(target.project, "project", "workspace")
106
107		_p('# %s %s makefile autogenerated by Premake', p.action.current().shortname, kind)
108		_p('')
109
110		gmake2.defaultconfig(target)
111
112		_p('ifndef verbose')
113		_p('  SILENT = @')
114		_p('endif')
115		_p('')
116	end
117
118
119--
120-- Rules for file ops based on the shell type. Can't use defines and $@ because
121-- it screws up the escaping of spaces and parethesis (anyone know a fix?)
122--
123
124	function gmake2.mkdir(dirname)
125		_p('ifeq (posix,$(SHELLTYPE))')
126		_p('\t$(SILENT) mkdir -p %s', dirname)
127		_p('else')
128		_p('\t$(SILENT) mkdir $(subst /,\\\\,%s)', dirname)
129		_p('endif')
130	end
131
132	function gmake2.mkdirRules(dirname)
133		_p('%s:', dirname)
134		_p('\t@echo Creating %s', dirname)
135		gmake2.mkdir(dirname)
136		_p('')
137	end
138
139--
140-- Format a list of values to be safely written as part of a variable assignment.
141--
142
143	function gmake2.list(value, quoted)
144		quoted = false
145		if #value > 0 then
146			if quoted then
147				local result = ""
148				for _, v in ipairs (value) do
149					if #result then
150						result = result .. " "
151					end
152					result = result .. p.quoted(v)
153				end
154				return result
155			else
156				return " " .. table.concat(value, " ")
157			end
158		else
159			return ""
160		end
161	end
162
163
164--
165-- Convert an arbitrary string (project name) to a make variable name.
166--
167
168	function gmake2.tovar(value)
169		value = value:gsub("[ -]", "_")
170		value = value:gsub("[()]", "")
171		return value
172	end
173
174
175
176	function gmake2.path(cfg, value)
177		cfg = cfg.project or cfg
178		local dirs = path.translate(project.getrelative(cfg, value))
179
180		if type(dirs) == 'table' then
181			dirs = table.filterempty(dirs)
182		end
183
184		return dirs
185	end
186
187
188	function gmake2.getToolSet(cfg)
189		local default = iif(cfg.system == p.MACOSX, "clang", "gcc")
190		local toolset = p.tools[_OPTIONS.cc or cfg.toolset or default]
191		if not toolset then
192			error("Invalid toolset '" .. cfg.toolset .. "'")
193		end
194		return toolset
195	end
196
197
198	function gmake2.outputSection(prj, callback)
199		local root = {}
200
201		for cfg in project.eachconfig(prj) do
202			-- identify the toolset used by this configurations (would be nicer if
203			-- this were computed and stored with the configuration up front)
204
205			local toolset = gmake2.getToolSet(cfg)
206
207			local settings = {}
208			local funcs = callback(cfg)
209			for i = 1, #funcs do
210				local c = p.capture(function ()
211					funcs[i](cfg, toolset)
212				end)
213				if #c > 0 then
214					table.insert(settings, c)
215				end
216			end
217
218			if not root.settings then
219				root.settings = table.arraycopy(settings)
220			else
221				root.settings = table.intersect(root.settings, settings)
222			end
223
224			root[cfg] = settings
225		end
226
227		if #root.settings > 0 then
228			for _, v in ipairs(root.settings) do
229				p.outln(v)
230			end
231			p.outln('')
232		end
233
234		local first = true
235		for cfg in project.eachconfig(prj) do
236			local settings = table.difference(root[cfg], root.settings)
237			if #settings > 0 then
238				if first then
239					_x('ifeq ($(config),%s)', cfg.shortname)
240					first = false
241				else
242					_x('else ifeq ($(config),%s)', cfg.shortname)
243				end
244
245				for k, v in ipairs(settings) do
246					p.outln(v)
247				end
248
249				_p('')
250			end
251		end
252
253		if not first then
254			p.outln('endif')
255			p.outln('')
256		end
257	end
258
259
260	-- convert a rule property into a string
261
262	function gmake2.expandRuleString(rule, prop, value)
263		-- list?
264		if type(value) == "table" then
265			if #value > 0 then
266				if prop.switch then
267					return prop.switch .. table.concat(value, " " .. prop.switch)
268				else
269					prop.separator = prop.separator or " "
270					return table.concat(value, prop.separator)
271				end
272			else
273				return nil
274			end
275		end
276
277		-- bool just emits the switch
278		if prop.switch and type(value) == "boolean" then
279			if value then
280				return prop.switch
281			else
282				return nil
283			end
284		end
285
286		local switch = prop.switch or ""
287
288		-- enum?
289		if prop.values then
290			value = table.findKeyByValue(prop.values, value)
291			if value == nil then
292				value = ""
293			end
294		end
295
296		-- primitive
297		value = tostring(value)
298		if #value > 0 then
299			return switch .. value
300		else
301			return nil
302		end
303	end
304
305
306	function gmake2.prepareEnvironment(rule, environ, cfg)
307		for _, prop in ipairs(rule.propertydefinition) do
308			local fld = p.rule.getPropertyField(rule, prop)
309			local value = cfg[fld.name]
310			if value ~= nil then
311
312				if fld.kind == "path" then
313					value = gmake2.path(cfg, value)
314				elseif fld.kind == "list:path" then
315					value = gmake2.path(cfg, value)
316				end
317
318				value = gmake2.expandRuleString(rule, prop, value)
319				if value ~= nil and #value > 0 then
320					environ[prop.name] = p.esc(value)
321				end
322			end
323		end
324	end
325
326
327---------------------------------------------------------------------------
328--
329-- Handlers for the individual makefile elements that can be shared
330-- between the different language projects.
331--
332---------------------------------------------------------------------------
333
334	function gmake2.phonyRules(prj)
335		_p('.PHONY: clean prebuild')
336		_p('')
337	end
338
339
340	function gmake2.shellType()
341		_p('SHELLTYPE := posix')
342		_p('ifeq (.exe,$(findstring .exe,$(ComSpec)))')
343		_p('\tSHELLTYPE := msdos')
344		_p('endif')
345		_p('')
346	end
347
348
349	function gmake2.target(cfg, toolset)
350		p.outln('TARGETDIR = ' .. project.getrelative(cfg.project, cfg.buildtarget.directory))
351		p.outln('TARGET = $(TARGETDIR)/' .. cfg.buildtarget.name)
352	end
353
354
355	function gmake2.objdir(cfg, toolset)
356		p.outln('OBJDIR = ' .. project.getrelative(cfg.project, cfg.objdir))
357	end
358
359
360	function gmake2.settings(cfg, toolset)
361		if #cfg.makesettings > 0 then
362			for _, value in ipairs(cfg.makesettings) do
363				p.outln(value)
364			end
365		end
366
367		local value = toolset.getmakesettings(cfg)
368		if value then
369			p.outln(value)
370		end
371	end
372
373
374	function gmake2.buildCmds(cfg, event)
375		_p('define %sCMDS', event:upper())
376		local steps = cfg[event .. "commands"]
377		local msg = cfg[event .. "message"]
378		if #steps > 0 then
379			steps = os.translateCommandsAndPaths(steps, cfg.project.basedir, cfg.project.location)
380			msg = msg or string.format("Running %s commands", event)
381			_p('\t@echo %s', msg)
382			_p('\t%s', table.implode(steps, "", "", "\n\t"))
383		end
384		_p('endef')
385	end
386
387
388	function gmake2.preBuildCmds(cfg, toolset)
389		gmake2.buildCmds(cfg, "prebuild")
390	end
391
392
393	function gmake2.preLinkCmds(cfg, toolset)
394		gmake2.buildCmds(cfg, "prelink")
395	end
396
397
398	function gmake2.postBuildCmds(cfg, toolset)
399		gmake2.buildCmds(cfg, "postbuild")
400	end
401
402
403	function gmake2.targetDirRules(cfg, toolset)
404		gmake2.mkdirRules("$(TARGETDIR)")
405	end
406
407
408	function gmake2.objDirRules(cfg, toolset)
409		gmake2.mkdirRules("$(OBJDIR)")
410	end
411
412
413	function gmake2.preBuildRules(cfg, toolset)
414		_p('prebuild: | $(OBJDIR)')
415		_p('\t$(PREBUILDCMDS)')
416		_p('')
417	end
418
419
420
421	include("gmake2_cpp.lua")
422	include("gmake2_csharp.lua")
423	include("gmake2_makefile.lua")
424	include("gmake2_utility.lua")
425	include("gmake2_workspace.lua")
426
427	return gmake2
428