Improve rule support:

- move rule code from gmake2.lua to rule.lua
- Add UTs
- Fix enum case
- uniformise code for list.
- Add support of rules for Codelite.
This commit is contained in:
Jarod42 2021-10-28 14:41:02 +02:00
parent 6c9fda87b5
commit 29fa743f19
12 changed files with 517 additions and 174 deletions

View File

@ -379,38 +379,51 @@
_p(4, '<CustomPostBuild/>') _p(4, '<CustomPostBuild/>')
local dependencies = {} local dependencies = {}
local rules = {} local makefilerules = {}
local function addrule(dependencies, rules, config, filename) local function addrule(dependencies, makefilerules, config, filename)
if #config.buildcommands == 0 or #config.buildOutputs == 0 then if #config.buildcommands == 0 or #config.buildOutputs == 0 then
return return false
end end
local inputs = table.implode(config.buildInputs,"",""," ") local inputs = table.implode(project.getrelative(cfg.project, config.buildInputs), "", "", " ")
if filename ~= "" and inputs ~= "" then if filename ~= "" and inputs ~= "" then
filename = filename .. " " filename = filename .. " "
end end
local outputs = config.buildOutputs[1] local outputs = project.getrelative(cfg.project, config.buildOutputs[1])
local buildmessage = "" local buildmessage = ""
if config.buildmessage then if config.buildmessage then
buildmessage = "\t@{ECHO} " .. config.buildmessage .. "\n" buildmessage = "\t@{ECHO} " .. config.buildmessage .. "\n"
end end
local commands = table.implode(config.buildCommands,"\t","\n","") local commands = table.implode(config.buildCommands,"\t","\n","")
table.insert(rules, os.translateCommandsAndPaths(outputs .. ": " .. filename .. inputs .. "\n" .. buildmessage .. commands, cfg.project.basedir, cfg.project.location)) table.insert(makefilerules, os.translateCommandsAndPaths(outputs .. ": " .. filename .. inputs .. "\n" .. buildmessage .. commands, cfg.project.basedir, cfg.project.location))
table.insertflat(dependencies, config.buildOutputs[1]) table.insertflat(dependencies, outputs)
return true
end end
local tr = project.getsourcetree(cfg.project) local tr = project.getsourcetree(cfg.project)
p.tree.traverse(tr, { p.tree.traverse(tr, {
onleaf = function(node, depth) onleaf = function(node, depth)
local filecfg = p.fileconfig.getconfig(node, cfg) local filecfg = p.fileconfig.getconfig(node, cfg)
addrule(dependencies, rules, filecfg, node.relpath) local prj = cfg.project
local rule = p.global.getRuleForFile(node.name, prj.rules)
if not addrule(dependencies, makefilerules, filecfg, node.relpath) and rule then
local environ = table.shallowcopy(filecfg.environ)
if rule.propertydefinition then
p.rule.prepareEnvironment(rule, environ, cfg)
p.rule.prepareEnvironment(rule, environ, filecfg)
end
local rulecfg = p.context.extent(rule, environ)
addrule(dependencies, makefilerules, rulecfg, node.relpath)
end
end end
}) })
addrule(dependencies, rules, cfg, "") addrule(dependencies, makefilerules, cfg, "")
if #rules == 0 and #dependencies == 0 then if #makefilerules == 0 and #dependencies == 0 then
_p(4, '<CustomPreBuild/>') _p(4, '<CustomPreBuild/>')
else else
_p(4, '<CustomPreBuild>' .. table.implode(dependencies,"",""," ")) _p(4, '<CustomPreBuild>' .. table.implode(dependencies,"",""," "))
_p(0, table.implode(rules,"","","\n") .. '</CustomPreBuild>') _p(0, table.implode(makefilerules,"","","\n") .. '</CustomPreBuild>')
end end
_p(3, '</AdditionalRules>') _p(3, '</AdditionalRules>')
end end

View File

@ -4,4 +4,5 @@ return {
"test_codelite_workspace.lua", "test_codelite_workspace.lua",
"test_codelite_project.lua", "test_codelite_project.lua",
"test_codelite_config.lua", "test_codelite_config.lua",
"test_codelite_additional_rules.lua",
} }

View File

@ -0,0 +1,256 @@
---
-- codelite/tests/test_codelite_config.lua
-- Automated test suite for CodeLite project generation.
-- Copyright (c) 2021 Joris Dauphin and the Premake project
---
local suite = test.declare("codelite_cproj_additional_rules")
local p = premake
local codelite = p.modules.codelite
---------------------------------------------------------------------------
-- Setup/Teardown
---------------------------------------------------------------------------
local wks, prj, cfg
local function prepare_rule()
rule "TestRule"
display "Test Rule"
fileextension ".rule"
propertydefinition {
name = "TestProperty",
kind = "boolean",
value = false,
switch = "-p"
}
propertydefinition {
name = "TestProperty2",
kind = "boolean",
value = false,
switch = "-p2"
}
propertydefinition {
name = "TestListProperty",
kind = "list"
}
propertydefinition {
name = "TestListPropertyWithSwitch",
kind = "list",
switch = "-S"
}
propertydefinition {
name = "TestListPropertySeparator",
kind = "list",
separator = ","
}
propertydefinition {
name = "TestListPropertySeparatorWithSwitch",
kind = "list",
separator = ",",
switch = "-O"
}
propertydefinition {
name = "TestEnumProperty",
values = { [0] = "V0", [1] = "V1"},
switch = { [0] = "S0", [1] = "S1"},
value = 0
}
buildmessage 'Rule-ing %{file.name}'
buildcommands 'dorule %{TestProperty} %{TestProperty2} %{TestListProperty} %{TestListPropertyWithSwitch} %{TestListPropertySeparator} %{TestListPropertySeparatorWithSwitch} %{TestEnumProperty} "%{file.path}"'
buildoutputs { "%{file.basename}.obj" }
end
function suite.setup()
p.action.set("codelite")
p.escaper(codelite.esc)
p.indent(" ")
prepare_rule()
wks = test.createWorkspace()
end
local function prepare()
prj = test.getproject(wks, 1)
cfg = test.getconfig(prj, "Debug")
end
function suite.customRuleEmpty()
prepare()
codelite.project.additionalRules(prj)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild/>
</AdditionalRules>
]]
end
function suite.customRuleWithPropertyDefinition()
rules { "TestRule" }
files { "test.rule", "test2.rule" }
testRuleVars {
TestProperty = true
}
filter "files:test2.rule"
testRuleVars {
TestProperty2 = true
}
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>test.obj test2.obj
test.obj: test.rule
@echo Rule-ing test.rule
dorule -p "test.rule"
test2.obj: test2.rule
@echo Rule-ing test2.rule
dorule -p -p2 "test2.rule"
</CustomPreBuild>
</AdditionalRules>
]]
end
function suite.customRuleWithPropertyDefinitionSeparator()
rules { "TestRule" }
files { "test.rule", "test2.rule", "test3.rule", "test4.rule" }
filter "files:test.rule"
testRuleVars {
TestListProperty = { "testValue1", "testValue2" }
}
filter "files:test2.rule"
testRuleVars {
TestListPropertyWithSwitch = { "testValue1", "testValue2" }
}
filter "files:test3.rule"
testRuleVars {
TestListPropertySeparator = { "testValue1", "testValue2" }
}
filter "files:test4.rule"
testRuleVars {
TestListPropertySeparatorWithSwitch = { "testValue1", "testValue2" }
}
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>test.obj test2.obj test3.obj test4.obj
test.obj: test.rule
@echo Rule-ing test.rule
dorule testValue1 testValue2 "test.rule"
test2.obj: test2.rule
@echo Rule-ing test2.rule
dorule -StestValue1 -StestValue2 "test2.rule"
test3.obj: test3.rule
@echo Rule-ing test3.rule
dorule testValue1,testValue2 "test3.rule"
test4.obj: test4.rule
@echo Rule-ing test4.rule
dorule -OtestValue1,testValue2 "test4.rule"
</CustomPreBuild>
</AdditionalRules>
]]
end
function suite.customRuleWithPropertyDefinitionEnum()
rules { "TestRule" }
files { "test.rule", "test2.rule" }
testRuleVars {
TestEnumProperty = "V0"
}
filter "files:test2.rule"
testRuleVars {
TestEnumProperty = "V1"
}
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>test.obj test2.obj
test.obj: test.rule
@echo Rule-ing test.rule
dorule S0 "test.rule"
test2.obj: test2.rule
@echo Rule-ing test2.rule
dorule S1 "test2.rule"
</CustomPreBuild>
</AdditionalRules>
]]
end
function suite.buildCommand()
files {"foo.txt", "bar.txt"}
buildinputs { "toto.txt", "extra_dependency" }
buildoutputs { "toto.c" }
buildcommands { "test", "test toto.c" }
buildmessage "Some message"
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>toto.c
toto.c: toto.txt extra_dependency
@echo Some message
test
test toto.c
</CustomPreBuild>
</AdditionalRules>]]
end
function suite.buildCommandPerFile()
files {"foo.txt", "bar.txt"}
filter "files:**.txt"
buildinputs { "%{file.basename}.h", "extra_dependency" }
buildoutputs { "%{file.basename}.c" }
buildcommands { "test", "test %{file.basename}" }
buildmessage "Some message"
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>bar.c foo.c
bar.c: bar.txt bar.h extra_dependency
@echo Some message
test
test bar
foo.c: foo.txt foo.h extra_dependency
@echo Some message
test
test foo
</CustomPreBuild>
</AdditionalRules>]]
end

View File

@ -203,52 +203,6 @@
]] ]]
end end
function suite.OnProjectCfg_BuildCommand()
files {"/c/foo.txt", "/c/bar.txt"}
buildinputs { "/c/toto.txt", "/c/extra_dependency" }
buildoutputs { "/c/toto.c" }
buildcommands { "test", "test /c/toto.c" }
buildmessage "Some message"
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>/c/toto.c
/c/toto.c: /c/toto.txt /c/extra_dependency
@echo Some message
test
test /c/toto.c
</CustomPreBuild>
</AdditionalRules>]]
end
function suite.OnProjectCfg_BuildCommandPerFile()
files {"/c/foo.txt", "/c/bar.txt"}
filter "files:**.txt"
buildinputs { "/c/%{file.basename}.h", "/c/extra_dependency" }
buildoutputs { "/c/%{file.basename}.c" }
buildcommands { "test", "test /c/%{file.basename}" }
buildmessage "Some message"
prepare()
codelite.project.additionalRules(cfg)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild>/c/bar.c /c/foo.c
/c/bar.c: /c/bar.txt /c/bar.h /c/extra_dependency
@echo Some message
test
test /c/bar
/c/foo.c: /c/foo.txt /c/foo.h /c/extra_dependency
@echo Some message
test
test /c/foo
</CustomPreBuild>
</AdditionalRules>]]
end
function suite.OnProjectCfg_General() function suite.OnProjectCfg_General()
system "Windows" system "Windows"
prepare() prepare()
@ -386,20 +340,6 @@ cmd2</StartupCommands>
]] ]]
end end
-- TODO: test custom build
function suite.OnProjectCfg_AdditionalRules()
prepare()
codelite.project.additionalRules(prj)
test.capture [[
<AdditionalRules>
<CustomPostBuild/>
<CustomPreBuild/>
</AdditionalRules>
]]
end
function suite.OnProjectCfg_Completion() function suite.OnProjectCfg_Completion()
language "C++" language "C++"
cppdialect "C++11" cppdialect "C++11"

View File

@ -171,20 +171,6 @@
return value return value
end 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) function gmake2.getToolSet(cfg)
local default = iif(cfg.system == p.MACOSX, "clang", "gcc") local default = iif(cfg.system == p.MACOSX, "clang", "gcc")
local toolset = p.tools[_OPTIONS.cc or cfg.toolset or default] local toolset = p.tools[_OPTIONS.cc or cfg.toolset or default]
@ -259,71 +245,6 @@
-- convert a rule property into a string -- convert a rule property into a string
function gmake2.expandRuleString(rule, prop, value)
-- list?
if type(value) == "table" then
if #value > 0 then
if prop.switch then
return prop.switch .. table.concat(value, " " .. prop.switch)
else
prop.separator = prop.separator or " "
return table.concat(value, prop.separator)
end
else
return nil
end
end
-- bool just emits the switch
if prop.switch and type(value) == "boolean" then
if value then
return prop.switch
else
return nil
end
end
local switch = prop.switch or ""
-- enum?
if prop.values then
value = table.findKeyByValue(prop.values, value)
if value == nil then
value = ""
end
end
-- primitive
value = tostring(value)
if #value > 0 then
return switch .. value
else
return nil
end
end
function gmake2.prepareEnvironment(rule, environ, cfg)
for _, prop in ipairs(rule.propertydefinition) 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 = gmake2.expandRuleString(rule, prop, value)
if value ~= nil and #value > 0 then
environ[prop.name] = p.esc(value)
end
end
end
end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
-- --
-- Handlers for the individual makefile elements that can be shared -- Handlers for the individual makefile elements that can be shared

View File

@ -269,8 +269,8 @@
local environ = table.shallowcopy(filecfg.environ) local environ = table.shallowcopy(filecfg.environ)
if rule.propertydefinition then if rule.propertydefinition then
gmake2.prepareEnvironment(rule, environ, cfg) p.rule.prepareEnvironment(rule, environ, cfg)
gmake2.prepareEnvironment(rule, environ, filecfg) p.rule.prepareEnvironment(rule, environ, filecfg)
end end
local shadowContext = p.context.extent(rule, environ) local shadowContext = p.context.extent(rule, environ)

View File

@ -175,8 +175,8 @@
local environ = table.shallowcopy(filecfg.environ) local environ = table.shallowcopy(filecfg.environ)
if rule.propertydefinition then if rule.propertydefinition then
gmake2.prepareEnvironment(rule, environ, cfg) p.rule.prepareEnvironment(rule, environ, cfg)
gmake2.prepareEnvironment(rule, environ, filecfg) p.rule.prepareEnvironment(rule, environ, filecfg)
end end
local shadowContext = p.context.extent(rule, environ) local shadowContext = p.context.extent(rule, environ)

View File

@ -41,15 +41,31 @@
name = "TestListProperty", name = "TestListProperty",
kind = "list" kind = "list"
} }
propertydefinition {
name = "TestListPropertyWithSwitch",
kind = "list",
switch = "-S"
}
propertydefinition { propertydefinition {
name = "TestListPropertySeparator", name = "TestListPropertySeparator",
kind = "list", kind = "list",
separator = "," separator = ","
} }
propertydefinition {
name = "TestListPropertySeparatorWithSwitch",
kind = "list",
separator = ",",
switch = "-O"
}
propertydefinition {
name = "TestEnumProperty",
values = { [0] = "V0", [1] = "V1"},
switch = { [0] = "S0", [1] = "S1"},
value = 0
}
buildmessage 'Rule-ing %{file.name}' buildmessage 'Rule-ing %{file.name}'
buildcommands 'dorule %{TestProperty} %{TestProperty2} %{TestListProperty} %{TestListPropertySeparator} "%{file.path}"' buildcommands 'dorule %{TestProperty} %{TestProperty2} %{TestListProperty} %{TestListPropertyWithSwitch} %{TestListPropertySeparator} %{TestListPropertySeparatorWithSwitch} %{TestEnumProperty} "%{file.path}"'
buildoutputs { "%{file.basename}.obj" } buildoutputs { "%{file.basename}.obj" }
wks = test.createWorkspace() wks = test.createWorkspace()
@ -292,7 +308,7 @@ test2.obj: test2.rule
rules { "TestRule" } rules { "TestRule" }
files { "test.rule", "test2.rule" } files { "test.rule", "test2.rule", "test3.rule", "test4.rule" }
filter "files:test.rule" filter "files:test.rule"
testRuleVars { testRuleVars {
@ -300,9 +316,18 @@ test2.obj: test2.rule
} }
filter "files:test2.rule" filter "files:test2.rule"
testRuleVars {
TestListPropertyWithSwitch = { "testValue1", "testValue2" }
}
filter "files:test3.rule"
testRuleVars { testRuleVars {
TestListPropertySeparator = { "testValue1", "testValue2" } TestListPropertySeparator = { "testValue1", "testValue2" }
} }
filter "files:test4.rule"
testRuleVars {
TestListPropertySeparatorWithSwitch = { "testValue1", "testValue2" }
}
prepare() prepare()
test.capture [[ test.capture [[
@ -314,7 +339,41 @@ test.obj: test.rule
$(SILENT) dorule testValue1\ testValue2 "test.rule" $(SILENT) dorule testValue1\ testValue2 "test.rule"
test2.obj: test2.rule test2.obj: test2.rule
@echo Rule-ing test2.rule @echo Rule-ing test2.rule
$(SILENT) dorule testValue1,testValue2 "test2.rule" $(SILENT) dorule -StestValue1\ -StestValue2 "test2.rule"
test3.obj: test3.rule
@echo Rule-ing test3.rule
$(SILENT) dorule testValue1,testValue2 "test3.rule"
test4.obj: test4.rule
@echo Rule-ing test4.rule
$(SILENT) dorule -OtestValue1,testValue2 "test4.rule"
]] ]]
end end
function suite.customRuleWithPropertyDefinitionEnum()
rules { "TestRule" }
files { "test.rule", "test2.rule" }
testRuleVars {
TestEnumProperty = "V0"
}
filter "files:test2.rule"
testRuleVars {
TestEnumProperty = "V1"
}
prepare()
test.capture [[
# File Rules
# #############################################
test.obj: test.rule
@echo Rule-ing test.rule
$(SILENT) dorule S0 "test.rule"
test2.obj: test2.rule
@echo Rule-ing test2.rule
$(SILENT) dorule S1 "test2.rule"
]]
end

View File

@ -136,21 +136,14 @@
-- @param format -- @param format
-- The formatting to be used, ie "[%s]". -- The formatting to be used, ie "[%s]".
--- ---
function rule.createEnvironment(self, format)
function rule.prepareEnvironment(self, environ, format) local environ = {}
for _, def in ipairs(self.propertydefinition) do for _, def in ipairs(self.propertydefinition) do
environ[def.name] = string.format(format, def.name) environ[def.name] = string.format(format, def.name)
end end
end
function rule.createEnvironment(self, format)
local environ = {}
rule.prepareEnvironment(self, environ, format)
return environ return environ
end end
--- ---
-- prepare an table of pathVars with the rule properties as global tokens, -- prepare an table of pathVars with the rule properties as global tokens,
-- according to the format specified. -- according to the format specified.
@ -172,3 +165,78 @@
rule.preparePathVars(self, pathVars, format) rule.preparePathVars(self, pathVars, format)
return pathVars return pathVars
end end
function rule.prepareEnvironment(self, environ, cfg)
local function 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
local function expandRuleString(prop, value)
-- list
if type(value) == "table" then
if #value > 0 then
local switch = prop.switch or ""
if prop.separator then
return switch .. table.concat(value, prop.separator)
else
return switch .. table.concat(value, " " .. switch)
end
else
return nil
end
end
-- bool just emits the switch
if prop.switch and type(value) == "boolean" then
if value then
return prop.switch
else
return nil
end
end
-- enum
if prop.values then
local switch = prop.switch or {}
value = table.findKeyByValue(prop.values, value)
if value == nil then
return nil
end
return switch[value]
end
-- primitive
local switch = prop.switch or ""
value = tostring(value)
if #value > 0 then
return switch .. value
else
return nil
end
end
for _, prop in ipairs(self.propertydefinition) do
local fld = p.rule.getPropertyField(self, prop)
local value = cfg[fld.name]
if value ~= nil then
if fld.kind == "path" then
value = path(cfg, value)
elseif fld.kind == "list:path" then
value = path(cfg, value)
end
value = expandRuleString(prop, value)
if value ~= nil and #value > 0 then
environ[prop.name] = p.esc(value)
end
end
end
end

View File

@ -16,6 +16,7 @@ return {
"base/test_premake_command.lua", "base/test_premake_command.lua",
"base/test_table.lua", "base/test_table.lua",
"base/test_tree.lua", "base/test_tree.lua",
"base/test_rule.lua",
"base/test_uuid.lua", "base/test_uuid.lua",
"base/test_versions.lua", "base/test_versions.lua",
"base/test_http.lua", "base/test_http.lua",

85
tests/base/test_rule.lua Normal file
View File

@ -0,0 +1,85 @@
--
-- tests/base/test_rule.lua
-- Automated test suite for custom rule.
-- Copyright (c) 2008-2021 Jason Perkins and the Premake project
--
local suite = test.declare("rule")
local p = premake
function suite.setup()
rule "TestRule"
display "Test Rule"
fileextension ".rule"
propertydefinition {
name = "TestPropertyFalse",
kind = "boolean",
value = false,
switch = "-dummy"
}
propertydefinition {
name = "TestPropertyTrue",
kind = "boolean",
value = false,
switch = "-p"
}
propertydefinition {
name = "TestListProperty",
kind = "list"
}
propertydefinition {
name = "TestListPropertyWithSwitch",
kind = "list",
switch = "-S"
}
propertydefinition {
name = "TestListPropertySeparator",
kind = "list",
separator = ","
}
propertydefinition {
name = "TestListPropertySeparatorWithSwitch",
kind = "list",
separator = ",",
switch = "-O"
}
propertydefinition {
name = "TestEnumProperty",
values = { [0] = "V0", [1] = "V1"},
switch = { [0] = "S0", [1] = "S1"},
value = 0
}
end
--
-- rule tests
--
function suite.prepareEnvironment()
local rule = premake.global.getRule("TestRule")
local environ = {}
local cfg = {
["_rule_TestRule_TestPropertyFalse"] = false,
["_rule_TestRule_TestPropertyTrue"] = true,
["_rule_TestRule_TestListProperty"] = {"a", "b"},
["_rule_TestRule_TestListPropertyWithSwitch"] = {"c", "d"},
["_rule_TestRule_TestListPropertySeparator"] = {"e", "f"},
["_rule_TestRule_TestListPropertySeparatorWithSwitch"] = {"1", "2"},
["_rule_TestRule_TestEnumProperty"] = 'V1'
}
p.rule.prepareEnvironment(rule, environ, cfg)
test.isequal(nil, environ["TestPropertyFalse"])
test.isequal("-p", environ["TestPropertyTrue"])
test.isequal("a b", environ["TestListProperty"])
test.isequal("-Sc -Sd", environ["TestListPropertyWithSwitch"])
test.isequal("e,f", environ["TestListPropertySeparator"])
test.isequal("-O1,2", environ["TestListPropertySeparatorWithSwitch"])
test.isequal("S1", environ["TestEnumProperty"])
end

View File

@ -47,9 +47,8 @@ For enum properties, a key-value table of the possible values of the property, a
The value to be placed into the command line for this property. See the examples below for more information. The value to be placed into the command line for this property. See the examples below for more information.
#### separator #### #### separator ####
For list properties, this sets the value of the list item separator in the command line.\ For list properties, this sets the value of the list item separator in the command line.
gmake2: Default value is ' '. If a switch is set, the separator is ignored and instead the switch is duplicated.\ If set, the list is concatenated by the separator and placed behind a single switch. If not set, the switch is duplicated.
VS201x: If set, the list is concatenated by the separator and placed behind a single switch. If not set, the switch is duplicated.
#### category #### #### category ####
Visual Studio only. Visual Studio only.