--
-- Name:        premake-ninja/ninja.lua
-- Purpose:     Define the ninja action.
-- Author:      Dmitry Ivanov
-- Created:     2015/07/04
-- Copyright:   (c) 2015 Dmitry Ivanov
--

local p = premake
local tree = p.tree
local project = p.project
local config = p.config
local fileconfig = p.fileconfig

premake.modules.ninja = {}
local ninja = p.modules.ninja

local function get_key(cfg)
	if cfg.platform then
		return cfg.project.name .. "_" .. cfg.buildcfg .. "_" .. cfg.platform
	else
		return cfg.project.name .. "_" .. cfg.buildcfg
	end
end

local build_cache = {}

local function add_build(cfg, output, extra_outputs, command, args)
	local cached = build_cache[output]
	if cached ~= nil then
		if extra_outputs == cached.extra_outputs
			and command == cached.command
			and table.equals(args or {}, cached.args or {})
		then
			-- custom_command rule is identical for each configuration (contrary to other rules)
			-- So we can compare extra parameter
			if string.startswith(cached.command, "custom_command") then
				p.w("# INFO: Rule ignored, same as " .. cached.cfg_key)
			else
				local cfg_key = get_key(cfg)
				p.warn(cached.cfg_key .. " and " .. cfg_key .. " both generate (differently?) " .. output .. ". Ignoring " .. cfg_key)
				p.w("# WARNING: Rule ignored, using the one from " .. cached.cfg_key)
			end
		else
			local cfg_key = get_key(cfg)
			p.warn(cached.cfg_key .. " and " .. cfg_key .. " both generate differently " .. output .. ". Ignoring " .. cfg_key)
			p.w("# ERROR: Rule ignored, using the one from " .. cached.cfg_key)
		end
		p.w("# build " .. output .. ": " .. command)
		for i, arg in ipairs(args or {}) do
			p.w("#   " .. arg)
		end
		return
	end
	p.w("build " .. output .. ": " .. command)
	for i, arg in ipairs(args or {}) do
		p.w("  " .. arg)
	end
	build_cache[output] = {
		cfg_key = get_key(cfg),
		extra_outputs = extra_outputs,
		command = command,
		args = args
	}
end

function ninja.esc(value)
	value = value:gsub("%$", "$$") -- TODO maybe there is better way
	value = value:gsub(":", "$:")
	value = value:gsub("\n", "$\n")
	value = value:gsub(" ", "$ ")
	return value
end

function ninja.quote(value)
	value = value:gsub("\\", "\\\\")
	value = value:gsub("'", "\\'")
	value = value:gsub("\"", "\\\"")

	return "\"" .. value .. "\""
end

-- in some cases we write file names in rule commands directly
-- so we need to propely escape them
function ninja.shesc(value)
	if type(value) == "table" then
		local result = {}
		local n = #value
		for i = 1, n do
			table.insert(result, ninja.shesc(value[i]))
		end
		return result
	end

	if value:find(" ") then
		return ninja.quote(value)
	end
	return value
end

-- generate solution that will call ninja for projects
function ninja.generateWorkspace(wks)
	local oldGetDefaultSeparator = path.getDefaultSeparator
	path.getDefaultSeparator = function() return "/" end

	p.w("# solution build file")
	p.w("# generated with premake ninja")
	p.w("")

	p.w("# build projects")
	local cfgs = {} -- key is concatenated name or variant name, value is string of outputs names
	local key = ""
	local cfg_first = nil
	local cfg_first_lib = nil

	for prj in p.workspace.eachproject(wks) do
		if p.action.supports(prj.kind) and prj.kind ~= p.NONE then
			for cfg in p.project.eachconfig(prj) do
				key = prj.name .. "_" .. cfg.buildcfg

				if cfg.platform ~= nil then key = key .. "_" .. cfg.platform end

				if not cfgs[cfg.buildcfg] then cfgs[cfg.buildcfg] = "" end
				cfgs[cfg.buildcfg] = cfgs[cfg.buildcfg] .. key .. " "

				-- set first configuration name
				if (cfg_first == nil) and (cfg.kind == p.CONSOLEAPP or cfg.kind == p.WINDOWEDAPP) then
					cfg_first = key
				end
				if (cfg_first_lib == nil) and (cfg.kind == p.STATICLIB or cfg.kind == p.SHAREDLIB) then
					cfg_first_lib = key
				end

				-- include other ninja file
				p.w("subninja " .. p.esc(ninja.projectCfgFilename(cfg, true)))
			end
		end
	end

	if cfg_first == nil then cfg_first = cfg_first_lib end

	p.w("")

	p.w("# targets")
	for cfg, outputs in pairs(cfgs) do
		p.w("build " .. p.esc(cfg) .. ": phony " .. outputs)
	end
	p.w("")

	p.w("# default target")
	p.w("default " .. p.esc(cfg_first))
	p.w("")

	path.getDefaultSeparator = oldGetDefaultSeparator
end

function ninja.list(value)
	if #value > 0 then
		return " " .. table.concat(value, " ")
	else
		return ""
	end
end

local function shouldcompileasc(filecfg)
	if filecfg.compileas and filecfg.compileas ~= "Default" then
		return p.languages.isc(filecfg.compileas)
	end
	return path.iscfile(filecfg.abspath)
end

local function shouldcompileascpp(filecfg)
	if filecfg.compileas and filecfg.compileas ~= "Default" then
		return p.languages.iscpp(filecfg.compileas)
	end
	return path.iscppfile(filecfg.abspath)
end

local function getDefaultToolsetFromOs()
	local system_name = os.target()

	if system_name == "windows" then
		return "msc"
	elseif system_name == "macosx" then
		return "clang"
	elseif system_name == "linux" then
		return "gcc"
	else
		p.warnOnce("unknown_system", "no toolchain set and unknown system " .. system_name .. " so assuming toolchain is gcc")
		return "gcc"
	end
end

local function getToolsetExecutables(cfg, toolset, toolset_name)
	local cc = ""
	local cxx = ""
	local ar = ""
	local link = ""
	local rc = ""

	if toolset_name == "msc" then
		-- TODO premake doesn't set tools names for msc, do we want to fix it ?
		cc = "cl"
		cxx = "cl"
		ar = "lib"
		link = "cl"
		rc = "rc"
	elseif toolset_name == "clang" or toolset_name == "gcc" then
		if not cfg.gccprefix then cfg.gccprefix = "" end
		cc = toolset.gettoolname(cfg, "cc")
		cxx = toolset.gettoolname(cfg, "cxx")
		ar = toolset.gettoolname(cfg, "ar")
		link = toolset.gettoolname(cfg, iif(cfg.language == "C", "cc", "cxx"))
	else
		p.error("unknown toolchain " .. toolset_name)
	end
	return cc, cxx, ar, link, rc
end

local function getFileDependencies(cfg)
	local dependencies = {}
	if #cfg.prebuildcommands > 0 or cfg.prebuildmessage then
		dependencies = {"prebuild_" .. get_key(cfg)}
	end
	for i = 1, #cfg.dependson do
		table.insert(dependencies, cfg.dependson[i] .. "_" .. cfg.buildcfg)
	end
	return dependencies
end

local function getcflags(toolset, cfg, filecfg)
	local buildopt = ninja.list(filecfg.buildoptions)
	local cppflags = ninja.list(toolset.getcppflags(filecfg))
	local cflags = ninja.list(toolset.getcflags(filecfg))
	local defines = ninja.list(table.join(toolset.getdefines(filecfg.defines), toolset.getundefines(filecfg.undefines)))
	local includes = ninja.list(toolset.getincludedirs(cfg, filecfg.includedirs, filecfg.externalincludedirs))
	local forceincludes = ninja.list(toolset.getforceincludes(cfg))

	return buildopt .. cppflags .. cflags .. defines .. includes .. forceincludes
end

local function getcxxflags(toolset, cfg, filecfg)
	local buildopt = ninja.list(filecfg.buildoptions)
	local cppflags = ninja.list(toolset.getcppflags(filecfg))
	local cxxflags = ninja.list(toolset.getcxxflags(filecfg))
	local defines = ninja.list(table.join(toolset.getdefines(filecfg.defines), toolset.getundefines(filecfg.undefines)))
	local includes = ninja.list(toolset.getincludedirs(cfg, filecfg.includedirs, filecfg.externalincludedirs))
	local forceincludes = ninja.list(toolset.getforceincludes(cfg))
	return buildopt .. cppflags .. cxxflags .. defines .. includes .. forceincludes
end

local function getldflags(toolset, cfg)
	local ldflags = ninja.list(table.join(toolset.getLibraryDirectories(cfg), toolset.getldflags(cfg), cfg.linkoptions))

	-- experimental feature, change install_name of shared libs
	--if (toolset_name == "clang") and (cfg.kind == p.SHAREDLIB) and ninja.endsWith(cfg.buildtarget.name, ".dylib") then
	--	ldflags = ldflags .. " -install_name " .. cfg.buildtarget.name
	--end
	return ldflags
end

local function prebuild_rule(cfg)
	if #cfg.prebuildcommands > 0 or cfg.prebuildmessage then
		local commands = {}
		if cfg.prebuildmessage then
			commands = {os.translateCommandsAndPaths("{ECHO} " .. cfg.prebuildmessage, cfg.project.basedir, cfg.project.location)}
		end
		commands = table.join(commands, os.translateCommandsAndPaths(cfg.prebuildcommands, cfg.project.basedir, cfg.project.location))
		if (#commands > 1) then
			commands = 'sh -c ' .. ninja.quote(table.implode(commands,"","",";"))
		else
			commands = commands[1]
		end
		p.w("rule run_prebuild")
		p.w("  command = " .. p.esc(commands))
		p.w("  description = prebuild")
		p.w("")
	end
end

local function postbuild_rule(cfg)
	if #cfg.postbuildcommands > 0 or cfg.postbuildmessage then
		local commands = {}
		if cfg.postbuildmessage then
			commands = {os.translateCommandsAndPaths("{ECHO} " .. cfg.postbuildmessage, cfg.project.basedir, cfg.project.location)}
		end
		commands = table.join(commands, os.translateCommandsAndPaths(cfg.postbuildcommands, cfg.project.basedir, cfg.project.location))
		if (#commands > 1) then
			commands = 'sh -c ' .. ninja.quote(table.implode(commands,"","",";"))
		else
			commands = commands[1]
		end
		p.w("rule run_postbuild")
		p.w("  command = " .. p.esc(commands))
		p.w("  description = postbuild")
		p.w("")
	end
end

local function compilation_rules(cfg, toolset, toolset_name, pch)
	---------------------------------------------------- figure out toolset executables
	local cc, cxx, ar, link, rc = getToolsetExecutables(cfg, toolset, toolset_name)

	local all_cflags = getcflags(toolset, cfg, cfg)
	local all_cxxflags = getcxxflags(toolset, cfg, cfg)
	local all_ldflags = getldflags(toolset, cfg)

	if toolset_name == "msc" then
		-- for some reason Visual Studio add this libraries as "defaults" and premake doesn't tell us this
		local default_msvc_libs = " kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib"

		p.w("rule cc")
		p.w("  command = " .. cc .. all_cflags .. " /nologo /showIncludes -c $in /Fo$out")
		p.w("  description = cc $out")
		p.w("  deps = msvc")
		p.w("")
		p.w("rule cxx")
		p.w("  command = " .. cxx .. all_cxxflags .. " /nologo /showIncludes -c $in /Fo$out")
		p.w("  description = cxx $out")
		p.w("  deps = msvc")
		p.w("")
		p.w("rule cc_flags")
		p.w("  command = " .. cc .. " $CFLAGS" .. " /nologo /showIncludes -c $in /Fo$out")
		p.w("  description = cc $out")
		p.w("  deps = msvc")
		p.w("")
		p.w("rule cxx_flags")
		p.w("  command = " .. cxx .. " $CXXFLAGS" .. " /nologo /showIncludes -c $in /Fo$out")
		p.w("  description = cxx $out")
		p.w("  deps = msvc")
		p.w("")
		p.w("rule rc")
		p.w("  command = " .. rc .. " /nologo /fo$out $in")
		p.w("  description = rc $out")
		p.w("")
		if cfg.kind == p.STATICLIB then
			p.w("rule ar")
			p.w("  command = " .. ar .. " $in /nologo -OUT:$out")
			p.w("  description = ar $out")
			p.w("")
		else
			p.w("rule link")
			p.w("  command = " .. link .. " $in" .. ninja.list(ninja.shesc(toolset.getlinks(cfg, true))) .. default_msvc_libs .. " /link" .. all_ldflags .. " /nologo /out:$out")
			p.w("  description = link $out")
			p.w("")
		end
	elseif toolset_name == "clang" then
		local force_include_pch = ""
		if pch then
			force_include_pch = " -include " .. p.esc(pch.placeholder)
			p.w("rule build_pch")
			p.w("  command = " .. iif(cfg.language == "C", cc .. all_cflags .. " -x c-header", cxx .. all_cxxflags .. " -x c++-header")  .. " -H -MMD -MF $out.d -c -o $out $in")
			p.w("  description = build_pch $out")
			p.w("  depfile = $out.d")
			p.w("  deps = gcc")
		end
		p.w("rule cc")
		p.w("  command = " .. cc .. all_cflags .. force_include_pch .. " -x c -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cc $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cxx")
		p.w("  command = " .. cxx .. all_cxxflags .. force_include_pch .. " -x c++ -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cxx $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cc_flags")
		p.w("  command = " .. cc .. " $CFLAGS".. force_include_pch .. " -x c -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cc $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cxx_flags")
		p.w("  command = " .. cxx .. " $CXXFLAGS" .. force_include_pch .. " -x c++ -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cxx $out")
		p.w("  deps = msvc")
		p.w("")
		if cfg.kind == p.STATICLIB then
			p.w("rule ar")
			p.w("  command = " .. ar .. " rcs $out $in")
			p.w("  description = ar $out")
			p.w("")
		else
			p.w("rule link")
			p.w("  command = " .. link .. " -o $out $in" .. ninja.list(ninja.shesc(toolset.getlinks(cfg, true))) .. all_ldflags)
			p.w("  description = link $out")
			p.w("")
		end
	elseif toolset_name == "gcc" then
		local force_include_pch = ""
		if pch then
			force_include_pch = " -include " .. p.esc(pch.placeholder)
			p.w("rule build_pch")
			p.w("  command = " .. iif(cfg.language == "C", cc .. all_cflags .. " -x c-header", cxx .. all_cxxflags .. " -x c++-header")  .. " -H -MMD -MF $out.d -c -o $out $in")
			p.w("  description = build_pch $out")
			p.w("  depfile = $out.d")
			p.w("  deps = gcc")
		end
		p.w("rule cc")
		p.w("  command = " .. cc .. all_cflags .. force_include_pch .. " -x c -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cc $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cxx")
		p.w("  command = " .. cxx .. all_cxxflags .. force_include_pch .. " -x c++ -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cxx $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cc_flags")
		p.w("  command = " .. cc .. " $CFLAGS".. force_include_pch .. " -x c -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cc $out")
		p.w("  depfile = $out.d")
		p.w("  deps = gcc")
		p.w("")
		p.w("rule cxx_flags")
		p.w("  command = " .. cxx .. " $CXXFLAGS" .. force_include_pch .. " -x c++ -MMD -MF $out.d -c -o $out $in")
		p.w("  description = cxx $out")
		p.w("  deps = msvc")
		p.w("")
		if cfg.kind == p.STATICLIB then
			p.w("rule ar")
			p.w("  command = " .. ar .. " rcs $out $in")
			p.w("  description = ar $out")
			p.w("")
		else
			p.w("rule link")
			p.w("  command = " .. link .. " -o $out $in" .. ninja.list(ninja.shesc(toolset.getlinks(cfg, true))) .. all_ldflags)
			p.w("  description = link $out")
			p.w("")
		end
	end
end

local function custom_command_rule()
	p.w("rule custom_command")
	p.w("  command = $CUSTOM_COMMAND")
	p.w("  description = $CUSTOM_DESCRIPTION")
	p.w("")
end

local function collect_generated_files(prj, cfg)
	local generated_files = {}
	tree.traverse(project.getsourcetree(prj), {
	onleaf = function(node, depth)
		function append_to_generated_files(filecfg)
			local output = project.getrelative(prj, filecfg.buildoutputs[1])
			table.insert(generated_files, p.esc(output))
		end
		local filecfg = fileconfig.getconfig(node, cfg)
		local rule = p.global.getRuleForFile(node.name, prj.rules)
		if fileconfig.hasCustomBuildRule(filecfg) then
			append_to_generated_files(filecfg)
		elseif 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)
			append_to_generated_files(rulecfg)
		end
	end,
	}, false, 1)
	return generated_files
end

local function pch_build(cfg, pch)
	local pch_dependency = ""
	if pch then
		pch_dependency = " | " .. pch.gch
		add_build(cfg, p.esc(pch.gch), "", "build_pch " .. p.esc(pch.input))
	end
	return pch_dependency
end

local function custom_command_build(prj, cfg, filecfg, filename, file_dependencies)
	local output = project.getrelative(prj, filecfg.buildoutputs[1])
	local inputs = ""
	if #filecfg.buildinputs > 0 then
		inputs = table.implode(filecfg.buildinputs," ","","")
	end

	local commands = {}
	if filecfg.buildmessage then
		commands = {os.translateCommandsAndPaths("{ECHO} " .. filecfg.buildmessage, prj.basedir, prj.location)}
	end
	commands = table.join(commands, os.translateCommandsAndPaths(filecfg.buildcommands, prj.basedir, prj.location))
	if (#commands > 1) then
		commands = 'sh -c ' .. ninja.quote(table.implode(commands,"","",";"))
	else
		commands = commands[1]
	end

	add_build(cfg, p.esc(output), "", "custom_command | " .. p.esc(filename) .. inputs .. iif(#file_dependencies > 0, "||" .. ninja.list(file_dependencies), ""),
		{"CUSTOM_COMMAND = " .. commands, "CUSTOM_DESCRIPTION = custom build " .. p.esc(output)})
end

local function compile_file_build(cfg, filecfg, toolset, pch_dependency, regular_file_dependencies, objfiles)
	local obj_dir = project.getrelative(cfg.workspace, cfg.objdir)
	local has_custom_settings = fileconfig.hasFileSettings(filecfg)

	if shouldcompileasc(filecfg) then
		local objfilename = obj_dir .. "/" .. filecfg.objname .. iif(toolset_name == "msc", ".obj", ".o")
		objfiles[#objfiles + 1] = objfilename
		local cflags = {}
		if has_custom_settings then
			cflags = {"CFLAGS = " .. getcflags(toolset, cfg, filecfg)}
		end
		add_build(cfg, p.esc(objfilename), "", iif(has_custom_settings, "cc_flags ", "cc ") .. p.esc(filecfg.relpath) .. pch_dependency .. regular_file_dependencies, cflags)
	elseif shouldcompileascpp(filecfg) then
		local objfilename = obj_dir .. "/" .. filecfg.objname .. iif(toolset_name == "msc", ".obj", ".o")
		objfiles[#objfiles + 1] = objfilename
		local cxxflags = {}
		if has_custom_settings then
			cxxflags = {"CXXFLAGS = " .. getcxxflags(toolset, cfg, filecfg)}
		end
		add_build(cfg, p.esc(objfilename), "", iif(has_custom_settings, "cxx_flags ", "cxx ") .. p.esc(filecfg.relpath) .. pch_dependency .. regular_file_dependencies, cxxflags)
	elseif path.isresourcefile(filecfg.abspath) then
		local objfilename = obj_dir .. "/" .. filecfg.name .. ".res"
		objfiles[#objfiles + 1] = objfilename
		add_build(cfg, p.esc(objfilename), "", "rc " .. p.esc(filecfg.relpath))
	end
end

local function files_build(prj, cfg, toolset, toolset_name, pch_dependency, regular_file_dependencies, file_dependencies)
	local objfiles = {}
	tree.traverse(project.getsourcetree(prj), {
	onleaf = function(node, depth)
		local filecfg = fileconfig.getconfig(node, cfg)
		local rule = p.global.getRuleForFile(node.name, prj.rules)
		if fileconfig.hasCustomBuildRule(filecfg) then
			custom_command_build(prj, cfg, filecfg, node.relpath, file_dependencies)
		elseif 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)
			custom_command_build(prj, cfg, rulecfg, node.relpath, file_dependencies)
		else
			compile_file_build(cfg, filecfg, toolset, pch_dependency, regular_file_dependencies, objfiles)
		end
	end,
	}, false, 1)
	p.w("")

	return objfiles
end

local function generated_files_build(cfg, generated_files, key)
	local final_dependency = ""
	if #generated_files > 0 then
		p.w("# generated files")
		add_build(cfg, "generated_files_" .. key, "", "phony" .. ninja.list(generated_files))
		final_dependency = " || generated_files_" .. key
	end
	return final_dependency
end

-- generate project + config build file
function ninja.generateProjectCfg(cfg)
	local oldGetDefaultSeparator = path.getDefaultSeparator
	path.getDefaultSeparator = function() return "/" end

	local prj = cfg.project
	local key = prj.name .. "_" .. cfg.buildcfg
	-- TODO why premake doesn't provide default name always ?
	local toolset_name = _OPTIONS.cc or cfg.toolset or ninja.getDefaultToolsetFromOs()
	local toolset = p.tools[toolset_name]

	p.w("# project build file")
	p.w("# generated with premake ninja")
	p.w("")

	-- premake-ninja relies on scoped rules
	-- and they were added in ninja v1.6
	p.w("ninja_required_version = 1.6")
	p.w("")

	---------------------------------------------------- figure out settings
	local pch = nil
	if toolset_name ~= "msc" then
		pch = p.tools.gcc.getpch(cfg)
		if pch then
			pch = {
				input = pch,
				placeholder = project.getrelative(cfg.workspace, path.join(cfg.objdir, path.getname(pch))),
				gch = project.getrelative(cfg.workspace, path.join(cfg.objdir, path.getname(pch) .. ".gch"))
			}
		end
	end

	---------------------------------------------------- write rules
	p.w("# core rules for " .. cfg.name)
	prebuild_rule(cfg)
	postbuild_rule(cfg)
	compilation_rules(cfg, toolset, toolset_name, pch)
	custom_command_rule()

	---------------------------------------------------- build all files
	p.w("# build files")

	local pch_dependency = pch_build(cfg, pch)

	local generated_files = collect_generated_files(prj, cfg)
	local file_dependencies = getFileDependencies(cfg)
	local regular_file_dependencies = ""
	if #generated_files > 0 then
		regular_file_dependencies = " || generated_files_" .. key .. ninja.list(file_dependencies)
	elseif #file_dependencies > 0 then
		regular_file_dependencies = " ||" .. ninja.list(file_dependencies)
	end

	local obj_dir = project.getrelative(cfg.workspace, cfg.objdir)
	local objfiles = files_build(prj, cfg, toolset, toolset_name, pch_dependency, regular_file_dependencies, file_dependencies)
	local final_dependency = generated_files_build(cfg, generated_files, key)

	---------------------------------------------------- build final target
	if #cfg.prebuildcommands > 0 or cfg.prebuildmessage then
		p.w("# prebuild")
		add_build(cfg, "prebuild_" .. get_key(cfg), "", "run_prebuild")
	end
	if #cfg.postbuildcommands > 0 or cfg.postbuildmessage then
		p.w("# postbuild")
		add_build(cfg, "postbuild_" .. get_key(cfg), "", "run_postbuild | " .. ninja.outputFilename(cfg))
	end

	-- we don't pass getlinks(cfg) through dependencies
	-- because system libraries are often not in PATH so ninja can't find them
	local libs = ninja.list(p.esc(config.getlinks(cfg, "siblings", "fullpath")))
	if cfg.kind == p.STATICLIB then
		p.w("# link static lib")
		add_build(cfg, p.esc(ninja.outputFilename(cfg)), "", "ar " .. table.concat(p.esc(objfiles), " ") .. libs .. final_dependency)

	elseif cfg.kind == p.SHAREDLIB then
		local output = ninja.outputFilename(cfg)
		p.w("# link shared lib")

		local extra_output = ""
		if ninja.endsWith(output, ".dll") then
			extra_output = " | " .. p.esc(ninja.noext(output, ".dll")) .. ".lib" .. " " .. p.esc(ninja.noext(output, ".dll")) .. ".exp"
		elseif ninja.endsWith(output, ".so") then
			extra_output = " | " .. p.esc(ninja.noext(output, ".so")) .. ".a"
		elseif ninja.endsWith(output, ".dylib") then
			-- in case of .dylib there are no corresponding .a file
		else
			p.error("unknown type of shared lib '" .. output .. "', so no idea what to do, sorry")
		end

		add_build(cfg, p.esc(output), extra_output, "link " .. table.concat(p.esc(objfiles), " ") .. libs .. final_dependency)

	elseif (cfg.kind == p.CONSOLEAPP) or (cfg.kind == p.WINDOWEDAPP) then
		p.w("# link executable")
		add_build(cfg, p.esc(ninja.outputFilename(cfg)), "", "link " .. table.concat(p.esc(objfiles), " ") .. libs .. final_dependency)

	else
		p.error("ninja action doesn't support this kind of target " .. cfg.kind)
	end

	p.w("")
	if #cfg.postbuildcommands > 0 or cfg.postbuildmessage then
		add_build(cfg, key, "", "phony postbuild_" .. get_key(cfg))
	else
		add_build(cfg, key, "", "phony " .. ninja.outputFilename(cfg))
	end
	p.w("")

	path.getDefaultSeparator = oldGetDefaultSeparator
end

-- return name of output binary relative to build folder
function ninja.outputFilename(cfg)
	return project.getrelative(cfg.workspace, cfg.buildtarget.directory) .. "/" .. cfg.buildtarget.name
end

-- return name of build file for configuration
function ninja.projectCfgFilename(cfg, relative)
	if relative ~= nil then
		relative = project.getrelative(cfg.workspace, cfg.location) .. "/"
	else
		relative = ""
	end

	local ninjapath = relative .. "build_" .. cfg.project.name  .. "_" .. cfg.buildcfg

	if cfg.platform ~= nil then ninjapath = ninjapath .. "_" .. cfg.platform end

	return ninjapath .. ".ninja"
end

-- check if string starts with string
function ninja.startsWith(str, starts)
	return str:sub(0, starts:len()) == starts
end

-- check if string ends with string
function ninja.endsWith(str, ends)
	return str:sub(-ends:len()) == ends
end

-- removes extension from string
function ninja.noext(str, ext)
	return str:sub(0, str:len() - ext:len())
end

-- generate all build files for every project configuration
function ninja.generateProject(prj)
	if not p.action.supports(prj.kind) or prj.kind == p.NONE then
		return
	end
	for cfg in project.eachconfig(prj) do
		p.generate(cfg, ninja.projectCfgFilename(cfg), ninja.generateProjectCfg)
	end
end

include("_preload.lua")

return ninja