865 lines
28 KiB
Lua
865 lines
28 KiB
Lua
module(..., package.seeall)
|
|
|
|
local native = require "tundra.native"
|
|
local nodegen = require "tundra.nodegen"
|
|
local path = require "tundra.path"
|
|
local util = require "tundra.util"
|
|
|
|
LF = '\r\n'
|
|
local UTF_HEADER = '\239\187\191' -- byte mark EF BB BF
|
|
|
|
local VERSION_NUMBER = "12.00"
|
|
local VERSION_YEAR = "2012"
|
|
local HOOKS = {}
|
|
|
|
local msvc_generator = {}
|
|
msvc_generator.__index = msvc_generator
|
|
|
|
local project_types = util.make_lookup_table {
|
|
"Program", "SharedLibrary", "StaticLibrary", "CSharpExe", "CSharpLib", "ObjGroup",
|
|
}
|
|
|
|
local toplevel_stuff = util.make_lookup_table {
|
|
".exe", ".lib", ".dll",
|
|
}
|
|
|
|
local binary_extension = util.make_lookup_table {
|
|
".exe", ".lib", ".dll", ".pdb", ".res", ".obj", ".o", ".a",
|
|
}
|
|
|
|
local header_exts = util.make_lookup_table {
|
|
".h", ".hpp", ".hh", ".inl",
|
|
}
|
|
|
|
-- Scan for sources, following dependencies until those dependencies seem to be
|
|
-- a different top-level unit
|
|
local function get_sources(dag, sources, generated, level, dag_lut)
|
|
for _, output in util.nil_ipairs(dag.outputs) do
|
|
local ext = path.get_extension(output)
|
|
if not binary_extension[ext] then
|
|
generated[output] = true
|
|
sources[output] = true -- pick up generated headers
|
|
end
|
|
end
|
|
|
|
for _, input in util.nil_ipairs(dag.inputs) do
|
|
local ext = path.get_extension(input)
|
|
if not binary_extension[ext] then
|
|
sources[input] = true
|
|
end
|
|
end
|
|
|
|
for _, dep in util.nil_ipairs(dag.deps) do
|
|
if not dag_lut[dep] then -- don't go into other top-level DAGs
|
|
get_sources(dep, sources, generated, level + 1, dag_lut)
|
|
end
|
|
end
|
|
end
|
|
|
|
function get_guid_string(data)
|
|
local sha1 = native.digest_guid(data)
|
|
local guid = sha1:sub(1, 8) .. '-' .. sha1:sub(9,12) .. '-' .. sha1:sub(13,16) .. '-' .. sha1:sub(17,20) .. '-' .. sha1:sub(21, 32)
|
|
assert(#guid == 36)
|
|
return guid:upper()
|
|
end
|
|
|
|
local function get_headers(unit, source_lut, dag_lut, name_to_dags)
|
|
local src_dir = ''
|
|
|
|
if not unit.Decl then
|
|
-- Ignore ExternalLibrary and similar that have no data.
|
|
return
|
|
end
|
|
|
|
if unit.Decl.SourceDir then
|
|
src_dir = unit.Decl.SourceDir .. '/'
|
|
end
|
|
for _, src in util.nil_ipairs(nodegen.flatten_list('*-*-*-*', unit.Decl.Sources)) do
|
|
if type(src) == "string" then
|
|
local ext = path.get_extension(src)
|
|
if header_exts[ext] then
|
|
local full_path = path.normalize(src_dir .. src)
|
|
source_lut[full_path] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local function toplevel(u)
|
|
if type(u) == "string" then
|
|
return type(name_to_dags[u]) ~= "nil"
|
|
end
|
|
|
|
for _, dag in pairs(u.Decl.__DagNodes) do
|
|
if dag_lut[dag] then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Repeat for dependencies ObjGroups
|
|
for _, dep in util.nil_ipairs(nodegen.flatten_list('*-*-*-*', unit.Decl.Depends)) do
|
|
|
|
if not toplevel(dep) then
|
|
get_headers(dep, source_lut, dag_lut)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function make_meta_project(base_dir, data)
|
|
data.Guid = get_guid_string(data.Name)
|
|
data.IdeGenerationHints = { Msvc = { SolutionFolder = "Build System Meta" } }
|
|
data.IsMeta = true
|
|
data.RelativeFilename = data.Name .. ".vcxproj"
|
|
data.Filename = base_dir .. data.RelativeFilename
|
|
data.Type = "meta"
|
|
if not data.Sources then
|
|
data.Sources = {}
|
|
end
|
|
return data
|
|
end
|
|
|
|
local function tundra_cmdline(args)
|
|
local root_dir = native.getcwd()
|
|
return "\"" .. TundraExePath .. "\" -C \"" .. root_dir .. "\" " .. args
|
|
end
|
|
|
|
local function project_regen_commandline(ide_script)
|
|
return tundra_cmdline("-g " .. ide_script)
|
|
end
|
|
|
|
local function make_project_data(units_raw, env, proj_extension, hints, ide_script)
|
|
|
|
-- Filter out stuff we don't care about.
|
|
local units = util.filter(units_raw, function (u)
|
|
return u.Decl.Name and project_types[u.Keyword]
|
|
end)
|
|
|
|
local base_dir = hints.MsvcSolutionDir and (hints.MsvcSolutionDir .. '\\') or env:interpolate('$(OBJECTROOT)$(SEP)')
|
|
native.mkdir(base_dir)
|
|
|
|
local project_by_name = {}
|
|
local all_sources = {}
|
|
local dag_node_lut = {} -- lookup table of all named, top-level DAG nodes
|
|
local name_to_dags = {} -- table mapping unit name to array of dag nodes (for configs)
|
|
|
|
-- Map out all top-level DAG nodes
|
|
for _, unit in ipairs(units) do
|
|
local decl = unit.Decl
|
|
|
|
local dag_nodes = assert(decl.__DagNodes, "no dag nodes for " .. decl.Name)
|
|
for build_id, dag_node in pairs(dag_nodes) do
|
|
dag_node_lut[dag_node] = unit
|
|
local array = name_to_dags[decl.Name]
|
|
if not array then
|
|
array = {}
|
|
name_to_dags[decl.Name] = array
|
|
end
|
|
array[#array + 1] = dag_node
|
|
end
|
|
end
|
|
|
|
local function get_output_project(name)
|
|
if not project_by_name[name] then
|
|
local relative_fn = name .. proj_extension
|
|
project_by_name[name] = {
|
|
Name = name,
|
|
Sources = {},
|
|
RelativeFilename = relative_fn,
|
|
Filename = base_dir .. relative_fn,
|
|
Guid = get_guid_string(name),
|
|
BuildByDefault = hints.BuildAllByDefault,
|
|
}
|
|
end
|
|
return project_by_name[name]
|
|
end
|
|
|
|
-- Sort units based on dependency complexity. We want to visit the leaf nodes
|
|
-- first so that any source file references are picked up as close to the
|
|
-- bottom of the dependency chain as possible.
|
|
local unit_weights = {}
|
|
for _, unit in ipairs(units) do
|
|
local decl = unit.Decl
|
|
local stack = { }
|
|
for _, dag in pairs(decl.__DagNodes) do
|
|
stack[#stack + 1] = dag
|
|
end
|
|
local weight = 0
|
|
while #stack > 0 do
|
|
local node = table.remove(stack)
|
|
if dag_node_lut[node] then
|
|
weight = weight + 1
|
|
end
|
|
for _, dep in util.nil_ipairs(node.deps) do
|
|
stack[#stack + 1] = dep
|
|
end
|
|
end
|
|
unit_weights[unit] = weight
|
|
end
|
|
|
|
table.sort(units, function (a, b)
|
|
return unit_weights[a] < unit_weights[b]
|
|
end)
|
|
|
|
-- Keep track of what source files have already been grabbed by other projects.
|
|
local grabbed_sources = {}
|
|
|
|
for _, unit in ipairs(units) do
|
|
local decl = unit.Decl
|
|
local name = decl.Name
|
|
|
|
local source_lut = {}
|
|
local generated_lut = {}
|
|
for build_id, dag_node in pairs(decl.__DagNodes) do
|
|
get_sources(dag_node, source_lut, generated_lut, 0, dag_node_lut)
|
|
end
|
|
|
|
-- Explicitly add all header files too as they are not picked up from the DAG
|
|
-- Also pick up headers from non-toplevel DAGs we're depending on
|
|
get_headers(unit, source_lut, dag_node_lut, name_to_dags)
|
|
|
|
-- Figure out which project should get this data.
|
|
local output_name = name
|
|
local ide_hints = unit.Decl.IdeGenerationHints
|
|
if ide_hints then
|
|
if ide_hints.OutputProject then
|
|
output_name = ide_hints.OutputProject
|
|
end
|
|
end
|
|
|
|
local proj = get_output_project(output_name)
|
|
|
|
if output_name == name then
|
|
-- This unit is the real thing for this project, not something that's
|
|
-- just being merged into it (like an ObjGroup). Set some more attributes.
|
|
proj.IdeGenerationHints = ide_hints
|
|
proj.DagNodes = decl.__DagNodes
|
|
proj.Unit = unit
|
|
end
|
|
|
|
for src, _ in pairs(source_lut) do
|
|
local norm_src = path.normalize(src)
|
|
if not grabbed_sources[norm_src] then
|
|
grabbed_sources[norm_src] = unit
|
|
local is_generated = generated_lut[src]
|
|
proj.Sources[#proj.Sources+1] = {
|
|
Path = norm_src,
|
|
Generated = is_generated,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Get all accessed Lua files
|
|
local accessed_lua_files = util.table_keys(get_accessed_files())
|
|
|
|
-- Filter out the ones that belong to this build (exclude ones coming from Tundra)
|
|
local function is_non_tundra_lua_file(p)
|
|
return not path.is_absolute(p)
|
|
end
|
|
local function make_src_node(p)
|
|
return { Path = path.normalize(p) }
|
|
end
|
|
local source_list = util.map(util.filter(accessed_lua_files, is_non_tundra_lua_file), make_src_node)
|
|
|
|
local solution_hints = hints.MsvcSolutions
|
|
if not solution_hints then
|
|
print("No IdeGenerationHints.MsvcSolutions specified - using defaults")
|
|
solution_hints = {
|
|
['tundra-generated.sln'] = {}
|
|
}
|
|
end
|
|
|
|
local projects = util.table_values(project_by_name)
|
|
local vanilla_projects = util.clone_array(projects)
|
|
|
|
local solutions = {}
|
|
|
|
-- Create meta project to regenerate solutions/projects. Added to every solution.
|
|
local regen_meta_proj = make_meta_project(base_dir, {
|
|
Name = "00-Regenerate-Projects",
|
|
FriendlyName = "Regenerate Solutions and Projects",
|
|
BuildCommand = project_regen_commandline(ide_script),
|
|
})
|
|
|
|
projects[#projects + 1] = regen_meta_proj
|
|
|
|
for name, data in pairs(solution_hints) do
|
|
local sln_projects
|
|
local ext_projects = {}
|
|
if data.Projects then
|
|
sln_projects = {}
|
|
for _, pname in ipairs(data.Projects) do
|
|
local pp = project_by_name[pname]
|
|
if not pp then
|
|
errorf("can't find project %s for inclusion in %s -- check your MsvcSolutions data", pname, name)
|
|
end
|
|
sln_projects[#sln_projects + 1] = pp
|
|
end
|
|
else
|
|
-- All the projects (that are not meta)
|
|
sln_projects = util.clone_array(vanilla_projects)
|
|
end
|
|
|
|
for _, ext in util.nil_ipairs(data.ExternalProjects) do
|
|
ext_projects[#ext_projects + 1] = ext
|
|
end
|
|
|
|
local meta_proj = make_meta_project(base_dir, {
|
|
Name = "00-tundra-" .. path.drop_suffix(name),
|
|
FriendlyName = "Build This Solution",
|
|
BuildByDefault = true,
|
|
Sources = source_list,
|
|
BuildProjects = util.clone_array(sln_projects),
|
|
})
|
|
|
|
sln_projects[#sln_projects + 1] = regen_meta_proj
|
|
sln_projects[#sln_projects + 1] = meta_proj
|
|
projects[#projects + 1] = meta_proj
|
|
|
|
solutions[#solutions + 1] = {
|
|
Filename = base_dir .. name,
|
|
Projects = sln_projects,
|
|
ExternalProjects = ext_projects,
|
|
BuildSolutionProject = meta_proj,
|
|
}
|
|
end
|
|
|
|
return solutions, projects
|
|
end
|
|
|
|
local cl_tags = {
|
|
['.h'] = 'ClInclude',
|
|
['.hh'] = 'ClInclude',
|
|
['.hpp'] = 'ClInclude',
|
|
['.inl'] = 'ClInclude',
|
|
}
|
|
|
|
local function slurp_file(fn)
|
|
local fh, err = io.open(fn, 'rb')
|
|
if fh then
|
|
local data = fh:read("*all")
|
|
fh:close()
|
|
return data
|
|
end
|
|
return ''
|
|
end
|
|
|
|
local function replace_if_changed(new_fn, old_fn)
|
|
local old_data = slurp_file(old_fn)
|
|
local new_data = slurp_file(new_fn)
|
|
if old_data == new_data then
|
|
os.remove(new_fn)
|
|
return
|
|
end
|
|
printf("Updating %s", old_fn)
|
|
os.remove(old_fn)
|
|
os.rename(new_fn, old_fn)
|
|
end
|
|
|
|
function msvc_generator:generate_solution(fn, projects, ext_projects, solution)
|
|
local sln = io.open(fn .. '.tmp', 'wb')
|
|
sln:write(UTF_HEADER, LF, "Microsoft Visual Studio Solution File, Format Version ", VERSION_NUMBER, LF, "# Visual Studio ", VERSION_YEAR, LF)
|
|
|
|
-- Map folder names to array of projects under that folder
|
|
local sln_folders = {}
|
|
for _, proj in ipairs(projects) do
|
|
local hints = proj.IdeGenerationHints
|
|
local msvc_hints = hints and hints.Msvc or nil
|
|
local folder = msvc_hints and msvc_hints.SolutionFolder or nil
|
|
if folder then
|
|
local projects = sln_folders[folder] or {}
|
|
projects[#projects + 1] = proj
|
|
sln_folders[folder] = projects
|
|
end
|
|
end
|
|
|
|
for _, proj in ipairs(projects) do
|
|
local name = proj.Name
|
|
local fname = proj.RelativeFilename
|
|
local guid = proj.Guid
|
|
sln:write(string.format('Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%s", "%s", "{%s}"', name, fname, guid), LF)
|
|
sln:write('EndProject', LF)
|
|
end
|
|
|
|
-- Dump external projects. Make them depend on everything in this solution being built by Tundra.
|
|
for _, data in util.nil_ipairs(ext_projects) do
|
|
local guid = data.Guid
|
|
local fname = path.normalize(path.join(native.getcwd(), data.Filename))
|
|
local name = path.get_filename_base(fname)
|
|
sln:write(string.format('Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%s", "%s", "{%s}"', name, fname, guid), LF)
|
|
local build_sln_proj = solution.BuildSolutionProject
|
|
if build_sln_proj then
|
|
local meta_guid = build_sln_proj.Guid
|
|
sln:write('\tProjectSection(ProjectDependencies) = postProject', LF)
|
|
sln:write('\t\t{', meta_guid,'} = {', meta_guid,'}', LF)
|
|
sln:write('\tEndProjectSection', LF)
|
|
end
|
|
sln:write('EndProject', LF)
|
|
end
|
|
|
|
for folder_name, _ in pairs(sln_folders) do
|
|
local folder_guid = get_guid_string("folder/" .. folder_name)
|
|
sln:write(string.format('Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "%s", "%s", "{%s}"', folder_name, folder_name, folder_guid), LF)
|
|
sln:write('EndProject', LF)
|
|
end
|
|
|
|
sln:write("Global", LF)
|
|
sln:write("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution", LF)
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
sln:write(string.format('\t\t%s = %s', tuple.MsvcName, tuple.MsvcName), LF)
|
|
end
|
|
sln:write("\tEndGlobalSection", LF)
|
|
|
|
sln:write("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution", LF)
|
|
for _, proj in ipairs(projects) do
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
local leader = string.format('\t\t{%s}.%s.', proj.Guid, tuple.MsvcName)
|
|
sln:write(leader, "ActiveCfg = ", tuple.MsvcName, LF)
|
|
if proj.BuildByDefault then
|
|
sln:write(leader, "Build.0 = ", tuple.MsvcName, LF)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- External projects build by default, and after Tundra is done (depends on "Build this solution").
|
|
for _, proj in util.nil_ipairs(ext_projects) do
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
local leader = string.format('\t\t{%s}.%s.', proj.Guid, tuple.MsvcName)
|
|
sln:write(leader, "ActiveCfg = ", tuple.MsvcName, LF)
|
|
if not proj.Platform or proj.Platform == tuple.MsvcPlatform then
|
|
sln:write(leader, "Build.0 = ", tuple.MsvcName, LF)
|
|
end
|
|
end
|
|
end
|
|
sln:write("\tEndGlobalSection", LF)
|
|
|
|
sln:write("\tGlobalSection(SolutionProperties) = preSolution", LF)
|
|
sln:write("\t\tHideSolutionNode = FALSE", LF)
|
|
sln:write("\tEndGlobalSection", LF)
|
|
|
|
sln:write("\tGlobalSection(NestedProjects) = preSolution", LF)
|
|
for folder_name, projects in pairs(sln_folders) do
|
|
local folder_guid = get_guid_string("folder/" .. folder_name)
|
|
for _, project in ipairs(projects) do
|
|
sln:write(string.format('\t\t{%s} = {%s}', project.Guid, folder_guid), LF)
|
|
end
|
|
end
|
|
sln:write("\tEndGlobalSection", LF)
|
|
|
|
sln:write("EndGlobal", LF)
|
|
sln:close()
|
|
|
|
replace_if_changed(fn .. ".tmp", fn)
|
|
end
|
|
|
|
local function find_dag_node_for_config(project, tuple)
|
|
local build_id = string.format("%s-%s-%s", tuple.Config.Name, tuple.Variant.Name, tuple.SubVariant)
|
|
local nodes = project.DagNodes
|
|
if not nodes then
|
|
return nil
|
|
end
|
|
|
|
if nodes[build_id] then
|
|
return nodes[build_id]
|
|
end
|
|
errorf("couldn't find config %s for project %s (%d dag nodes) - available: %s",
|
|
build_id, project.Name, #nodes, table.concat(util.table_keys(nodes), ", "))
|
|
end
|
|
|
|
function msvc_generator:generate_project(project, all_projects)
|
|
local fn = project.Filename
|
|
local p = assert(io.open(fn .. ".tmp", 'wb'))
|
|
p:write('<?xml version="1.0" encoding="utf-8"?>', LF)
|
|
p:write('<Project')
|
|
p:write(' DefaultTargets="Build"')
|
|
p:write(' ToolsVersion="4.0"')
|
|
p:write(' xmlns="http://schemas.microsoft.com/developer/msbuild/2003"')
|
|
p:write('>', LF)
|
|
|
|
-- List all project configurations
|
|
p:write('\t<ItemGroup Label="ProjectConfigurations">', LF)
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
p:write('\t\t<ProjectConfiguration Include="', tuple.MsvcName, '">', LF)
|
|
p:write('\t\t\t<Configuration>', tuple.MsvcConfiguration, '</Configuration>', LF)
|
|
p:write('\t\t\t<Platform>', tuple.MsvcPlatform, '</Platform>', LF)
|
|
p:write('\t\t</ProjectConfiguration>', LF)
|
|
end
|
|
p:write('\t</ItemGroup>', LF)
|
|
|
|
p:write('\t<PropertyGroup Label="Globals">', LF)
|
|
p:write('\t\t<ProjectGuid>{', project.Guid, '}</ProjectGuid>', LF)
|
|
p:write('\t\t<Keyword>MakeFileProj</Keyword>', LF)
|
|
if project.FriendlyName then
|
|
p:write('\t\t<ProjectName>', project.FriendlyName, '</ProjectName>', LF)
|
|
end
|
|
|
|
if HOOKS.global_properties then
|
|
HOOKS.global_properties(p, project)
|
|
end
|
|
|
|
p:write('\t</PropertyGroup>', LF)
|
|
p:write('\t<PropertyGroup>', LF)
|
|
if VERSION_YEAR == '2012' then
|
|
p:write('\t\t<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>', LF)
|
|
end
|
|
p:write('\t</PropertyGroup>', LF)
|
|
|
|
p:write('\t<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />', LF)
|
|
|
|
-- Mark all project configurations as makefile-type projects
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
p:write('\t<PropertyGroup Condition="\'$(Configuration)|$(Platform)\'==\'', tuple.MsvcName, '\'" Label="Configuration">', LF)
|
|
p:write('\t\t<ConfigurationType>Makefile</ConfigurationType>', LF)
|
|
p:write('\t\t<UseDebugLibraries>true</UseDebugLibraries>', LF) -- I have no idea what this setting affects
|
|
if VERSION_YEAR == '2012' then
|
|
p:write('\t\t<PlatformToolset>v110</PlatformToolset>', LF) -- I have no idea what this setting affects
|
|
elseif VERSION_YEAR == '2013' then
|
|
p:write('\t\t<PlatformToolset>v120</PlatformToolset>', LF) -- I have no idea what this setting affects
|
|
end
|
|
p:write('\t</PropertyGroup>', LF)
|
|
end
|
|
|
|
p:write('\t<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />', LF)
|
|
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
p:write('\t<PropertyGroup Condition="\'$(Configuration)|$(Platform)\'==\'', tuple.MsvcName, '\'">', LF)
|
|
|
|
local dag_node = find_dag_node_for_config(project, tuple)
|
|
local include_paths, defines
|
|
if dag_node then
|
|
local env = dag_node.src_env
|
|
local paths = util.map(env:get_list("CPPPATH"), function (p)
|
|
local ip = path.normalize(env:interpolate(p))
|
|
if not path.is_absolute(ip) then
|
|
ip = native.getcwd() .. '\\' .. ip
|
|
end
|
|
return ip
|
|
end)
|
|
include_paths = table.concat(paths, ';')
|
|
local ext_paths = env:get_external_env_var('INCLUDE')
|
|
if ext_paths then
|
|
include_paths = include_paths .. ';' .. ext_paths
|
|
end
|
|
defines = env:interpolate("$(CPPDEFS:j;)")
|
|
else
|
|
include_paths = ''
|
|
defines = ''
|
|
end
|
|
|
|
local root_dir = native.getcwd()
|
|
local build_id = string.format("%s-%s-%s", tuple.Config.Name, tuple.Variant.Name, tuple.SubVariant)
|
|
local base = "\"" .. TundraExePath .. "\" -C \"" .. root_dir .. "\" "
|
|
local build_cmd = base .. build_id
|
|
local clean_cmd = base .. "--clean " .. build_id
|
|
local rebuild_cmd = base .. "--rebuild " .. build_id
|
|
|
|
if project.BuildCommand then
|
|
build_cmd = project.BuildCommand
|
|
clean_cmd = ""
|
|
rebuild_cmd = ""
|
|
elseif not project.IsMeta then
|
|
build_cmd = build_cmd .. " " .. project.Name
|
|
clean_cmd = clean_cmd .. " " .. project.Name
|
|
rebuild_cmd = rebuild_cmd .. " " .. project.Name
|
|
else
|
|
local all_projs_str = table.concat(
|
|
util.map(assert(project.BuildProjects), function (p) return p.Name end), ' ')
|
|
build_cmd = build_cmd .. " " .. all_projs_str
|
|
clean_cmd = clean_cmd .. " " .. all_projs_str
|
|
rebuild_cmd = rebuild_cmd .. " " .. all_projs_str
|
|
end
|
|
|
|
p:write('\t\t<NMakeBuildCommandLine>', build_cmd, '</NMakeBuildCommandLine>', LF)
|
|
p:write('\t\t<NMakeOutput></NMakeOutput>', LF)
|
|
p:write('\t\t<NMakeCleanCommandLine>', clean_cmd, '</NMakeCleanCommandLine>', LF)
|
|
p:write('\t\t<NMakeReBuildCommandLine>', rebuild_cmd, '</NMakeReBuildCommandLine>', LF)
|
|
p:write('\t\t<NMakePreprocessorDefinitions>', defines, ';$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>', LF)
|
|
p:write('\t\t<NMakeIncludeSearchPath>', include_paths, ';$(NMakeIncludeSearchPath)</NMakeIncludeSearchPath>', LF)
|
|
p:write('\t\t<NMakeForcedIncludes>$(NMakeForcedIncludes)</NMakeForcedIncludes>', LF)
|
|
p:write('\t</PropertyGroup>', LF)
|
|
end
|
|
|
|
if HOOKS.pre_sources then
|
|
HOOKS.pre_sources(p, project)
|
|
end
|
|
|
|
-- Emit list of source files
|
|
p:write('\t<ItemGroup>', LF)
|
|
for _, record in ipairs(project.Sources) do
|
|
local path_str = assert(record.Path)
|
|
if not path.is_absolute(path_str) then
|
|
path_str = native.getcwd() .. '\\' .. path_str
|
|
end
|
|
local ext = path.get_extension(path_str)
|
|
local cl_tag = cl_tags[ext] or 'ClCompile'
|
|
p:write('\t\t<', cl_tag,' Include="', path_str, '" />', LF)
|
|
end
|
|
p:write('\t</ItemGroup>', LF)
|
|
|
|
local post_src_hook = HOOKS.post_sources
|
|
if post_src_hook then
|
|
post_src_hook(p, project)
|
|
end
|
|
|
|
p:write('\t<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />', LF)
|
|
|
|
if VERSION_YEAR == "2012" then
|
|
-- Import helper msbuild stuff to make build aborting work propertly in VS2012
|
|
local xml = path.normalize(TundraScriptDir .. '/tundra/ide/msvc-rules.xml')
|
|
p:write('\t<Import Project="', xml, '" />', LF)
|
|
end
|
|
|
|
p:write('</Project>', LF)
|
|
p:close()
|
|
|
|
replace_if_changed(fn .. ".tmp", fn)
|
|
end
|
|
|
|
local function get_common_dir(sources)
|
|
local dir_tokens = {}
|
|
for _, src in ipairs(sources) do
|
|
local path = assert(src.Path)
|
|
if not tundra.path.is_absolute(path) then
|
|
local subdirs = {}
|
|
for subdir in path:gmatch("([^\\\]+)\\") do
|
|
subdirs[#subdirs + 1] = subdir
|
|
end
|
|
|
|
if #dir_tokens == 0 then
|
|
dir_tokens = subdirs
|
|
else
|
|
for i = 1, #dir_tokens do
|
|
if dir_tokens[i] ~= subdirs[i] then
|
|
while #dir_tokens >= i do
|
|
table.remove(dir_tokens)
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local result = table.concat(dir_tokens, '\\')
|
|
if #result > 0 then
|
|
result = result .. '\\'
|
|
end
|
|
return result
|
|
end
|
|
|
|
function msvc_generator:generate_project_filters(project)
|
|
local fn = project.Filename .. ".filters"
|
|
local p = assert(io.open(fn .. ".tmp", 'wb'))
|
|
p:write('<?xml version="1.0" encoding="Windows-1252"?>', LF)
|
|
p:write('<Project')
|
|
p:write(' ToolsVersion="4.0"')
|
|
p:write(' xmlns="http://schemas.microsoft.com/developer/msbuild/2003"')
|
|
p:write('>', LF)
|
|
|
|
local common_dir = get_common_dir(util.filter(project.Sources, function (s) return not s.Generated end))
|
|
local common_dir_gen = get_common_dir(util.filter(project.Sources, function (s) return s.Generated end))
|
|
|
|
local filters = {}
|
|
local sources = {}
|
|
|
|
-- Mangle source filenames, and find which filters need to be created
|
|
for _, record in ipairs(project.Sources) do
|
|
local fn = record.Path
|
|
local common_start = record.Generated and common_dir_gen or common_dir
|
|
if fn:find(common_start, 1, true) then
|
|
fn = fn:sub(#common_start+1)
|
|
end
|
|
|
|
local dir, filename = path.split(fn)
|
|
|
|
if dir == '.' then
|
|
dir = nil
|
|
end
|
|
|
|
local abs_path = record.Path
|
|
if not path.is_absolute(abs_path) then
|
|
abs_path = native.getcwd() .. '\\' .. abs_path
|
|
end
|
|
|
|
if record.Generated then
|
|
dir = 'Generated Files'
|
|
end
|
|
|
|
sources[#sources + 1] = {
|
|
FullPath = abs_path,
|
|
Directory = dir,
|
|
}
|
|
|
|
-- Register filter and all its parents
|
|
while dir and dir ~= '.' do
|
|
filters[dir] = true
|
|
dir, _ = path.split(dir)
|
|
end
|
|
end
|
|
|
|
-- Emit list of filters
|
|
p:write('\t<ItemGroup>', LF)
|
|
for filter_name, _ in pairs(filters) do
|
|
if filter_name ~= "" then
|
|
filter_guid = get_guid_string(filter_name)
|
|
p:write('\t\t<Filter Include="', filter_name, '">', LF)
|
|
p:write('\t\t\t<UniqueIdentifier>{', filter_guid, '}</UniqueIdentifier>', LF)
|
|
p:write('\t\t</Filter>', LF)
|
|
end
|
|
end
|
|
p:write('\t</ItemGroup>', LF)
|
|
|
|
-- Emit list of source files
|
|
p:write('\t<ItemGroup>', LF)
|
|
for _, source in ipairs(sources) do
|
|
local ext = path.get_extension(source.FullPath)
|
|
local cl_tag = cl_tags[ext] or 'ClCompile'
|
|
if not source.Directory then
|
|
p:write('\t\t<', cl_tag, ' Include="', source.FullPath, '" />', LF)
|
|
else
|
|
p:write('\t\t<', cl_tag, ' Include="', source.FullPath, '">', LF)
|
|
p:write('\t\t\t<Filter>', source.Directory, '</Filter>', LF)
|
|
p:write('\t\t</', cl_tag, '>', LF)
|
|
end
|
|
end
|
|
p:write('\t</ItemGroup>', LF)
|
|
|
|
p:write('</Project>', LF)
|
|
|
|
p:close()
|
|
|
|
replace_if_changed(fn .. ".tmp", fn)
|
|
end
|
|
|
|
function msvc_generator:generate_project_user(project)
|
|
local fn = project.Filename .. ".user"
|
|
-- Don't overwrite user settings
|
|
do
|
|
local p, err = io.open(fn, 'rb')
|
|
if p then
|
|
p:close()
|
|
return
|
|
end
|
|
end
|
|
|
|
local p = assert(io.open(fn, 'wb'))
|
|
p:write('<?xml version="1.0" encoding="utf-8"?>', LF)
|
|
p:write('<Project')
|
|
p:write(' ToolsVersion="4.0"')
|
|
p:write(' xmlns="http://schemas.microsoft.com/developer/msbuild/2003"')
|
|
p:write('>', LF)
|
|
|
|
for _, tuple in ipairs(self.config_tuples) do
|
|
local dag_node = find_dag_node_for_config(project, tuple)
|
|
if dag_node then
|
|
local exe = nil
|
|
for _, output in util.nil_ipairs(dag_node.outputs) do
|
|
if output:match("%.exe") then
|
|
exe = output
|
|
break
|
|
end
|
|
end
|
|
if exe then
|
|
p:write('\t<PropertyGroup Condition="\'$(Configuration)|$(Platform)\'==\'', tuple.MsvcName, '\'">', LF)
|
|
p:write('\t\t<LocalDebuggerCommand>', native.getcwd() .. '\\' .. exe, '</LocalDebuggerCommand>', LF)
|
|
p:write('\t\t<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>', LF)
|
|
p:write('\t\t<LocalDebuggerWorkingDirectory>', native.getcwd(), '</LocalDebuggerWorkingDirectory>', LF)
|
|
p:write('\t</PropertyGroup>', LF)
|
|
end
|
|
end
|
|
end
|
|
|
|
p:write('</Project>', LF)
|
|
|
|
p:close()
|
|
end
|
|
|
|
function msvc_generator:generate_files(ngen, config_tuples, raw_nodes, env, default_names, hints, ide_script)
|
|
assert(config_tuples and #config_tuples > 0)
|
|
|
|
if not hints then
|
|
hints = {}
|
|
end
|
|
|
|
local complained_mappings = {}
|
|
|
|
self.msvc_platforms = {}
|
|
local msvc_hints = hints.Msvc or {}
|
|
local variant_mappings = msvc_hints.VariantMappings or {}
|
|
local platform_mappings = msvc_hints.PlatformMappings or {}
|
|
local full_mappings = msvc_hints.FullMappings or {}
|
|
|
|
for _, tuple in ipairs(config_tuples) do
|
|
local build_id = string.format("%s-%s-%s", tuple.Config.Name, tuple.Variant.Name, tuple.SubVariant)
|
|
if full_mappings[build_id] then
|
|
local m = full_mappings[build_id]
|
|
tuple.MsvcConfiguration = assert(m.Config)
|
|
tuple.MsvcPlatform = assert(m.Platform)
|
|
elseif variant_mappings[tuple.Variant.Name] then
|
|
tuple.MsvcConfiguration = variant_mappings[tuple.Variant.Name]
|
|
elseif variant_mappings[tuple.Variant.Name .. "-" .. tuple.SubVariant] then
|
|
tuple.MsvcConfiguration = variant_mappings[tuple.Variant.Name .. "-" .. tuple.SubVariant]
|
|
else
|
|
tuple.MsvcConfiguration = tuple.Variant.Name
|
|
end
|
|
|
|
-- Use IdeGenerationHints.Msvc.PlatformMappings table to map tundra
|
|
-- configurations to MSVC platform names. Note that this isn't a huge deal
|
|
-- for building stuff as Tundra doesn't care about this setting. But it
|
|
-- might influence the choice of debugger and affect include paths for
|
|
-- things like Intellisense that certain users may care about.
|
|
if not tuple.MsvcPlatform then
|
|
tuple.MsvcPlatform = platform_mappings[tuple.Config.Name]
|
|
end
|
|
|
|
-- If we didn't find anything, warn and then default to Win32, which VS
|
|
-- will always accept (or so one would assume)
|
|
if not tuple.MsvcPlatform then
|
|
tuple.MsvcPlatform = "Win32"
|
|
if not complained_mappings[tuple.Config.Name] then
|
|
printf("warning: No VS platform mapping for %s, mapping to Win32", tuple.Config.Name)
|
|
print("(Add one to IdeGenerationHints.Msvc.PlatformMappings to override)")
|
|
complained_mappings[tuple.Config.Name] = true
|
|
end
|
|
end
|
|
tuple.MsvcName = tuple.MsvcConfiguration .. "|" .. tuple.MsvcPlatform
|
|
self.msvc_platforms[tuple.MsvcPlatform] = true
|
|
end
|
|
|
|
self.config_tuples = config_tuples
|
|
|
|
printf("Generating Visual Studio projects for %d configurations/variants", #config_tuples)
|
|
|
|
-- Figure out where we're going to store the projects
|
|
local solutions, projects = make_project_data(raw_nodes, env, ".vcxproj", hints, ide_script)
|
|
|
|
local proj_lut = {}
|
|
for _, p in ipairs(projects) do
|
|
proj_lut[p.Name] = p
|
|
end
|
|
|
|
for _, sln in pairs(solutions) do
|
|
self:generate_solution(sln.Filename, sln.Projects, sln.ExternalProjects, sln)
|
|
end
|
|
|
|
for _, proj in ipairs(projects) do
|
|
self:generate_project(proj, projects)
|
|
self:generate_project_filters(proj)
|
|
self:generate_project_user(proj)
|
|
end
|
|
end
|
|
|
|
function setup(version_short, version_year, hooks)
|
|
VERSION_NUMBER = version_short
|
|
VERSION_YEAR = version_year
|
|
if hooks then
|
|
HOOKS = hooks
|
|
end
|
|
nodegen.set_ide_backend(function(...)
|
|
local state = setmetatable({}, msvc_generator)
|
|
state:generate_files(...)
|
|
end)
|
|
end
|
|
|