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