Premake学习

最近跟着YouTube大神TheCherno学习制作游戏引擎,其中用到了Premake5,Premake是一个命令行工具,它读取软件项目的脚本定义,然后使用它来执行构建配置任务或为Visual Studio, Xcode和GNU Make等工具集生成项目文件。

先介绍一下Premake5, 官方给了个简单的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- premake5.lua
workspace "HelloWorld"
configurations { "Debug", "Release" }

project "HelloWorld"
kind "ConsoleApp"
language "C"
targetdir "bin/%{cfg.buildcfg}"

files { "**.h", "**.c" }

--这里面写附加库的头文件目录,对应的VS项目属性里的Additional Include Directories
include {}
--filter后面一般加特定平台的Configurations,其范围会一直持续到碰到下一个filter或project
filter "configurations:Debug"
defines { "DEBUG" }
symbols "On"
--到这,碰到fitler,上面的filter对应Debug平台下的filter范围结束
filter "configurations:Release"
defines { "NDEBUG" }
optimize "On"

这里的filter一般限定了范围,比如说特定的Windows平台,如下所示:

1
2
3
4
5
6
7
8
9
--比如说windows平台下的filter
filter "system:windows"
cppdialect "C++17"
staticruntime "On" --表示会Link对应的dll
systemversion "latest" --使用最新的windows sdk版本,否则会默认选择8.1的版本

filter "configurations:Release"
defines { "NDEBUG" }
optimize "On"

如上所示,filter相当于筛选器,上述写法,如果是安卓平台的Release模式,则下面的filter还是会执行,如果想限定两个,比如只生在windows的Release情况下的filter,则应该这么写

1
2
3
4
5
6
filter {"system:windows", "configurations:Release"}
cppdialect "C++17"
staticruntime "On" --表示会Link对应的dll
systemversion "latest" --使用最新的windows sdk版本,否则会默认选择8.1的版本
defines { "NDEBUG" }
optimize "On"

如果想取消对应filter的限定,则在后面加上这一行即可:

1
2
-- Reset the filter for other settings
filter { }

学到了一个单词,叫做token,我原本以为叫做Macro,token表示一些代表符号,比如VS里的$(SolutionDir),而Premake的宏大概是这么写:

1
2
3
%{wks.name}
%{prj.location}
%{cfg.targetdir}

在Github上Premake的Wiki界面搜索Token可以找到对应的一些符号,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
wks.name
wks.location -- (location where the workspace/solution is written, not the premake-wks.lua file)

prj.name
prj.location -- (location where the project is written, not the premake-prj.lua file)
prj.language
prj.group

cfg.longname
cfg.shortname
cfg.kind
cfg.architecture
cfg.platform
cfg.system
cfg.buildcfg
cfg.buildtarget -- (see [target], below)
cfg.linktarget -- (see [target], below)
cfg.objdir

file.path
file.abspath
file.relpath
file.directory
file.reldirectory
file.name
file.basename -- (file part without extension)
file.extension -- (including '.'; eg ".cpp")

-- These values are available on build and link targets
-- Replace [target] with one of "cfg.buildtarget" or "cfg.linktarget"
-- Eg: %{cfg.buildtarget.abspath}
[target].abspath
[target].relpath
[target].directory
[target].name
[target].basename -- (file part without extension)
[target].extension -- (including '.'; eg ".cpp")
[target].bundlename
[target].bundlepath
[target].prefix
[target].suffix

还可以使用postbuildcommand来实现build完成之后的文件拷贝和复制工作,如下所示:

1
2
3
4
5
postbuildcommand
{
-- %{cfg.buildtarget.relpath}是生成文件,相较于当前premake5.lua文件的相对路径
{"COPY" %{cfg.buildtarget.relpath} ../.. output../Sandbox"}-- ..是一种语法,output相当于之前声明的一个string变量
}

更多内容可前往官网查看https://premake.github.io/docs/,接下来开始我的配置

解决方案的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
workspace "HEngine"		//解决方案名
architecture "64" //设置解决方案平台为x64还是x32

startproject "Sandbox" //设置开始项目

configurations //解决方案配置,默认配置是Debug,与顺序无关
{
"Debug",
"Release",
"Dist"
}
outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}"

//cfg.buildcfg:解决方案配置名(Debug,Release等)
//cfg.system:平台系统名(window,linux等)
//cfg.architecture:解决方案平台名(x86,x86_x64)对应architecture "x32"/"x64"
//outputdir相当于全局变量,会在后面用到

项目的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
project "HEngine"		//项目名
location "HEngine" //项目配置文件的生成路径
kind "SharedLib" //SharedLib生成(.dll) 改为 ConsoleApp则生成.exe
language "C++" //语言为c++

targetdir ("bin/" .. outputdir .."/%{prj.name}")
//prj.name为此项目名
//最后等于"bin\Debug-windows-x86\HEngine"

objdir ("bin-int/" .. outputdir .."/%{prj.name}")
//指定中间目录,最后等于bin-int\Debug-windows-x86\HEngine
//两边的..是lua语言字符串拼接的写法

Premake的本质就是生成Visual Studio等工具的项目文件,所以上面写的所有代码最后都会配置到项目中,运行完脚本后在VS右键项目打开Properties中查看设置的输出目录、中间目录、其他包含目录的路径是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
	files	//指定编译的文件类型
{
"%{prj.name}/src/**.h",
"%{prj.name}/src/**.cpp"
}

includedirs //附加包含目录,自定义头文件的位置
{
"%{prj.name}/vendor/spdlog/include"
}

filter "system:windows" //针对windows系统,进行如下配置
cppdialect "c++17" //c++语言标准
staticruntime "On" //
systemversion "10.0.26100.0" //根据你下的windows SDK版本设置

defines //预处理器定义,也就是预定义宏
{
"HE_PLATFORM_WINDOWS",
"HENGINE_BUILD_DLL"
}

postbuildcommands //对编译链接得到的二进制文件进行操作
{
"{COPYDIR} %{cfg.buildtarget.relpath} \"../bin/" .. outputdir .. "/SandBox/\""
--xcopy /Q /E /Y /I ..\bin\Debug-windows-x86_64\HEngine\HEngine.dll ..\bin\Debug-windows-x86_64\SandBox\
}

filter "configurations:Debug"
defines "HE_DEBUG"
symbols "On" //将项目管理器属性中 运行库改为多线程(MT)

filter "configurations:Release"
defines "HE_RELEASE"
optimize "On" //开启优化

filter "configurations:Dist"
defines "HE_DIST"
optimize "On"


//来自Sandbox项目
links
{
"HEngine" //链接HEngine项目给Sandbox项目
}

遇到的问题

在Premake使用{COPY}命令时程序会卡死不动,或者显示找不到文件的报错

Cherno用的premake5是更老的版本,在那个版本{COPY}已经被弃用了,所以跟着写不行,官方推荐使用{COPYFILE}或者{COPYDIR}命令,但是官网的doc对介绍太粗略,只给了一个简单的例子

下面说一下解决思路,首先要知道我们在脚本写的{COPY}是会转换成实际Windows批处理命令的,在VS的properties(属性)>生成事件(Build Events)>生成后事件(post-Build Event)中可以找到脚本使用的实际命令是什么,根据这个来看问题出现在哪

1
2
3
4
5
6
7
8
9
10
11
//报错代码的对应命令
("{COPY} %{cfg.buildtarget.relpath} ../bin/" .. outputdir .. "/Sandbox")

//IF EXIST ..\bin\Debug-windows-x86_64\HEngine\HEngine.dll\ (xcopy /Q /E /Y /I ..\bin\Debug-windows-x86_64\HEngine\HEngine.dll "..\bin\Debug-windows-x86_64\Sandbox\" > nul) ELSE (xcopy /Q /Y /I ..\bin\Debug-windows-x86_64\HEngine\HEngine.dll "..\bin\Debug-windows-x86_64\Sandbox\" > nul)
xcopy参数: /Q:静默复制,不显示文件名。
/E:复制所有子目录,包括空目录。
/Y:自动覆盖目标位置的同名文件,无需确认。
/I:如果目标不存在,假定为目录。
>nul 表示不需要打印输出
根据分析后我们发现问题出在目标路径写的不对,应该是..\bin\Debug-windows-x86_64\Sandbox\,少了尾部的路径符号,找到问题后修改脚本即可
("{COPY} %{cfg.buildtarget.relpath} \"../bin/" .. outputdir .. "/Sandbox/\"")

接下来我们使用官网推荐替换版本{COPYFILE}或者{COPYDIR},但使用{COPYFILE}的话对应的批处理命令是copy不是xcopy,copy无法在目标目录为空时自动创建文件夹,所以我们使用{COPYDIR},这样问题就解决啦

1
2
"{COPYDIR} %{cfg.buildtarget.relpath} \"../bin/" .. outputdir ..  "/SandBox/\"" 
//xcopy /Q /E /Y /I ..\bin\Debug-windows-x86_64\HEngine\HEngine.dll ..\bin\Debug-windows-x86_64\SandBox\

上面演示的是项目初期的配置,后期由于添加了很多的子模块(Submodules),每个项目都有不同的设置,所以每个项目都会有一个Premake,然后通过include来添加,而不是全写在同一个Premake中,更好管理各个项目的配置

最终项目:

首先是根目录下的Premake5.lua,也就是解决方案的配置文件,里面只Inclue各个项目的lua,并做好分组,看起来很干净

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
include "Dependencies.lua"

workspace "HEngine"
...
//上面介绍过的就不再写出来了

group "Dependencies"
include "vendor/premake"
include "HEngine/vendor/Glad"
include "HEngine/vendor/msdf-atlas-gen"

//子模块是官网链接的我没权限添加Premake上去,意味着别人clone我的项目后是没有Premake文件的
//为了让大家Clone后能使用,就把这类的单独放个文件夹里传上去
include "HEngine/vendor/premake/premake5_GLFW.lua"
include "HEngine/vendor/premake/premake5_imgui.lua"
include "HEngine/vendor/premake/premake5_yaml.lua"
include "HEngine/vendor/premake/premake5_Box2D.lua"

group "" //写一个空的group作用是结束当前的分组

group "Core"
include "HEngine"
include "HEngine-ScriptCore"
group ""

group "Tools"
include "HEngine-Editor"
group ""

group "Misc"
include "Sandbox"
group ""

Dependencies.lua 里面写好各个项目要Include的目录,后面项目就可以直接调用这个数组了

1
2
3
4
5
6
7
8
IncludeDir = {}
IncludeDir["stb_image"] = "%{wks.location}/HEngine/vendor/stb_image"
IncludeDir["yaml_cpp"] = "%{wks.location}/HEngine/vendor/yaml-cpp/include"
IncludeDir["Box2D"] = "%{wks.location}/HEngine/vendor/Box2D/include"
IncludeDir["GLFW"] = "%{wks.location}/HEngine/vendor/GLFW/include"
IncludeDir["Glad"] = "%{wks.location}/HEngine/vendor/Glad/include"
IncludeDir["ImGui"] = "%{wks.location}/HEngine/vendor/ImGui"
.....

接下来是我们举两个项目的Premake例子

HEngine.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
project "HEngine"
kind "StaticLib"
language "C++"
cppdialect "C++17"
staticruntime "off"

targetdir ("%{wks.location}/bin/" .. outputdir .. "/%{prj.name}")
objdir ("%{wks.location}/bin-int/" .. outputdir .. "/%{prj.name}")

pchheader "hepch.h" //Pch预编译文件,在HEngine学习笔记1-30有讲解到
pchsource "src/hepch.cpp"

//以下的上面单独介绍过了,就不重新介绍了
files
{
"src/**.h",
"src/**.cpp",
"vendor/stb_image/**.h",
"vendor/stb_image/**.cpp"
...
}

defines
{
"_CRT_SECURE_NO_WARNINGS",
"GLFW_INCLUDE_NONE",
"YAML_CPP_STATIC_DEFINE",
"IMGUI_DEFINE_MATH_OPERATORS"
}

includedirs
{
"src",
"vendor/spdlog/include",
"%{IncludeDir.Box2D}", //这里用上了之前Dependencies.lua的数组
"%{IncludeDir.filewatch}",
"%{IncludeDir.GLFW}",
...
}

links
{
"Box2D",
"GLFW",
"Glad",
...
}

GLFW.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
local premakeDir = path.getabsolute(".")
local projectDir = path.getabsolute("../GLFW")
os.chdir(projectDir)

project "GLFW"
kind "StaticLib"
language "C"
staticruntime "off"
warnings "off" -- 隐藏一些编译警告


targetdir ("bin/" .. outputdir .. "/%{prj.name}")
objdir ("bin-int/" .. outputdir .. "/%{prj.name}")

files
{
"include/GLFW/glfw3.h",
"include/GLFW/glfw3native.h",
"src/glfw_config.h",
"src/internal.h",
"src/platform.h",
...
}

filter "system:windows"
systemversion "latest"

files
{
"src/win32_init.c",
"src/win32_module.c",
"src/win32_joystick.c",
"src/win32_monitor.c",
...
}

defines
{
"_GLFW_WIN32",
"_CRT_SECURE_NO_WARNINGS"
}

filter "configurations:Debug"
runtime "Debug"
symbols "on"

filter "configurations:Release"
runtime "Release"
optimize "on"

写好了所有要配置的文件后,最后运行Script文件夹下的Win-GenProjects.bat,运行Premake.exe,即可完成项目的配置

Win-GenProjects.bat

1
2
3
4
5
@echo off
pushd %~dp0\..\
call vendor\premake\bin\premake5.exe vs2022
popd
PAUSE

最后可以看到在VStudio看到,里面各个项目是有分组的,很方便管理

2