Basic Premake

So, making more use of Premake. Let’s build up from the very minimal to something reasonable, all the while striving for simplicity and lack of duplication.

Minimal premake script

This is the minimal Premake script, even if it does nothing:

00_min.lua

workspace "00_min"
configurations { "Debug" }

For each solution, and for each configuration, Premake does stuff. Without a solution, there’s nothing to attach it to, and without any configurations, nothing gets done.

Admittedly, if you run this against Premake, you’ll get some fairly useless project files.

$ premake5 --file=00_min.lua gmake
$ premake5 --file=00_min.lua vs2015
$ premake5 --file=00_min.lua xcode

Alas, premake clean doesn’t work (output says “** The clean action has not yet been ported”)

I won’t show the generated projects here, but you can run this for yourself and look.

Almost useful Premake script

Even though projects are really a consequence of IDEs like Visual Studio and XCode, they are required even if you just build makefile targets. So our real minimal script is this:

01_min.lua

workspace "01_min"
configurations { "Debug" }
project "01_min"
kind "ConsoleApp"

This will generate a solution with a single project in it, with a single target, and no files. You must specify kind. Sometimes I feel like defaults would be useful, but, that said, no real user of Premake would trust the defaults.

Run this and you can build against these generated projects.

Scopes (Indentation is an illusion)

Premake scripts are a special kind of Lua script. For the most part, there are no blocks, so there’s no control flow, and indentation is actually a little misleading.

A more human way to write the above is:

workspace "01_min"
    configurations { "Debug" }

    project "01_min"
        kind "ConsoleApp"

And you should do this, but fight the temptation to think that arbitrary scope rules are in force. Instead, there are three scopes

  • global scope
  • workspace scope
  • project scope

See Scopes and Inheritance in the Premake5 docs for more details.

A side note - you may see solution used in Premake scripts - this is an older alias for workspace, still supported in Premake4 but not mentioned in the documentation.

All workspace scopes are in the global scope, and project scopes are in workspace scopes. Each line of the form workspace "name" or project "name" selects that scope, creating it if necessary.

You can select the parent of the current project scope with project "*" - this selects whatever workspace is the parent of the current project. There is a hack to get back to global scope, and that is workspace "*".

So, that said, what you really need to pay attention to is that lines in a Premake script are declarations, and they are added to the active scope. They are not statements in an imperative program. The reason for this is that, once you have declared your workspaces and projects, Premake iterates through the workspaces, configurations and projects to actually generate output.

Most declarations are settings; settings in a workspace are inherited by all projects in the workspace. So, in general, prefer to put workspace-wide settings in the relevant workspace. And if there are truly global settings, put them in the global scope.

We’ll come back to that

Adding files

Let’s have a file to add

02_main.cpp

#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("Hello, world\n");
    return 0;
}

and then add it to our project:

02-min.lua

workspace "02_min"
    configurations { "Debug" }

    project "02_min"
        kind "ConsoleApp"
        files { "**.cpp" }

and after we generate a project and build it, we can run the output

$ bin\Debug\02_min.exe
Hello, world

It’s common to use wildcards to add files to projects; the **.cpp filter means “add files in the current directory and any sub-directories”. In this case, it only matched one file, but if we write it this way, we don’t need to update our Premake script if we add more source files. In fact, for C++, we typically want this:

workspace "02_min"
    configurations { "Debug" }

    project "02_min"
        kind "ConsoleApp"
        files { "**.cpp" }

We can get a lot done with just this minimal amount of Premake - we could have hundreds of source files in many directories.

Cleaning up

We didn’t specify where to put output. Premake has defaults for that:

  • generated projects go in .
  • binaries go in ./bin/<target>
  • intermediates go in ./obj/<target>

where . is the location of the Premake script, and not the current working directory. E.g. if we were in some other directory and ran Premake like this:

$ premake5 --file=premake-scripts/tests/02_min.lua vs2015

then we would see generated projects in premake-scripts/tests, and those projects would target locations in this directory as well.

First, let’s control where compiler-generated files go. Premake has a location function which sets the location of generated output, so let’s use that to make bin and obj go in a build directory.

workspace "03_min"
    configurations { "Debug" }
    location "build"

    project "03_min"
        kind "ConsoleApp"
        files { "**.cpp" }

This is, alas, where inheritance breaks down. The location function is one of the few that aren’t inherited to contained scopes. So in this case, location applies to the workspace only and this has an unexpected behavior - our generated projects go in our build directory. The reason that the project ends up in the build folder is that a contained project defaults to location "." which is relative to the enclosing workspace. Of course, this behaves like it’s inherited, so maybe the technical detail should be ignored.

But this still has a useful behavior: we can control where generated projects go. In a Premake world, these are artifacts just like objects and executables. And with everything in a build directory, we just have one directory to clean up (or not commit to source control).

There are specific functions to set the path for object files and binaries:

workspace "03_min"
    configurations { "Debug" }
    location "build"

    project "03_min"
        kind "ConsoleApp"
        files { "**.cpp" }
        objdir "debug/obj"
        targetdir "debug"

and if we build and run, we’ll see workspace and project in build, object files in build/debug/obj, and binaries in build/debug.

This is where simplicity and maintainability part company. We have hard-coded the configuration name twice; worse, if we add a new configuration, we’ll be directing files to the wrong location. That’s because the default paths for objdir and targetdir actually look like this:

objdir "%{wks.location}/obj"
targetdir "%{wks.location}/bin"

where the syntax %{...} is a Value Token expression that is interpolated into the string. In this case, the wks object is the workspace.

If we put these into our lua script

04_min.lua

workspace "04_min"
    configurations { "Debug" }
    location "build"

    project "04_min"
        kind "ConsoleApp"
        files { "**.cpp" }
        objdir "%{wks.location}/obj/%{cfg.buildcfg}"
        targetdir "%{wks.location}/bin/%{cfg.buildcfg}"

we will see exactly the behavior that we saw before. Of course, you don’t want to be more verbose about default settings; this is just to illustrate how you can access these. When you can access them, you can change them.

Returning back to location for a minute, let’s set location for both the workspace and the project

workspace "04_min"
    configurations { "Debug" }
    location "build"

    project "04_min"
        kind "ConsoleApp"
        location "%{wks.location}/prjbuild"
        files { "**.cpp" }

If we generate and build, we’ll see a workspace/solution file in build, a project file in build/prjbuild, an object directory at build/prjbuild/obj, and a binaries directory at build/prjbuild/bin.

Note something important - all paths in the Premake script are relative to the Premake script’s location.

Debugging

Short of building Premake yourself and using a system that has a Lua debugger, it’s hard to introspect the process of generating project files. There are some tricks that can be employed.

Token expansion happens in Premake script commands. There are a few that can result in inspectable output.

Let’s add a text file so we can filter on it

05_help.txt

Nothing important

and we add it to the project along with a filter and a buildmessage

05_min.lua

workspace "05_min"
    configurations { "Debug" }
    location "build"

    project "05_min"
        kind "ConsoleApp"
        location "%{wks.location}/prjbuild"
        files { "**.cpp" }

        filter 'files:**.txt'
            buildmessage 'wks.location = %{wks.location}'
            buildcommands { 'dir' }
            buildoutputs { 'output.i' }

This build rule won’t actually work, of course. But it will put text into the generated project that we can look at. Of course, if you do this with a Visual Studio target, many of the Premake variables will be translated into MSBuild variables. From the Premake source

    vstudio.pathVars = {
        ["cfg.objdir"]                  = { absolute = true,  token = "$(IntDir)" },
        ["prj.location"]                = { absolute = true,  token = "$(ProjectDir)" },
        ["prj.name"]                    = { absolute = false, token = "$(ProjectName)" },
        ["sln.location"]                = { absolute = true,  token = "$(SolutionDir)" },
        ["sln.name"]                    = { absolute = false, token = "$(SolutionName)" },
        ["wks.location"]                = { absolute = true,  token = "$(SolutionDir)" },
        ["wks.name"]                    = { absolute = false, token = "$(SolutionName)" },
        ["cfg.buildtarget.directory"]   = { absolute = false, token = "$(TargetDir)" },
        ["cfg.buildtarget.name"]        = { absolute = false, token = "$(TargetFileName)" },
        ["cfg.buildtarget.basename"]    = { absolute = false, token = "$(TargetName)" },
        ["file.basename"]               = { absolute = false, token = "%(Filename)" },
        ["file.abspath"]                = { absolute = true,  token = "%(FullPath)" },
        ["file.relpath"]                = { absolute = false, token = "%(Identity)" },
        ["file.path"]                   = { absolute = false, token = "%(Identity)" },
        ["file.directory"]              = { absolute = true,  token = "%(RootDir)%(Directory)" },
        ["file.reldirectory"]           = { absolute = false, token = "%(RelativeDir)" },
        ["file.extension"]              = { absolute = false, token = "%(Extension)" },
    }

but makefile targets won’t do such translation - they will be relative to the Makefile, of course. Still, in a pinch, you can see what variables are expanding to, since the documentation isn’t all that clear.