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看到,里面各个项目是有分组的,很方便管理