premake/website/docs/Overrides-and-Call-Arrays.md
KyrietS 19f8561bd8 Upgrade docusaurus version to beta.6
- some files had to be renamed as files prefixed with an underscore are ignored now
- docusaurus reserves /docs/tags path so we can't have a page there
2021-09-08 21:42:18 +02:00

7.6 KiB

title
Overrides & Call Arrays

Premake's extensibility is built around two coding conventions: overrides, a formalized way of replacing one function with another, and call arrays, a way of sequencing a series of steps at runtime.

Your First Customization

Let's jump right in with a simple example. Let's say that we're planning to keep our Premake-generated Visual Studio projects around for a while and, for historical reference, we'd like to know which version of Premake was used to generate them. To do so, we would like to add an XML comment to the top of the generated project files, like so:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by Premake 5.0.0-alpha3 -->
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
  <!-- ... and so on... -->

We don't want to modify Premake's own source code, because then our changes would be overwritten by each new update, and we'd be stuck maintaining our own fork of the code. It would also mean that everyone who generated our projects would need to have the customized version of Premake, otherwise we'd end up with generated projects that did not contain our version commment.

Instead, we'd really like to implement this customization right in our project scripts. That way we can share the scripts with any developer, and they can then generate a new project that has the version comment in it.

Use the Source!

Before we can make this change, we first need to know what function in the Premake source code is emitting this particular markup. As described in the Code Overview, the Visual Studio exporter is currently located in the src/actions/vstudio folder in the Premake source tree (go ahead and find it, we'll wait!).

We're looking for the code which generates the .vcxproj files, and browsing the file names brings us to vs2010_vcxproj.lua. Opening this file, we can then search for the "<Project" string, which we find in the m.project() function:

	function m.project(prj)
		local action = premake.action.current()
		p.push('<Project DefaultTargets="Build" ToolsVersion="%s" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">',
			action.vstudio.toolsVersion)
	end

(Or, if you are using a tool which supports it, it can be quicker to just run a full text search across the Premake sources for the markup you are looking to override.)

For the moment we don't really need to worry too much about how this code works because we aren't actually going to change it at all. Instead, we will override it with a new function that outputs our version comment, and then calls the original function to output the Project element, unmodified.

Before we can do that, we need one more bit of information: what is m? By convention, m is a shortcut for the module's namespace (really just a Lua table) which we declare at the top of the file. Looking at the top of vs2010_vcxproj.lua we find:

local p = premake
local m = p.vstudio.vc2010

Expanding that out, we can deduce that the fully-qualified name of the function we want to override is premake.vstudio.vc2010.project().

Introducing Overrides

Now that we've identified the function that emits the markup we wish to change, we can override it using Premake's aptly named override() function.

Note that actions don't get pulled in until they are actually used so you will need to require it in order to access it

require('vstudio')

Then (and only then) you can go ahead and call the override function !

premake.override(premake.vstudio.vc2010, "project", function(base, prj)
	premake.w('<!-- Generated by Premake ' .. _PREMAKE_VERSION .. ' -->')
	base(prj)
end)

This snippet replaces the original implementation of m.project() with my new (anonymous) function. From this point on, when someone calls m.project(), Premake will call my new function, passing it the original implementation as the first argument (base). If the function requires any other arguments (in this case, it receives the project being exported as prj) they appear after.

In our replacement function, we emit our comment header using premake.w(), which is short for "premake write", and _PREMAKE_VERSION, which is a global variable holding the version of the currently running Premake executable.

After emitting the comment we call base(prj), the original implementation of m.project(), to do the rest of the work for us. Easy!

To enable our override, place that code anywhere in your project or system scripts. Perhaps something like:

workspace "MyWorkspace"
   configurations { "Debug", "Release" }

project "MyProject"
   kind "ConsoleApp"
   -- ... the rest of the project settings...

-- Write the current Premake version into our generated files, for reference
premake.override(premake.vstudio.vc2010, "project", function(base, prj)
	premake.w('<!-- Generated by Premake ' .. _PREMAKE_VERSION .. ' -->')
	base(prj)
end)

The next time you generate a Visual Studio project from your scripts, the comment header will be placed before the Project element.

Introducing Call Arrays

Overrides are a great way to intercept an existing call to modify its arguments or return value or even replace it entirely. There is another, more self-contained way that we could have implemented our customization by leveraging Premake's call array convention.

If you look at the top of vs2010_vcxproj.lua, you will see that m.project() is called via an array of function references:

m.elements.project = function(prj)
	return {
		m.xmlDeclaration,
		m.project,
		m.projectConfigurations,
		m.globals,
		m.importDefaultProps,
		m.configurationPropertiesGroup,
		m.importExtensionSettings,
		m.propertySheetGroup,
		m.userMacros,
		m.outputPropertiesGroup,
		m.itemDefinitionGroups,
		m.assemblyReferences,
		m.files,
		m.projectReferences,
		m.importExtensionTargets,
	}
end

function m.generate(prj)
	io.utf8()
	p.callArray(m.elements.project, prj)
	p.out('</Project>')
end

Premake calls m.generate() to export the project—we'll talk about how that happens later. m.generate() calls p.callArray() (remember p is an alias for premake), which calls all of the functions in the list returned by m.elements.project(), passing the provided arguments (in this case prj) to each of them. This indirection allows project script authors like yourself an opportunity to modify that list of calls by adding, removing, or reordering the list.

Let's implement our version comment as an addition to this particular call array. To do so, we will override the m.elements.project() function (remember from the earlier example that m is short for premake.vstudio.vc2010). We'll call the original implementation to get the array of calls, and then add our own before returning it to m.generate().

local function premakeVersionComment(prj)
	premake.w('<!-- Generated by Premake ' .. _PREMAKE_VERSION .. ' -->')
end

premake.override(premake.vstudio.vc2010.elements, "project", function(base, prj)
	local calls = base(prj)
	table.insertafter(calls, m.xmlDeclaration, premakeVersionComment)
	return calls
end)

If you add that snippet to your project or system script, your new function will get called between m.xmlDeclaration() and m.project() and place our comment right where we'd like it.

(Wondering why the call array is in a function and not just a global table? Hint: because otherwise overrides wouldn't work.)