First pass at support for configuration filter prefixes (e.g. "files:Debug**")

This commit is contained in:
Jason Perkins 2014-04-07 15:04:19 -04:00
parent 6812f6493b
commit 28cfa55886
7 changed files with 258 additions and 138 deletions

View File

@ -41,6 +41,9 @@
---
-- Retrieve a value from the configuration set.
--
-- This and the criteria supporting code are the inner loops of the app. Some
-- readability has been sacrificed for overall performance.
--
-- @param cset
-- The configuration set to query.
-- @param field
@ -50,39 +53,42 @@
-- blocks with terms fully contained by this list will be considered in
-- determining the returned value. Terms should be lower case to make
-- the context filtering case-insensitive.
-- @param filename
-- An optional filename; if provided, only blocks with pattern that
-- matches the name will be considered.
-- @return
-- The requested value.
---
function configset.fetch(cset, field, context, filename)
function configset.fetch(cset, field, context)
if not context then
context = cset.current._criteria.terms
end
if filename then
filename = filename:lower()
end
if premake.field.merges(field) then
return configset._fetchMerged(cset, field, context, filename)
return configset._fetchMerged(cset, field, context)
else
return configset._fetchDirect(cset, field, context, filename)
return configset._fetchDirect(cset, field, context)
end
end
function configset._fetchDirect(cset, field, filter, filename)
function configset._fetchDirect(cset, field, filter)
local abspath = filter.files
local basedir
local key = field.name
local blocks = cset.blocks
local n = #blocks
for i = n, 1, -1 do
local block = blocks[i]
local value = block[key]
if value and (cset.compiled or configset.testblock(block, filter, filename)) then
-- If the filter contains a file path, make it relative to
-- this block's basedir
if value and abspath and block._basedir ~= basedir and not cset.compiled then
basedir = block._basedir
filter.files = path.getrelative(basedir, abspath)
end
if value and (cset.compiled or criteria.matches(block._criteria, filter)) then
-- If value is an object, return a copy of it so that any
-- changes later made to it by the caller won't alter the
-- original value (that was a tough bug to find)
@ -93,37 +99,55 @@
end
end
filter.files = abspath
if cset.parent then
return configset._fetchDirect(cset.parent, field, filter, filename)
return configset._fetchDirect(cset.parent, field, filter)
end
end
function configset._fetchMerged(cset, field, filter, filename)
function configset._fetchMerged(cset, field, filter)
local result = {}
local function remove(patterns)
for _, pattern in ipairs(patterns) do
local i = 1
while i <= #result do
local value = result[i]:lower()
for i = 1, #patterns do
local pattern = patterns[i]
local j = 1
while j <= #result do
local value = result[j]:lower()
if value:match(pattern) == value then
result[result[i]] = nil
table.remove(result, i)
result[result[j]] = nil
table.remove(result, j)
else
i = i + 1
j = j + 1
end
end
end
end
if cset.parent then
result = configset._fetchMerged(cset.parent, field, filter, filename)
result = configset._fetchMerged(cset.parent, field, filter)
end
local abspath = filter.files
local basedir
local key = field.name
for _, block in ipairs(cset.blocks) do
if cset.compiled or configset.testblock(block, filter, filename) then
local blocks = cset.blocks
local n = #blocks
for i = 1, n do
local block = blocks[i]
-- If the filter contains a file path, make it relative to
-- this block's basedir
if abspath and block._basedir ~= basedir and not cset.compiled then
basedir = block._basedir
filter.files = path.getrelative(basedir, abspath)
end
if cset.compiled or criteria.matches(block._criteria, filter) then
if block._removes and block._removes[key] then
remove(block._removes[key])
end
@ -135,6 +159,7 @@
end
end
filter.files = abspath
return result
end
@ -303,22 +328,6 @@
--
-- Check to see if an individual configuration block applies to the
-- given context and filename.
--
function configset.testblock(block, context, filename)
-- Make file tests relative to the blocks base directory,
-- so path relative pattern matches will work.
if block._basedir and filename then
filename = path.getrelative(block._basedir, filename)
end
return criteria.matches(block._criteria, context, filename)
end
--
-- Compiles a new configuration set containing only the blocks which match
-- the specified criteria. Fetches against this compiled configuration set
@ -327,42 +336,48 @@
--
-- @param cset
-- The configuration set to query.
-- @param context
-- @param filter
-- A list of lowercase context terms to use during the fetch. Only those
-- blocks with terms fully contained by this list will be considered in
-- determining the returned value. Terms should be lower case to make
-- the context filtering case-insensitive.
-- @param filename
-- An optional filename; if provided, only blocks with pattern that
-- matches the name will be considered.
-- @return
-- A new configuration set containing only the selected blocks, and the
-- "compiled" field set to true.
--
function configset.compile(cset, context, filename)
if filename then
filename = filename:lower()
end
function configset.compile(cset, filter)
-- always start with the parent
local result
if cset.parent then
result = configset.compile(cset.parent, context, filename)
result = configset.compile(cset.parent, filter)
else
result = configset.new()
end
-- add in my own blocks
for _, block in ipairs(cset.blocks) do
if configset.testblock(block, context, filename) then
local blocks = cset.blocks
local n = #blocks
local abspath = filter.files
local basedir
for i = 1, n do
local block = blocks[i]
-- If the filter contains a file path, make it relative to
-- this block's basedir
if abspath and block._basedir ~= basedir then
basedir = block._basedir
filter.files = path.getrelative(basedir, abspath)
end
if criteria.matches(block._criteria, filter) then
table.insert(result.blocks, block)
end
end
filter.files = abspath
result.compiled = true
return result
end

View File

@ -33,11 +33,10 @@
-- A new context object.
--
function context.new(cfgset, environ, filename)
function context.new(cfgset, environ)
local ctx = {}
ctx._cfgset = cfgset
ctx.environ = environ or {}
ctx._filename = { filename } or {}
ctx.terms = {}
-- This base directory is used when expanding path tokens encountered
@ -56,26 +55,31 @@
end
--
-- Add additional filtering terms to an existing context.
---
-- Add a new key-value pair to refine the context filtering.
--
-- @param ctx
-- The context to contain the new terms.
-- @param terms
-- One or more new terms to add to the context. May be nil.
--
-- The context to be filtered.
-- @param key
-- The new (or an existing) key value.
-- @param value
-- The filtering value for the key.
---
function context.addterms(ctx, terms)
if terms then
terms = table.flatten({terms})
for _, term in ipairs(terms) do
-- make future tests case-insensitive
table.insert(ctx.terms, term:lower())
function context.addFilter(ctx, key, value)
if type(value) == "table" then
for i = 1, #value do
value[i] = value[i]:lower()
end
elseif value then
value = value:lower()
end
ctx.terms[key:lower()] = value
end
--
-- Copies the list of terms from an existing context.
--
@ -85,8 +89,8 @@
-- The context containing the terms to copy.
--
function context.copyterms(ctx, src)
ctx.terms = table.arraycopy(src.terms)
function context.copyFilters(ctx, src)
ctx.terms = table.deepcopy(src.terms)
end
@ -119,7 +123,7 @@
--
function context.compile(ctx)
ctx._cfgset = configset.compile(ctx._cfgset, ctx.terms, ctx._filename[1])
ctx._cfgset = configset.compile(ctx._cfgset, ctx.terms)
end
@ -164,7 +168,7 @@
-- If there is a matching field, then go fetch the aggregated value
-- from my configuration set, and then cache it future lookups.
local value = configset.fetch(ctx._cfgset, field, ctx.terms, ctx._filename[1])
local value = configset.fetch(ctx._cfgset, field, ctx.terms)
if value then
-- do I need to expand tokens?
if field and field.tokens then

View File

@ -24,17 +24,27 @@
function criteria.new(terms)
terms = table.flatten(terms)
-- convert Premake wildcard symbols into the appropriate Lua patterns; this
-- list of patterns is what will actually be tested against
-- Preprocess the terms list for performance in matches() later.
-- Wildcards are replaced with Lua patterns. Terms with "or" and
-- "not" modifiers are split into arrays of parts to test.
-- Prefixes are split out and stored under a quick lookup key.
local patterns = {}
for i, term in ipairs(terms) do
terms[i] = term:lower()
local parts = path.wildcards(terms[i])
parts = parts:explode(" or ")
term = term:lower()
terms[i] = term
local pattern = {}
local n = term:find(":", 1, true)
if n then
pattern.prefix = term:sub(1, n - 1)
term = term:sub(n + 1)
end
local parts = path.wildcards(term)
parts = parts:explode(" or ")
for i, part in ipairs(parts) do
if part:startswith("not ") then
table.insert(pattern, "not")
@ -62,57 +72,82 @@
-- @param context
-- The list of context terms to test against, provided as a list of
-- lowercase strings.
-- @param filenamae
-- An optional filename; if provided, at least one pattern matching the
-- name must be present to pass the test.
-- @return
-- True if all criteria are satisfied by the context.
---
function criteria.matches(crit, context, filename)
function criteria.matches(crit, context)
-- If the context specifies a filename, I should only match against
-- blocks targeted at that file specifically. This way, files only
-- pick up the settings that a different from the main project.
local filename = context.files
local filematched = false
function testcontext(part, negated)
for i = 1, #context do
local value = context[i]
if value:match(part) == value then
return true
-- Test one value from the context against a part of a pattern
function testValue(value, part)
if type(value) == "table" then
for i = 1, #value do
if testValue(value[i], part) then
return true
end
end
else
if value and value:match(part) == value then
return true;
end
end
if filename and not negated and filename:match(part) == filename then
filematched = true
return true
end
return false
end
function testparts(pattern)
local n = #pattern
local i = 1
while i <= n do
local part = pattern[i]
if part == "not" then
i = i + 1
if not testcontext(pattern[i], true) then
return true
end
else
if testcontext(part) then
return true
end
-- Test one part of one pattern against the provided context
function testContext(prefix, part, assertion)
if prefix then
local result = testValue(context[prefix], part)
if prefix == "files" and result == assertion then
filematched = true
end
if result then
return assertion
end
else
if filename and assertion and filename:match(part) == filename then
filematched = true
return assertion
end
i = i + 1
for prefix, value in pairs(context) do
if testValue(value, part) then
return assertion
end
end
end
return not assertion
end
-- Test an individual pattern in this criteria's list of patterns
function testPattern(pattern)
local n = #pattern
local assertion = true
for i = 1, n do
local part = pattern[i]
if part == "not" then
assertion = false
else
if testContext(pattern.prefix, part, assertion) then
return true
end
assertion = true
end
end
end
-- Iterate the list of patterns and test each in turn
local n = #crit.patterns
for i = 1, n do
local pattern = crit.patterns[i]
if not testparts(pattern) then
if not testPattern(pattern) then
return false
end
end
@ -123,3 +158,5 @@
return true
end

View File

@ -83,8 +83,9 @@
-- specific to the file.
local environ = {}
local fsub = context.new(prj, environ, fcfg.abspath)
context.copyterms(fsub, cfg)
local fsub = context.new(prj, environ)
context.copyFilters(fsub, cfg)
context.addFilter(fsub, "files", fcfg.abspath:lower())
fcfg.configs[cfg] = fsub

View File

@ -66,17 +66,21 @@
-- terms describe the "operating environment"; only results contained by
-- configuration blocks which match these terms will be returned.
context.addterms(ctx, _ACTION)
context.addFilter(ctx, "_ACTION", _ACTION)
context.addFilter(ctx, "action", _ACTION)
-- Add command line options to the filtering options
local options = {}
for key, value in pairs(_OPTIONS) do
local term = key
if value ~= "" then
term = term .. "=" .. value
end
context.addterms(ctx, term)
table.insert(options, term)
end
context.addFilter(ctx, "_OPTIONS", options)
context.addFilter(ctx, "options", options)
context.compile(ctx)
@ -143,19 +147,19 @@
-- Add filtering terms to the context to make it as specific as I can.
-- Start with the same filtering that was applied at the solution level.
context.copyterms(ctx, sln)
context.copyFilters(ctx, sln)
-- Now filter on the current system and architecture, allowing the
-- values that might already in the context to override my defaults.
ctx.system = ctx.system or premake.action.current().os or os.get()
context.addterms(ctx, ctx.system)
context.addterms(ctx, ctx.architecture)
context.addFilter(ctx, "system", ctx.system)
context.addFilter(ctx, "architecture", ctx.architecture)
-- The kind is a configuration level value, but if it has been set at the
-- project level allow that to influence the other project-level results.
context.addterms(ctx, ctx.kind)
context.addFilter(ctx, "kind", ctx.kind)
-- Go ahead and distill all of that down now; this is my new project object
@ -461,22 +465,22 @@
-- by copying over the top-level environment from the solution. Don't
-- copy the project terms though, so configurations can override those.
context.copyterms(ctx, prj.solution)
context.copyFilters(ctx, prj.solution)
context.addterms(ctx, buildcfg)
context.addterms(ctx, platform)
context.addterms(ctx, prj.language)
context.addFilter(ctx, "configurations", buildcfg)
context.addFilter(ctx, "platforms", platform)
context.addFilter(ctx, "language", prj.language)
-- allow the project script to override the default system
ctx.system = ctx.system or system
context.addterms(ctx, ctx.system)
context.addFilter(ctx, "system", ctx.system)
-- allow the project script to override the default architecture
ctx.architecture = ctx.architecture or architecture
context.addterms(ctx, ctx.architecture)
context.addFilter(ctx, "architecture", ctx.architecture)
-- if a kind is set, allow that to influence the configuration
context.addterms(ctx, ctx.kind)
context.addFilter(ctx, "kind", ctx.kind)
context.compile(ctx)

View File

@ -111,7 +111,7 @@
local f = field.get("buildaction")
configset.addblock(cset, { "hello.c" }, os.getcwd())
configset.store(cset, f, "Copy")
test.isequal("Copy", configset.fetch(cset, f, {}, path.join(os.getcwd(), "hello.c")))
test.isequal("Copy", configset.fetch(cset, f, { files=path.join(os.getcwd(), "hello.c"):lower() }))
end

View File

@ -1,11 +1,10 @@
--
-- tests/base/test_criteria.lua
-- Test suite for the criteria matching API.
-- Copyright (c) 2012 Jason Perkins and the Premake project
-- Copyright (c) 2012-2014 Jason Perkins and the Premake project
--
T.criteria = {}
local suite = T.criteria
local suite = test.declare("criteria")
local criteria = premake.criteria
@ -70,7 +69,7 @@
--
-- The "not" modifier should fail the test if the term is matched.
--
function suite.fails_onNotMatch()
crit = criteria.new { "not windows" }
test.isfalse(criteria.matches(crit, { "windows" }))
@ -138,20 +137,80 @@
function suite.passes_onFilenameAndMatchingPattern()
crit = criteria.new { "**.c", "windows" }
test.istrue(criteria.matches(crit, { "windows" }, "hello.c"))
test.istrue(criteria.matches(crit, { system = "windows", files = "hello.c" }))
end
function suite.fails_onFilenameAndNoMatchingPattern()
crit = criteria.new { "windows" }
test.isfalse(criteria.matches(crit, { "windows" }, "hello.c"))
test.isfalse(criteria.matches(crit, { "windows", files = "hello.c" }))
end
--
-- "Not" modifiers should not match filenames.
--
function suite.fails_onFilnameAndNotModifier()
function suite.fails_onFilenameAndNotModifier()
crit = criteria.new { "not linux" }
test.isfalse(criteria.matches(crit, { "windows" }, "hello.c"))
test.isfalse(criteria.matches(crit, { "windows", files = "hello.c" }))
end
--
-- "Open" or non-prefixed terms can match against any scope.
--
function suite.openTerm_matchesAnyKeyedScope()
crit = criteria.new { "debug" }
test.istrue(criteria.matches(crit, { configuration="debug" }))
end
--
-- Prefixed terms should only matching against context that
-- uses a matching key.
--
function suite.prefixedTermMatches_onKeyMatch()
crit = criteria.new { "configurations:debug" }
test.istrue(criteria.matches(crit, { configurations="debug" }))
end
function suite.prefixedTermFails_onNoKeyMatch()
crit = criteria.new { "configurations:debug" }
test.isfalse(criteria.matches(crit, { configurations="release", platforms="debug" }))
end
function suite.prefixTermFails_onFilenameMatch()
crit = criteria.new { "configurations:hello**" }
test.isfalse(criteria.matches(crit, { files = "hello.cpp" }))
end
--
-- If context provides a list of values, match against them.
--
function suite.termMatchesList_onNoPrefix()
crit = criteria.new { "debug" }
test.istrue(criteria.matches(crit, { options={ "debug", "logging" }}))
end
function suite.termMatchesList_onPrefix()
crit = criteria.new { "options:debug" }
test.istrue(criteria.matches(crit, { options={ "debug", "logging" }}))
end
--
-- Check handling of the files: prefix.
--
function suite.matchesFilePrefix_onPositiveMatch()
crit = criteria.new { "files:**.cpp" }
test.istrue(criteria.matches(crit, { files = "hello.cpp" }))
end
function suite.matchesFilePrefix_onNotModifier()
crit = criteria.new { "files:not **.h" }
test.istrue(criteria.matches(crit, { files = "hello.cpp" }))
end