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 workspace "HelloWorld"     configurations { "Debug" , "Release"  } project "HelloWorld"     kind "ConsoleApp"     language "C"     targetdir "bin/%{cfg.buildcfg}"     files { "**.h" , "**.c"  }        include {}        filter "configurations:Debug"        defines { "DEBUG"  }       symbols "On"         filter "configurations:Release"        defines { "NDEBUG"  }       optimize "On"  
这里的filter一般限定了范围,比如说特定的Windows平台,如下所示:
1 2 3 4 5 6 7 8 9 filter  "system:windows"   cppdialect "C++17"    staticruntime "On"       systemversion "latest"     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"       systemversion "latest"     defines { "NDEBUG" }    optimize "On" 
如果想取消对应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   prj.name prj.location   prj.language  prj.group  cfg.longname cfg.shortname cfg.kind cfg.architecture cfg.platform cfg.system  cfg.buildcfg cfg.buildtarget  cfg.linktarget  cfg.objdir file.path file.abspath file.relpath file.directory file.reldirectory file.name file.basename  file.extension   [target].abspath [target].relpath [target].directory [target].name [target].basename  [target].extension   [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/\""   		} 	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" 			 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}" ,		 	"%{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看到,里面各个项目是有分组的,很方便管理