1--
2-- base/oven.lua
3--
4-- Process the workspaces, projects, and configurations that were specified
5-- by the project script, and make them suitable for use by the exporters
6-- and actions. Fills in computed values (e.g. object directories) and
7-- optimizes the layout of the data for faster fetches.
8--
9-- Copyright (c) 2002-2014 Jason Perkins and the Premake project
10--
11
12	local p = premake
13
14	p.oven = {}
15
16	local oven = p.oven
17	local context = p.context
18
19
20--
21-- These fields get special treatment, "bubbling up" from the configurations
22-- to the project. This allows you to express, for example: "use this config
23-- map if this configuration is present in the project", and saves the step
24-- of clearing the current configuration filter before creating the map.
25--
26
27	p.oven.bubbledFields = {
28		configmap = true,
29		vpaths = true
30	}
31
32
33
34---
35-- Traverses the container hierarchy built up by the project scripts and
36-- filters, merges, and munges the information based on the current runtime
37-- environment in preparation for doing work on the results, like exporting
38-- project files.
39--
40-- This call replaces the existing the container objects with their
41-- processed replacements. If you are using the provided container APIs
42-- (p.global.*, p.workspace.*, etc.) this will be transparent.
43---
44
45	function oven.bake()
46		-- reset the root _isBaked state.
47		-- this really only affects the unit-tests, since that is the only place
48		-- where multiple bakes per 'exe run' happen.
49		local root = p.api.rootContainer()
50		root._isBaked = false;
51
52		p.container.bake(root)
53	end
54
55	function oven.bakeWorkspace(wks)
56		return p.container.bake(wks)
57	end
58
59	p.alias(oven, "bakeWorkspace", "bakeSolution")
60
61
62	local function addCommonContextFilters(self)
63		context.addFilter(self, "_ACTION", _ACTION)
64		context.addFilter(self, "action", _ACTION)
65
66		self.system = self.system or os.target()
67		context.addFilter(self, "system", os.getSystemTags(self.system))
68		context.addFilter(self, "host", os.getSystemTags(os.host()))
69
70		-- Add command line options to the filtering options
71		local options = {}
72		for key, value in pairs(_OPTIONS) do
73			local term = key
74			if value ~= "" then
75				term = term .. "=" .. tostring(value)
76			end
77			table.insert(options, term)
78		end
79		context.addFilter(self, "_OPTIONS", options)
80		context.addFilter(self, "options", options)
81	end
82
83---
84-- Bakes a specific workspace object.
85---
86
87	function p.workspace.bake(self)
88		-- Add filtering terms to the context and then compile the results. These
89		-- terms describe the "operating environment"; only results contained by
90		-- configuration blocks which match these terms will be returned.
91
92		addCommonContextFilters(self)
93
94		-- Set up my token expansion environment
95
96		self.environ = {
97			wks = self,
98			sln = self,
99		}
100
101		context.compile(self)
102
103		-- Specify the workspaces's file system location; when path tokens are
104		-- expanded in workspace values, they will be made relative to this.
105
106		self.location = self.location or self.basedir
107		context.basedir(self, self.location)
108
109		-- Build a master list of configuration/platform pairs from all of the
110		-- projects contained by the workspace; I will need this when generating
111		-- workspace files in order to provide a map from workspace configurations
112		-- to project configurations.
113
114		self.configs = oven.bakeConfigs(self)
115
116		-- Now bake down all of the projects contained in the workspace, and
117		-- store that for future reference
118
119		p.container.bakeChildren(self)
120
121		-- I now have enough information to assign unique object directories
122		-- to each project configuration in the workspace.
123
124		oven.bakeObjDirs(self)
125
126		-- now we can post process the projects for 'buildoutputs' files
127		-- that have the 'compilebuildoutputs' flag
128		oven.addGeneratedFiles(self)
129	end
130
131
132	function oven.addGeneratedFiles(wks)
133
134		local function addGeneratedFile(cfg, source, filename)
135			-- mark that we have generated files.
136			cfg.project.hasGeneratedFiles = true
137
138			-- add generated file to the project.
139			local files = cfg.project._.files
140			local node = files[filename]
141			if not node then
142				node = p.fileconfig.new(filename, cfg.project)
143				files[filename] = node
144				table.insert(files, node)
145			end
146
147			-- always overwrite the dependency information.
148			node.dependsOn = source
149			node.generated = true
150
151			-- add to config if not already added.
152			if not p.fileconfig.getconfig(node, cfg) then
153				p.fileconfig.addconfig(node, cfg)
154			end
155		end
156
157		local function addFile(cfg, node)
158			local filecfg = p.fileconfig.getconfig(node, cfg)
159			if not filecfg or filecfg.flags.ExcludeFromBuild or not filecfg.compilebuildoutputs then
160				return
161			end
162
163			if p.fileconfig.hasCustomBuildRule(filecfg) then
164				local buildoutputs = filecfg.buildoutputs
165				if buildoutputs and #buildoutputs > 0 then
166					for _, output in ipairs(buildoutputs) do
167						if not path.islinkable(output) then
168							addGeneratedFile(cfg, node, output)
169						end
170					end
171				end
172			end
173		end
174
175
176		for prj in p.workspace.eachproject(wks) do
177			local files = table.shallowcopy(prj._.files)
178			for cfg in p.project.eachconfig(prj) do
179				table.foreachi(files, function(node)
180					addFile(cfg, node)
181				end)
182			end
183
184			-- generated files might screw up the object sequences.
185			if prj.hasGeneratedFiles and p.project.isnative(prj) then
186				oven.assignObjectSequences(prj)
187			end
188		end
189	end
190
191
192	function p.project.bake(self)
193		verbosef('    Baking %s...', self.name)
194
195		self.solution = self.workspace
196		self.global = self.workspace.global
197
198		local wks = self.workspace
199
200		-- Add filtering terms to the context to make it as specific as I can.
201		-- Start with the same filtering that was applied at the workspace level.
202
203		context.copyFilters(self, wks)
204
205		-- Now filter on the current system and architecture, allowing the
206		-- values that might already in the context to override my defaults.
207
208		self.system = self.system or os.target()
209		context.addFilter(self, "system", os.getSystemTags(self.system))
210		context.addFilter(self, "host", os.getSystemTags(os.host()))
211		context.addFilter(self, "architecture", self.architecture)
212		context.addFilter(self, "tags", self.tags)
213
214		-- The kind is a configuration level value, but if it has been set at the
215		-- project level allow that to influence the other project-level results.
216
217		context.addFilter(self, "kind", self.kind)
218
219		-- Allow the project object to also be treated like a configuration
220
221		self.project = self
222
223		-- Populate the token expansion environment
224
225		self.environ = {
226			wks = wks,
227			sln = wks,
228			prj = self,
229		}
230
231		-- Go ahead and distill all of that down now; this is my new project object
232
233		context.compile(self)
234
235		p.container.bakeChildren(self)
236
237		-- Set the context's base directory to the project's file system
238		-- location. Any path tokens which are expanded in non-path fields
239		-- are made relative to this, ensuring a portable generated project.
240
241		self.location = self.location or self.basedir
242		context.basedir(self, self.location)
243
244		-- This bit could use some work: create a canonical set of configurations
245		-- for the project, along with a mapping from the workspace's configurations.
246		-- This works, but it could probably be simplified.
247
248		local cfgs = table.fold(self.configurations or {}, self.platforms or {})
249		oven.bubbleFields(self, self, cfgs)
250		self._cfglist = oven.bakeConfigList(self, cfgs)
251
252		-- Don't allow a project-level system setting to influence the configurations
253
254		local projectSystem = self.system
255		self.system = nil
256
257		-- Finally, step through the list of configurations I built above and
258		-- bake all of those down into configuration contexts as well. Store
259		-- the results with the project.
260
261		self.configs = {}
262
263		for _, pairing in ipairs(self._cfglist) do
264			local buildcfg = pairing[1]
265			local platform = pairing[2]
266			local cfg = oven.bakeConfig(wks, self, buildcfg, platform)
267
268			if p.action.supportsconfig(p.action.current(), cfg) then
269				self.configs[(buildcfg or "*") .. (platform or "")] = cfg
270			end
271		end
272
273		-- Process the sub-objects that are contained by this project. The
274		-- configuration build stuff above really belongs in here now.
275
276		self._ = {}
277		self._.files = oven.bakeFiles(self)
278
279		-- If this type of project generates object files, look for files that will
280		-- generate object name collisions (i.e. src/hello.cpp and tests/hello.cpp
281		-- both create hello.o) and assign unique sequence numbers to each. I need
282		-- to do this up front to make sure the sequence numbers are the same for
283		-- all the tools, even they reorder the source file list.
284
285		if p.project.isnative(self) then
286			oven.assignObjectSequences(self)
287		end
288
289		-- at the end, restore the system, so it's usable elsewhere.
290		self.system = projectSystem
291	end
292
293
294	function p.rule.bake(self)
295		-- Add filtering terms to the context and then compile the results. These
296		-- terms describe the "operating environment"; only results contained by
297		-- configuration blocks which match these terms will be returned.
298
299		addCommonContextFilters(self)
300
301		-- Populate the token expansion environment
302
303		self.environ = {
304			rule = self,
305		}
306
307		-- Go ahead and distill all of that down now; this is my new rule object
308
309		context.compile(self)
310
311		-- sort the propertydefinition table.
312		table.sort(self.propertydefinition, function (a, b)
313			return a.name < b.name
314		end)
315
316		-- Set the context's base directory to the rule's file system
317		-- location. Any path tokens which are expanded in non-path fields
318		-- are made relative to this, ensuring a portable generated rule.
319
320		self.location = self.location or self.basedir
321		context.basedir(self, self.location)
322	end
323
324
325
326--
327-- Assigns a unique objects directory to every configuration of every project
328-- in the workspace, taking any objdir settings into account, to ensure builds
329-- from different configurations won't step on each others' object files.
330-- The path is built from these choices, in order:
331--
332--   [1] -> the objects directory as set in the config
333--   [2] -> [1] + the platform name
334--   [3] -> [2] + the build configuration name
335--   [4] -> [3] + the project name
336--
337-- @param wks
338--    The workspace to process. The directories are modified inline.
339--
340
341	function oven.bakeObjDirs(wks)
342		-- function to compute the four options for a specific configuration
343		local function getobjdirs(cfg)
344			-- the "!" prefix indicates the directory is not to be touched
345			local objdir = cfg.objdir or "obj"
346			local i = objdir:find("!", 1, true)
347			if i then
348				cfg.objdir = objdir:sub(1, i - 1) .. objdir:sub(i + 1)
349				return nil
350			end
351
352			local dirs = {}
353
354			local dir = path.getabsolute(path.join(cfg.project.location, objdir))
355			table.insert(dirs, dir)
356
357			if cfg.platform then
358				dir = path.join(dir, cfg.platform)
359				table.insert(dirs, dir)
360			end
361
362			dir = path.join(dir, cfg.buildcfg)
363			table.insert(dirs, dir)
364
365			dir = path.join(dir, cfg.project.name)
366			table.insert(dirs, dir)
367
368			return dirs
369		end
370
371		-- walk all of the configs in the workspace, and count the number of
372		-- times each obj dir gets used
373		local counts = {}
374		local configs = {}
375
376		for prj in p.workspace.eachproject(wks) do
377			for cfg in p.project.eachconfig(prj) do
378				-- get the dirs for this config, and associate them together,
379				-- and increment a counter for each one discovered
380				local dirs = getobjdirs(cfg)
381				if dirs then
382					configs[cfg] = dirs
383					for _, dir in ipairs(dirs or {}) do
384						counts[dir] = (counts[dir] or 0) + 1
385					end
386				end
387			end
388		end
389
390		-- now walk the list again, and assign the first unique value
391		for cfg, dirs in pairs(configs) do
392			for _, dir in ipairs(dirs) do
393				if counts[dir] == 1 then
394					cfg.objdir = dir
395					break
396				end
397			end
398		end
399	end
400
401
402--
403-- Create a list of workspace-level build configuration/platform pairs.
404--
405
406	function oven.bakeConfigs(wks)
407		local buildcfgs = wks.configurations or {}
408		local platforms = wks.platforms or {}
409
410		local configs = {}
411
412		local pairings = table.fold(buildcfgs, platforms)
413		for _, pairing in ipairs(pairings) do
414			local cfg = oven.bakeConfig(wks, nil, pairing[1], pairing[2])
415			if p.action.supportsconfig(p.action.current(), cfg) then
416				table.insert(configs, cfg)
417			end
418		end
419
420		return configs
421	end
422
423
424--
425-- It can be useful to state "use this map if this configuration is present".
426-- To allow this to happen, config maps that are specified within a project
427-- configuration are allowed to "bubble up" to the top level. Currently,
428-- maps are the only values that get this special behavior.
429--
430-- @param ctx
431--    The project context information.
432-- @param cset
433--    The project's original configuration set, which contains the settings
434--    of all the project configurations.
435-- @param cfgs
436--    The list of the project's build cfg/platform pairs.
437--
438
439	function oven.bubbleFields(ctx, cset, cfgs)
440		-- build a query filter that will match any configuration name,
441		-- within the existing constraints of the project
442
443		local configurations = {}
444		local platforms = {}
445
446		for _, cfg in ipairs(cfgs) do
447			if cfg[1] then
448				table.insert(configurations, cfg[1]:lower())
449			end
450			if cfg[2] then
451				table.insert(platforms, cfg[2]:lower())
452			end
453		end
454
455		local terms = table.deepcopy(ctx.terms)
456		terms.configurations = configurations
457		terms.platforms = platforms
458
459		for key in pairs(oven.bubbledFields) do
460			local field = p.field.get(key)
461			if not field then
462				ctx[key] = rawget(ctx, key)
463			else
464				local value = p.configset.fetch(cset, field, terms, ctx)
465				if value then
466					ctx[key] = value
467				end
468			end
469		end
470	end
471
472
473--
474-- Builds a list of build configuration/platform pairs for a project,
475-- along with a mapping between the workspace and project configurations.
476--
477-- @param ctx
478--    The project context information.
479-- @param cfgs
480--    The list of the project's build cfg/platform pairs.
481-- @return
482--     An array of the project's build configuration/platform pairs,
483--     based on any discovered mappings.
484--
485
486	function oven.bakeConfigList(ctx, cfgs)
487		-- run them all through the project's config map
488		for i, cfg in ipairs(cfgs) do
489			cfgs[i] = p.project.mapconfig(ctx, cfg[1], cfg[2])
490		end
491
492		-- walk through the result and remove any duplicates
493		local buildcfgs = {}
494		local platforms = {}
495
496		for _, pairing in ipairs(cfgs) do
497			local buildcfg = pairing[1]
498			local platform = pairing[2]
499
500			if not table.contains(buildcfgs, buildcfg) then
501				table.insert(buildcfgs, buildcfg)
502			end
503
504			if platform and not table.contains(platforms, platform) then
505				table.insert(platforms, platform)
506			end
507		end
508
509		-- merge these de-duped lists back into pairs for the final result
510		return table.fold(buildcfgs, platforms)
511	end
512
513
514---
515-- Flattens out the build settings for a particular build configuration and
516-- platform pairing, and returns the result.
517--
518-- @param wks
519--    The workpace which contains the configuration data.
520-- @param prj
521--    The project which contains the configuration data. Can be nil.
522-- @param buildcfg
523--    The target build configuration, a value from configurations().
524-- @param platform
525--    The target platform, a value from platforms().
526-- @param extraFilters
527--    Optional. Any extra filter terms to use when retrieving the data for
528--    this configuration
529---
530
531	function oven.bakeConfig(wks, prj, buildcfg, platform, extraFilters)
532
533		-- Set the default system and architecture values; if the platform's
534		-- name matches a known system or architecture, use that as the default.
535		-- More than a convenience; this is required to work properly with
536		-- external Visual Studio project files.
537
538		local system = os.target()
539		local architecture = nil
540		local toolset = p.action.current().toolset
541
542		if platform then
543			system = p.api.checkValue(p.fields.system, platform) or system
544			architecture = p.api.checkValue(p.fields.architecture, platform) or architecture
545			toolset = p.api.checkValue(p.fields.toolset, platform) or toolset
546		end
547
548		-- Wrap the projects's configuration set (which contains all of the information
549		-- provided by the project script) with a context object. The context handles
550		-- the expansion of tokens, and caching of retrieved values. The environment
551		-- values are used when expanding tokens.
552
553		local environ = {
554			wks = wks,
555			sln = wks,
556			prj = prj,
557		}
558
559		local ctx = context.new(prj or wks, environ)
560
561		ctx.project = prj
562		ctx.workspace = wks
563		ctx.solution = wks
564		ctx.global = wks.global
565		ctx.buildcfg = buildcfg
566		ctx.platform = platform
567		ctx.action = _ACTION
568
569		-- Allow the configuration information to be accessed by tokens contained
570		-- within the configuration itself
571
572		environ.cfg = ctx
573
574		-- Add filtering terms to the context and then compile the results. These
575		-- terms describe the "operating environment"; only results contained by
576		-- configuration blocks which match these terms will be returned. Start
577		-- by copying over the top-level environment from the workspace. Don't
578		-- copy the project terms though, so configurations can override those.
579
580		context.copyFilters(ctx, wks)
581
582		context.addFilter(ctx, "configurations", buildcfg)
583		context.addFilter(ctx, "platforms", platform)
584		if prj then
585			context.addFilter(ctx, "language", prj.language)
586		end
587
588		-- allow the project script to override the default system
589		ctx.system = ctx.system or system
590		context.addFilter(ctx, "system", os.getSystemTags(ctx.system))
591		context.addFilter(ctx, "host", os.getSystemTags(os.host()))
592
593		-- allow the project script to override the default architecture
594		ctx.architecture = ctx.architecture or architecture
595		context.addFilter(ctx, "architecture", ctx.architecture)
596
597		-- allow the project script to override the default toolset
598		ctx.toolset = _OPTIONS.cc or ctx.toolset or toolset
599		context.addFilter(ctx, "toolset", ctx.toolset)
600
601		-- if a kind is set, allow that to influence the configuration
602		context.addFilter(ctx, "kind", ctx.kind)
603
604		-- if a sharedlibtype is set, allow that to influence the configuration
605		context.addFilter(ctx, "sharedlibtype", ctx.sharedlibtype)
606
607		-- if tags are set, allow that to influence the configuration
608		context.addFilter(ctx, "tags", ctx.tags)
609
610		-- if any extra filters were specified, can include them now
611		if extraFilters then
612			for k, v in pairs(extraFilters) do
613				context.addFilter(ctx, k, v)
614			end
615		end
616
617		context.compile(ctx)
618
619		ctx.location = ctx.location or prj and prj.location
620		context.basedir(ctx, ctx.location)
621
622		-- Fill in a few calculated for the configuration, including the long
623		-- and short names and the build and link target.
624
625		oven.finishConfig(ctx)
626		return ctx
627	end
628
629
630--
631-- Create configuration objects for each file contained in the project. This
632-- collects and collates all of the values specified in the project scripts,
633-- and computes extra values like the relative path and object names.
634--
635-- @param prj
636--    The project object being baked. The project
637-- @return
638--    A collection of file configurations, keyed by both the absolute file
639--    path and an alpha-sorted index.
640--
641
642	function oven.bakeFiles(prj)
643
644		local files = {}
645
646		-- Start by building a comprehensive list of all the files contained by the
647		-- project. Some files may only be included in a subset of configurations so
648		-- I need to look at them all.
649
650		for cfg in p.project.eachconfig(prj) do
651			local function addFile(fname, i)
652
653				-- If this is the first time I've seen this file, start a new
654				-- file configuration for it. Track both by key for quick lookups
655				-- and indexed for ordered iteration.
656				local fcfg = files[fname]
657				if not fcfg then
658					fcfg = p.fileconfig.new(fname, prj)
659					fcfg.order = i
660					files[fname] = fcfg
661					table.insert(files, fcfg)
662				end
663
664				p.fileconfig.addconfig(fcfg, cfg)
665			end
666
667			table.foreachi(cfg.files, addFile)
668
669			-- If this project uses NuGet, we need to add the generated
670			-- packages.config file to the project. Is there a better place to
671			-- do this?
672
673			if #prj.nuget > 0 and (_ACTION < "vs2017" or p.project.iscpp(prj)) then
674				addFile("packages.config")
675			end
676		end
677
678		-- Alpha sort the indices, so I will get consistent results in
679		-- the exported project files.
680
681		table.sort(files, function(a,b)
682			return a.vpath < b.vpath
683		end)
684
685		return files
686	end
687
688
689--
690-- Assign unique sequence numbers to any source code files that would generate
691-- conflicting object file names (i.e. src/hello.cpp and tests/hello.cpp both
692-- create hello.o).
693--
694-- a file list of: src/hello.cpp, tests/hello.cpp and src/hello1.cpp also generates
695-- conflicting object file names - hello1.o
696
697	function oven.uniqueSequence(f, cfg, seq, bases)
698		while true do
699			f.sequence = seq[cfg] or 0
700			seq[cfg] = f.sequence + 1
701
702			if f.sequence == 0 then
703				-- first time seeing this objname
704				break
705			end
706
707			-- getting here has changed our sequence number, but this new "basename"
708			-- may still collide with files that actually end with this "sequence number"
709			-- so we have to check the bases table now
710
711			-- objname changes with the sequence number on every loop
712			local lowerobj = f.objname:lower()
713			if not bases[lowerobj] then
714				-- this is the first appearance of a file that produces this objname
715				-- intialize the table for any future basename that matches our objname
716				bases[lowerobj] = {}
717			end
718
719			if not bases[lowerobj][cfg] then
720				-- not a collision
721				-- start a sequence for a future basename that matches our objname for this cfg
722				bases[lowerobj][cfg] = 1
723				break
724			end
725			-- else we have a objname collision, try the next sequence number
726		end
727	end
728
729
730	function oven.assignObjectSequences(prj)
731
732		-- Iterate over the file configurations which were prepared and cached in
733		-- project.bakeFiles(); find buildable files with common base file names.
734
735		local bases = {}
736		table.foreachi(prj._.files, function(file)
737
738			-- Only consider sources that actually generate object files
739
740			if not path.isnativefile(file.abspath) then
741				return
742			end
743
744			-- For each base file name encountered, keep a count of the number of
745			-- collisions that have occurred for each project configuration. Use
746			-- this collision count to generate the unique object file names.
747
748			local lowerbase = file.basename:lower()
749			if not bases[lowerbase] then
750				bases[lowerbase] = {}
751			end
752
753			local sequences = bases[lowerbase]
754
755			for cfg in p.project.eachconfig(prj) do
756				local fcfg = p.fileconfig.getconfig(file, cfg)
757				if fcfg ~= nil and not fcfg.flags.ExcludeFromBuild then
758					oven.uniqueSequence(fcfg, cfg, sequences, bases)
759				end
760			end
761
762			-- Makefiles don't use per-configuration object names yet; keep
763			-- this around until they do. At which point I might consider just
764			-- storing the sequence number instead of the whole object name
765
766			oven.uniqueSequence(file, prj, sequences, bases)
767
768		end)
769	end
770
771
772--
773-- Finish the baking process for a workspace or project level configurations.
774-- Doesn't bake per se, just fills in some calculated values.
775--
776
777	function oven.finishConfig(cfg)
778		-- assign human-readable names
779		cfg.longname = table.concat({ cfg.buildcfg, cfg.platform }, "|")
780		cfg.shortname = table.concat({ cfg.buildcfg, cfg.platform }, " ")
781		cfg.shortname = cfg.shortname:gsub(" ", "_"):lower()
782		cfg.name = cfg.longname
783
784		-- compute build and link targets
785		if cfg.project and cfg.kind then
786			cfg.buildtarget = p.config.gettargetinfo(cfg)
787			cfg.buildtarget.relpath = p.project.getrelative(cfg.project, cfg.buildtarget.abspath)
788
789			cfg.linktarget = p.config.getlinkinfo(cfg)
790			cfg.linktarget.relpath = p.project.getrelative(cfg.project, cfg.linktarget.abspath)
791		end
792	end
793