diff --git a/README.md b/README.md index 65bfadf..4abf330 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,31 @@ # premake-vscode An extension for premake that adds project and workspace generation for Visual Studio Code. -I got inspired to write this extension when I found these already existing extensions: [https://github.com/Enhex/premake-vscode](https://github.com/Enhex/premake-vscode) and [https://github.com/paullackner/premake-vscode](https://github.com/paullackner/premake-vscode), however they're a bit out-dated at the moment, and neither of them supports the premake features that I require, so I decided to write my own extension based on theirs. +This project was originally created [by peter](https://github.com/peter1745) + +This was initially inspired by these repositories: +* [https://github.com/Enhex/premake-vscode](https://github.com/Enhex/premake-vscode) +* [https://github.com/paullackner/premake-vscode](https://github.com/paullackner/premake-vscode) + +The goal of this project was to create a more up to date extension with more feature support! + +Namely, +* Most of the C++ configuration properties that premake offers +* Might also support C and C# in the future, but for now C++ is the main focus. + + +## Supported Languages + +### C++ +Supported Premake `"CppDialect"` options: +* `C++98`, `C++11`, `C++14`, `C++17`, `C++20`, `C++2a`, `gnu++98`, `gnu++11`, `gnu++14`, `gnu++17`, `gnu++20` + +C++ Compilers for Windows: +* `msvc`, `clang` + +C++ Compilers for Linux: +* `gcc`, `clang` -My goal with this extension is to support most of the C++ configuration properties that premake offers, I might also support C and C# in the future, but for now C++ is the main focus. ## Usage To use this extension add this repository to one of the Premake [search paths](https://premake.github.io/docs/Locating-Scripts/), and then add the following inside `premake-system.lua`: @@ -15,3 +37,8 @@ Or add the following to your `premake5.lua` script if you added this repository ```lua require("path/to/this/repo/vscode") ``` + +Then, you can invoke generation like so: +```bash +$ premake vscode +``` diff --git a/_manifest.lua b/_manifest.lua index ef6ac40..6e8b47b 100644 --- a/_manifest.lua +++ b/_manifest.lua @@ -3,5 +3,7 @@ return { "_preload.lua", "vscode.lua", "vscode_workspace.lua", - "vscode_project.lua" -} \ No newline at end of file + "vscode_project.lua", + "vscode_tasks.lua", + "vscode_launch.lua" +} diff --git a/_preload.lua b/_preload.lua index 59a03c6..cfb83ad 100644 --- a/_preload.lua +++ b/_preload.lua @@ -18,6 +18,7 @@ newaction { valid_languages = { "C", "C++", "C#" }, valid_tools = { "gcc", "clang", "msc" }, + -- Generate appropriate build files onStart = function() if _TARGET_OS == "windows" then os.execute(_PREMAKE_COMMAND .. " vs2022") @@ -39,4 +40,4 @@ newaction { return function(cfg) return _ACTION == "vscode" -end \ No newline at end of file +end diff --git a/vscode.lua b/vscode.lua index 964256e..dd74276 100644 --- a/vscode.lua +++ b/vscode.lua @@ -1,38 +1,41 @@ -local p = premake +-- VSCODE MODULE -p.modules.vscode = { _VERSION = "1.0.0" } -local vscode = p.modules.vscode +-- Include workspace and project files + +include("vscode_workspace.lua") +include("vscode_project.lua") + +-- preload +include("_preload.lua") + + +-- Aliases +local p = premake local project = p.project +local vscode = p.modules.vscode +vscode = { _VERSION = "1.0.0" } + +-- Workspace Generation function vscode.generateWorkspace(wks) p.eol("\r\n") p.indent("\t") + -- NOTE: the .code-workspace file contains the tasks and launch configurations. p.generate(wks, ".code-workspace", vscode.workspace.generate) - p.generate(wks, wks.location .. "/Tasks/.vscode/tasks.json", vscode.workspace.tasks.generate) end +-- Project Generation function vscode.generateProject(prj) p.eol("\r\n") p.indent("\t") + -- C/C++ Support + -- * This is where support for other languages will eventually be specified if (project.isc(prj) or project.iscpp(prj)) then p.generate(prj, prj.location .. "/.vscode/c_cpp_properties.json", vscode.project.cCppProperties.generate) end - - local isLaunchable = false - - for cfg in project.eachconfig(prj) do - isLaunchable = cfg.kind == "ConsoleApp" or cfg.kind == "WindowedApp" - - if isLaunchable then - break - end - end - - if isLaunchable then - p.generate(prj, prj.location .. "/.vscode/launch.json", vscode.project.launch.generate) - end + end function vscode.configName(config, includePlatform) @@ -44,11 +47,13 @@ function vscode.configName(config, includePlatform) end function vscode.getToolsetName(cfg) + -- MSC for windows, CLANG for everything else local default = iif(cfg.system == p.WINDOWS, "msc", "clang") return _OPTIONS.cc or cfg.toolset or default end function vscode.getCompiler(cfg) + -- MSC for windows, CLANG for everything else local default = iif(cfg.system == p.WINDOWS, "msc", "clang") local toolset = p.tools[_OPTIONS.cc or cfg.toolset or default] if not toolset then @@ -63,9 +68,4 @@ function vscode.esc(value) return value end -include("vscode_workspace.lua") -include("vscode_project.lua") - -include("_preload.lua") - -return vscode \ No newline at end of file +return vscode diff --git a/vscode_launch.lua b/vscode_launch.lua new file mode 100644 index 0000000..20dacb5 --- /dev/null +++ b/vscode_launch.lua @@ -0,0 +1,102 @@ +-- LAUNCH CONFIG GENERATION + +-- aliases (for nicer code and less writing) +local p = premake +local project = p.project +local vscode = p.modules.vscode + +-- Initialize premake module object +vscode.launch = {} +local launch = vscode.launch + +-- Function reflection +launch.configProps = function(prj, cfg) + -- Returns an array of the generation functions for each property (if any new are created, they should be added here). + return { + launch.type, + launch.request, + launch.program, + launch.args, + launch.stopAtEntry, + launch.cwd, + launch.environment, + launch.preLaunchTask + } +end + + +-- GENERATION -- + +function launch.type(prj, cfg) + if cfg.system == "windows" then + p.w('"type": "cppvsdbg",') + else + p.w('"type": "cppdbg",') + end +end + +function launch.request(prj, cfg) + p.w('"request": "launch",') +end + +-- build target +function launch.program(prj, cfg) + local targetdir = project.getrelative(prj, cfg.buildtarget.directory) + local targetname = cfg.buildtarget.name + p.w('"program": "%s/%s",', prj.location, path.join(targetdir, targetname)) +end + +function launch.args(prj, cfg) + p.w('"args": [],') +end + +function launch.stopAtEntry(prj, cfg) + p.w('"stopAtEntry": false,') +end + +function launch.cwd(prj, cfg) + -- I think this currently launches from the workspace directory since launch configs now generate in the ".code-workspace" file + -- TODO: Make sure that these are running from proper project directories. Otherwise programs that depend on files relative to the program in some resource file won't be able to find the files.' + p.w('"cwd": "./",') +end + +function launch.environment(prj, cfg) + p.w('"environment": [],') +end + +-- Pre-launch task +function launch.preLaunchTask(prj, cfg) + -- Calls the relevant build task before launching. + local configName = vscode.configName(cfg, #prj.workspace.platforms > 1) + p.w('"preLaunchTask": "BUILD-%s",', cfg.name) +end + +function launch.generate(prj) + -- Generates ".json" formatted launch configs to generate in the ".code-workspace" file. + -- * NOTE: This doesn't need to return anything since the scope that it's called in should have an open file that it's writing to through premake's API + + -- Start launch options block + p.push('"launch": {') + p.w('"version": "0.2.0",') + + -- Start configurations array + p.push('"configurations": [') + + -- Loop through each project's configs + for cfg in project.eachconfig(prj) do + p.push('{') + + -- Set Launch name (name that shows in launch menu in vscode) + local configName = vscode.configName(cfg, #prj.workspace.platforms > 1) + p.w('"name": "Launch %s",', configName) + + -- Call each of the config generation functions for this project configuration + -- * TODO: Find a way to get rid of the extra comma after the last property (avoiding hard-coding). I think vscode still parses it just fine, but it does show some scary errors in the generated files if you open them (not ideal). + p.callArray(launch.configProps, prj, cfg) + + p.pop('},') + end + -- Done + p.pop(']') + p.pop('}') +end diff --git a/vscode_project.lua b/vscode_project.lua index 68c51b6..f584dd5 100644 --- a/vscode_project.lua +++ b/vscode_project.lua @@ -1,16 +1,19 @@ +-- PROJECT GENERATION + +-- Aliases local p = premake local project = p.project local config = p.config local tree = p.tree local vscode = p.modules.vscode +-- Initialize project object vscode.project = {} vscode.project.cCppProperties = {} -vscode.project.launch = {} -vscode.project.tasks = {} + +-- Supported C/C++ Properties local cCppProperties = vscode.project.cCppProperties -local launch = vscode.project.launch cCppProperties.cppStandards = { ["C++98"] = "c++98", @@ -38,6 +41,7 @@ cCppProperties.toolsetPaths = { } } +-- C/C++ Property Generation reflection cCppProperties.configProps = function(prj, cfg) return { cCppProperties.intelliSenseMode, @@ -49,25 +53,34 @@ cCppProperties.configProps = function(prj, cfg) } end + +-- GENERATION METHODS -- + +-- Intellisense function cCppProperties.intelliSenseMode(prj, cfg) + -- Supported intellisense modes + -- NOTE(minifalafel): Maybe this should be stored somewhere else? In case we ever need to access it anywhere else. local supportedModes = { ["msc"] = "msvc-x64", ["clang"] = "clang-x64", ["gcc"] = "gcc-x64" } + -- Select the mode based on the toolset (if it's supported) local toolset = vscode.getToolsetName(cfg) local mode = supportedModes[toolset] if mode == nil then - error("Invalid toolset '" .. toolset "'") + error("Unsupported toolset '" .. toolset "'") end + -- Finally write the option p.w('"intelliSenseMode": "%s",', mode) end function cCppProperties.includeDirs(prj, cfg) + -- TODO: Maybe these could be consolidated into a single array and then checked? local hasIncludeDirs = #cfg.sysincludedirs > 0 or #cfg.externalincludedirs > 0 or #cfg.includedirs > 0 if hasIncludeDirs then @@ -86,7 +99,6 @@ function cCppProperties.includeDirs(prj, cfg) for _, includedir in ipairs(cfg.includedirs) do p.w('"%s",', includedir:gsub([[\]], "/")) end - p.pop('],') end end @@ -140,16 +152,22 @@ function cCppProperties.compilerPath(prj, cfg) p.w('"compilerPath": "%s",', toolsetPath) end + +-- C/C++ COMBINED GENERATION function cCppProperties.generate(prj) + -- ".json" formatted opening bracket and property name p.push('{') p.push('"configurations": [') - + + -- For each project configuration for cfg in project.eachconfig(prj) do - local configName = vscode.configName(cfg, #prj.workspace.platforms > 1) - p.push('{') + + -- Set the name + local configName = vscode.configName(cfg, #prj.workspace.platforms > 1) p.w('"name": "%s",', configName) + -- Generate the C/C++ Properties p.callArray(cCppProperties.configProps, prj, cfg) p.pop('},') @@ -159,74 +177,3 @@ function cCppProperties.generate(prj) p.w('"version": 4') p.pop('}') end - -launch.configProps = function(prj, cfg) - return { - launch.type, - launch.request, - launch.program, - launch.args, - launch.stopAtEntry, - launch.cwd, - launch.environment, - launch.console, - } -end - -function launch.type(prj, cfg) - if cfg.system == "windows" then - p.w('"type": "cppvsdbg",') - else - p.w('"type": "cppdbg",') - end -end - -function launch.request(prj, cfg) - p.w('"request": "launch",') -end - -function launch.program(prj, cfg) - local targetdir = project.getrelative(prj, cfg.buildtarget.directory) - local targetname = cfg.buildtarget.name - p.w('"program": "%s",', path.join(targetdir, targetname)) -end - -function launch.args(prj, cfg) - p.w('"args": [],') -end - -function launch.stopAtEntry(prj, cfg) - p.w('"stopAtEntry": false,') -end - -function launch.cwd(prj, cfg) - p.w('"cwd": "${workspaceFolder}",') -end - -function launch.environment(prj, cfg) - p.w('"environment": [],') -end - -function launch.console(prj, cfg) - p.w('"console": "integratedTerminal"') -end - -function launch.generate(prj) - p.push('{') - p.w('"version": "0.2.0",') - p.push('"configurations": [') - - for cfg in project.eachconfig(prj) do - local configName = vscode.configName(cfg, #prj.workspace.platforms > 1) - - p.push('{') - p.w('"name": "Launch %s",', configName) - - p.callArray(launch.configProps, prj, cfg) - - p.pop('},') - end - - p.pop(']') - p.pop('}') -end diff --git a/vscode_tasks.lua b/vscode_tasks.lua new file mode 100644 index 0000000..35b9060 --- /dev/null +++ b/vscode_tasks.lua @@ -0,0 +1,76 @@ +-- TASK GENERATION + +-- Aliases +local p = premake +local vscode = p.modules.vscode +local tasks = vscode.tasks + +-- Define tasks object +vscode.tasks = {} + +-- Task build functions +function tasks.buildSolutionTask(wks) + local solutionFile = p.filename(wks, ".sln") + + local enablePreReleases = _OPTIONS["enable_prereleases"] + local vswhere = '"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" -latest'; + + if enablePreReleases then + vswhere = vswhere .. ' -prerelease' + end + + vswhere = vswhere .. ' -find MSBuild' + + local msBuildPath, err = os.outputof(vswhere) + msBuildPath = path.normalize(path.join(msBuildPath, "Current", "Bin", "MSBuild.exe")) + + for cfg in p.workspace.eachconfig(wks) do + p.push('{') + p.w('"type": "shell",') + p.w('"label": "BUILD-%s",', cfg.name) + p.w('"command": "%s",', msBuildPath) + p.w('"args": ["%s", "-p:Configuration=%s"],', solutionFile, cfg.name) + p.w('"problemMatcher": "$msCompile",') + p.w('"group": "build",') + p.pop('},') + end +end + +function tasks.buildMakefileTask(wks) + for cfg in p.workspace.eachconfig(wks) do + p.push('{') + p.w('"type": "shell",') + p.w('"label": "BUILD-%s",', cfg.name) + p.w('"command": "make",') + p.w('"args": ["config=%s"],', string.lower(cfg.name)) + p.w('"problemMatcher": "$gcc",') + p.w('"group": "build",') + p.pop('},') + end +end + +tasks.buildTasks = function(wks) + if _TARGET_OS == "windows" then + return { + tasks.buildSolutionTask + } + else + return { + tasks.buildMakefileTask + } + end + +end + + +-- COMBINED GENERATION +function tasks.generate(wks) + p.push('"tasks": {') + p.w('"version": "2.0.0",') + p.push('"tasks": [') + + p.callArray(tasks.buildTasks, wks) + + p.pop(']') + p.pop('},') +end diff --git a/vscode_workspace.lua b/vscode_workspace.lua index 1f9cd89..9b50217 100644 --- a/vscode_workspace.lua +++ b/vscode_workspace.lua @@ -1,104 +1,53 @@ local p = premake -local project = p.project -local workspace = p.workspace local tree = p.tree local vscode = p.modules.vscode -vscode.workspace = {} -vscode.workspace.tasks = {} - -local m = vscode.workspace +-- INCLUDES +include("vscode_tasks.lua") +include("vscode_launch.lua") -function m.generate(wks) - p.utf8() +-- WORKSPACE FILE +vscode.workspace = {} - p.push('{') +function vscode.workspace.generateFolders(wks) p.push('"folders": [') - -- Project List - tree.traverse(workspace.grouptree(wks), { - onleaf = function(n) - local prj = n.project - - local prjpath = path.getrelative(prj.workspace.location, prj.location) - p.push('{') - p.w('"path": "%s"', prjpath) - p.pop('},') - end, - }) - - -- HACK(Peter): Hack around the tasks not being picked up when workspace is opened - local prjpath = path.getrelative(wks.location, "Tasks") + -- Workspace vscode folder p.push('{') - p.w('"path": "%s"', prjpath) - p.pop('}') - - p.pop(']') - p.pop('}') -end - -local tasks = vscode.workspace.tasks - -function tasks.buildSolutionTask(wks) - local solutionFile = p.filename(wks, ".sln") - - local enablePreReleases = _OPTIONS["enable_prereleases"] - local vswhere = '"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe" -latest'; + p.w('"path": "."') + p.pop('},') - if enablePreReleases then - vswhere = vswhere .. ' -prerelease' - end - - vswhere = vswhere .. ' -find MSBuild' - - local msBuildPath, err = os.outputof(vswhere) - msBuildPath = path.normalize(path.join(msBuildPath, "Current", "Bin", "MSBuild.exe")) - - for cfg in workspace.eachconfig(wks) do - p.push('{') - p.w('"type": "shell",') - p.w('"label": "Build All (%s)",', cfg.name) - p.w('"command": "%s",', msBuildPath) - p.w('"args": ["%s", "-p:Configuration=%s"],', solutionFile, cfg.name) - p.w('"problemMatcher": "$msCompile",') - p.w('"group": "build"') - p.pop('},') - end + -- Project List + --tree.traverse(p.workspace.grouptree(wks), { + -- onleaf = function(n) + -- local prj = n.project +-- + -- local prjpath = path.getrelative(prj.workspace.location, prj.location) + -- p.push('{') + -- p.w('"path": "%s"', prjpath) + -- p.pop('},') + -- end, + --}) + + p.pop('],') end -function tasks.buildMakefileTask(wks) - for cfg in workspace.eachconfig(wks) do - p.push('{') - p.w('"type": "shell",') - p.w('"label": "Build All (%s)",', cfg.name) - p.w('"command": "make",') - p.w('"args": ["-j$((`nproc` - 1))", "config=%s"],', cfg.name) - p.w('"problemMatcher": "$gcc",') - p.w('"group": "build"') - p.pop('},') - end -end +-- WORKSPACE AND TASK GENERATION +function vscode.workspace.generate(wks) + p.push('{') -tasks.buildTasks = function(wks) - if _TARGET_OS == "windows" then - return { - tasks.buildSolutionTask - } + vscode.workspace.generateFolders(wks) + vscode.tasks.generate(wks) + -- Check if there is a startup project specified + if not (wks.startproject == nil or wks.startproject == "") then + vscode.launch.generate(wks.projects[wks.startproject]) -- launch/startup project + -- HACK (minifalafel) If no startproject was specified, just generate for the first project in the workspace else - return { - tasks.buildMakefileTask - } + prj = wks.projects[1] + if not (prj == nil) then + vscode.launch.generate(prj) + end end -end - -function tasks.generate(wks) - p.push('{') - p.w('"version": "2.0.0",') - p.push('"tasks": [') - - p.callArray(tasks.buildTasks, wks) - - p.pop(']') p.pop('}') end