Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
822b317
Add basic implementation for kit scanner
drook207 Oct 25, 2025
09afee7
Fix call to make directory
drook207 Oct 25, 2025
206038a
Add global config dir for kit file
drook207 Oct 27, 2025
e4f2c2d
Fix version parsing
drook207 Oct 27, 2025
52f1efc
Add linker and toolchain to global kit file
drook207 Oct 27, 2025
4481b50
Add automatic kit scanning
drook207 Oct 27, 2025
e58c87d
Add user command for kit scanning
drook207 Oct 27, 2025
a1bc307
Use path from const table
drook207 Nov 12, 2025
adeb9e5
Use vim mkdir builtin
drook207 Nov 12, 2025
e0bd683
Fix: Config path for cmake kits
drook207 Jan 20, 2026
a6602cd
Feature: Use vim builtin json functions
drook207 Jan 20, 2026
b71824b
Feature: Add tests
drook207 Jan 20, 2026
49f717d
Merge pull request #1 from Civitasv/master
drook207 Jan 20, 2026
6f259b5
Merge branch 'master' into feature-add-kit-scanner
drook207 Jan 20, 2026
3022b5c
Fix: Make regex more simple
drook207 Jan 20, 2026
e547e0d
Fix: Use built in functions
drook207 Jan 20, 2026
e44aae1
Refactor: Improve kit detection
drook207 Apr 10, 2026
dd64e52
Merge branch 'Civitasv:master' into master
drook207 Apr 11, 2026
9bf4e1b
Refactor: Reimplement scanner function
drook207 Apr 11, 2026
8d06f2d
Merge branch 'master' into feature-add-kit-scanner
drook207 Apr 11, 2026
ea837cc
Refactor: Remove Debug logs
drook207 Apr 11, 2026
8975ae3
feat(constants): Add cmake-tools config path
drook207 Apr 11, 2026
12d1018
fix(scanner): Create config dir if not exists
drook207 Apr 11, 2026
9f1c58b
fix(scanner):Move initial scan for kits to setup
drook207 Apr 11, 2026
47b60a8
fix(autocommands): Add missing implementation for scan for kits
drook207 Apr 11, 2026
e3723fe
refactor: Use stdpath(config) for path expansion
drook207 Apr 16, 2026
9d46a1e
refactor: Reduce noise while scanning for kits
drook207 Apr 16, 2026
75c8279
refactor: name loop variable more meaningfull
drook207 Apr 16, 2026
684d9eb
refactor: Remove unused files
drook207 Apr 16, 2026
2898fd0
fix: make path separator system dependant
drook207 Apr 16, 2026
02a80c1
refactor: return boolean as the function name suggests
drook207 Apr 16, 2026
b83a051
refactor: move executable variable to global scope
drook207 Apr 16, 2026
757cd4a
refactor: remove unnecessary conditions
drook207 Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codebook.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
words = ["cxx"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this file was not meant to be added?

3 changes: 2 additions & 1 deletion lua/cmake-tools/const.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ local const = {
---@type string|fun(): string
target = vim.loop.cwd, -- path or function returning path to directory, this is used only if action == "soft_link" or action == "copy"
},
cmake_kits_path = nil, -- this is used to specify global cmake kits path, see CMakeKits for detailed usage
cmake_config_path = vim.fn.expand("~") .. "/.config/cmake-tools/", -- this is used to specify global cmake config path
cmake_kits_path = vim.fn.expand("~") .. "/.config/cmake-tools/cmake-kits.json", -- this is used to specify global cmake kits path, see CMakeKits for detailed usage
Comment thread
drook207 marked this conversation as resolved.
Outdated
cmake_variants_message = {
short = { show = true }, -- whether to show short message
long = { show = true, max_length = 40 }, -- whether to show long message
Expand Down
10 changes: 9 additions & 1 deletion lua/cmake-tools/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local const = require("cmake-tools.const")
local Config = require("cmake-tools.config")
local variants = require("cmake-tools.variants")
local kits = require("cmake-tools.kits")
local scanner = require("cmake-tools.scanner")
local Presets = require("cmake-tools.presets")
local log = require("cmake-tools.log")
local hints = require("cmake-tools.hints")
Expand Down Expand Up @@ -65,6 +66,9 @@ function cmake.setup(values)
cmake.register_autocmd()
cmake.register_autocmd_provided_by_users()
cmake.register_scratch_buffer(config.executor.name, config.runner.name)
if not vim.uv.fs_stat(const.cmake_kits_path) then
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maye we should add an option like we did for the presets to enable kit usage?

I think it might be worth keeping const.cmake_kits_path = nil as default and not change existing behaviour 🤔

scanner.scan_for_kits()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a synchronous which can block the ui on startup. I think it is work running async, right?

end
end

---@param callback fun(result: cmake.Result)
Expand Down Expand Up @@ -214,7 +218,7 @@ function cmake.generate(opt, callback)

-- if exists cmake-kits.json, kit is used to set
-- environmental variables and args.
local kits_config = kits.parse(const.cmake_kits_path, config.cwd)
local kits_config = kits.parse(const.cmake_config_path, config.cwd)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has to stay const.cmake_kits_path, right?

If so, do we need const.cmake_config_path at all?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parse function expects not only a path but also the filename for the cmake kits. But I agree to leave the const.cmake_kits_path nil.
The questions is should we add the kits filename to the const table and concat the config path and the filename ?

if kits_config and not config.kit then
return cmake.select_kit(function(result)
if not result:is_ok() then
Expand Down Expand Up @@ -770,6 +774,10 @@ function cmake.select_build_type(callback)
)
end

function cmake.scan_for_kits()
scanner.scan_for_kits()
end

---@param callback? fun(result: cmake.Result)
function cmake.select_kit(callback)
callback = type(callback) == "function" and callback
Expand Down
186 changes: 186 additions & 0 deletions lua/cmake-tools/scanner.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
local constants = require("cmake-tools.const")
local scanner = {}
Comment thread
drook207 marked this conversation as resolved.

local C_COMPILERS = { "gcc", "clang" }
local TOOLCHAIN_SEARCH_PATHS = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not familiar with kits, but AFAIK there is no "default toolchain location" in cmake. If we need to fine the toolchain, we might need to check the CMAKE_TOOLCHAIN_FILE env var, don't we?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I misunderstand the kits documentation, but I think the toolchain file is part of the kit...
https://vector-of-bool.github.io/docs/vscode-cmake-tools/kits.html#toolchain

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay yes, thanks for the link. Maybe we could still add the ability to check the CMAKE_TOOLCHAIN_FILE env variable on cross compilation kits. For me this would be a big advantage because I am working mostly with embedded linux devices and c++. And I find it quiet annoying to manualy adjust the kits file after detection.
But yeah that a niche use case.

vim.fn.expand("~/.cmake/toolchains"),
"/usr/share/cmake/toolchains",
"/usr/local/share/cmake/toolchains",
"/etc/cmake/toolchains",
}

local function toolchain_candidates(prefix)
if prefix == "" then
return {}
end

local triplet = prefix:gsub("%-$", "")

return {
triplet .. ".cmake",
triplet .. "-toolchain.cmake",
"toolchain-" .. triplet .. ".cmake",
}
end

local function find_toolchain_file(prefix)
local candidates = toolchain_candidates(prefix)
if #candidates == 0 then
return nil
end

for _, search_dir in ipairs(TOOLCHAIN_SEARCH_PATHS) do
for _, filename in ipairs(candidates) do
local full_path = search_dir .. "/" .. filename
if vim.fn.filereadable(full_path) == 1 then
return full_path
end
end
end

return nil
end
local function match_c_compiler(exe)
for _, c_name in ipairs(C_COMPILERS) do
Comment thread
drook207 marked this conversation as resolved.
Outdated
local prefix = exe:match("^(.+%-)" .. c_name .. "$")
if prefix then
return prefix, c_name
end
if exe == c_name then
return "", c_name
end
end
return nil, nil
end
local function derive_toolchain(prefix, c_name)
local map = {
gcc = { cxx = "g++", linker = "ld" },
clang = { cxx = "clang++", linker = "lld" },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, but I think clang might also use ld in some cases. Have to double check that tho

}
local companions = map[c_name]
if not companions then
return nil
end

return {
c = (prefix or "") .. c_name,
cxx = (prefix or "") .. companions.cxx,
linker = (prefix or "") .. companions.linker,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks again for the docs. So should we maybe take out the linker detection complety? Becaus if I get it correct, vscode's cmake-tools does not add the linker at all. It is again a manual step.

prefix = prefix or "",
}
end

local function get_path_executables()
local path_dirs = vim.split(vim.env.PATH or "", ":", { plain = true })
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will break on windows

local executables = {}
for _, dir in ipairs(path_dirs) do
local entries = vim.fn.readdir(dir)
for _, entry in ipairs(entries) do
local full = dir .. "/" .. entry
if vim.fn.executable(full) == 1 then
executables[entry] = true
end
end
end
return executables
end
local function discover_toolchains(executables)
local seen = {}
local chains = {}

for exe in pairs(executables) do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this filtering could be moved to the point of searching the paths variable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get what you mean, sorry. Could you maybe give a further explenation?

local prefix, c_name = match_c_compiler(exe)
if c_name then
local key = prefix .. c_name
if not seen[key] then
seen[key] = true
local chain = derive_toolchain(prefix, c_name)
if chain then
table.insert(chains, chain)
end
end
end
end
return chains
end

local function check_executable_exists(compiler)
if not compiler or compiler == "" then
return nil
Comment thread
drook207 marked this conversation as resolved.
Outdated
end
local exists = vim.fn.executable(compiler) == 1
return exists
end

local function get_executable_path(compiler)
if not compiler or compiler == "" then
return nil
end
local path = vim.fn.exepath(compiler)
return path
end

local function get_compiler_version(compiler)
if not compiler or compiler == "" then
return nil
end
local version_output = vim.fn.system({ compiler, "--version" })
local version = version_output:match("%d+%.%d+%.%d+")
return version
end

-- Main function to scan for kits
function scanner.scan_for_kits()
Comment thread
drook207 marked this conversation as resolved.
vim.notify("Scanning for kits…")

local executables = get_path_executables()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be a module level variable. No need to parse that everytime we call scan_for_kits

local toolchains = discover_toolchains(executables)
local kits = {}

for _, tc in ipairs(toolchains) do
local has_c = check_executable_exists(tc.c)
local has_cxx = check_executable_exists(tc.cxx)

if has_c then
local kit = { compilers = {} }

local version = get_compiler_version(tc.c)
local prefix_label = tc.prefix ~= "" and (tc.prefix:gsub("%-$", "") .. " ") or ""
kit.name = prefix_label .. tc.c .. " " .. (version or "Unknown")

kit.compilers.C = get_executable_path(tc.c)

if has_cxx then
kit.compilers.CXX = get_executable_path(tc.cxx)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be called unconditionally. vim.fn.exepath should yield the correct result

end

if check_executable_exists(tc.linker) then
kit.linker = get_executable_path(tc.linker)
end
local toolchain_file = find_toolchain_file(tc.prefix)
if toolchain_file then
kit.toolchainFile = toolchain_file
vim.notify("Toolchain file found: " .. toolchain_file)
else
if tc.prefix ~= "" then
vim.notify("No toolchain file found for prefix: " .. tc.prefix, vim.log.levels.WARN)
end
end
table.insert(kits, kit)
end
end
if vim.fn.isdirectory(constants.cmake_config_path) == 0 then
vim.fn.mkdir(constants.cmake_config_path, "p")
end
local json_kits = vim.fn.json_encode(kits)
if json_kits then
vim.fn.writefile({ json_kits }, constants.cmake_kits_path)
vim.notify("Kits saved to: " .. constants.cmake_kits_path)
else
vim.notify("Failed to encode kits to JSON.", vim.log.levels.ERROR)
end
vim.notify("Scanning complete.")
return kits
end

return scanner
1 change: 1 addition & 0 deletions lua/cmake-tools/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ local Types = {
"ANOTHER_JOB_RUNNING",
"CMAKE_RUN_FAILED",
"SETTINGS_ALREADY_OPENED",
"CANNOT_FIND_CMAKE_KITS",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this return type is not used, is it?

}

Types[0] = "SUCCESS"
Expand Down
9 changes: 9 additions & 0 deletions plugin/cmake-tools.lua
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ vim.api.nvim_create_user_command(
desc = "CMake select cmake kit",
}
)
--- CMake scan for kits
vim.api.nvim_create_user_command(
"CMakeScanForKits", -- name
Comment thread
drook207 marked this conversation as resolved.
cmake_tools.scan_for_kits, -- command
{ -- opts
nargs = 0,
desc = "CMake scan for cmake kits",
}
)

--- CMake select configure preset
vim.api.nvim_create_user_command(
Expand Down
11 changes: 11 additions & 0 deletions scripts/scan_kits.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- scripts/scan_kits.lua
Comment thread
drook207 marked this conversation as resolved.
Outdated
package.path = package.path .. ";./lua/?.lua;./lua/?/init.lua"

local scanner = require("cmake-tools.scanner")

local function main()
local kits = scanner.scan_for_kits()
print(vim.inspect(kits)) -- Use simple print in standalone
end

main()
106 changes: 106 additions & 0 deletions tests/scanner_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
-- tests/scanner_spec.lua
describe("scanner", function()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests have sideeffects which should be avoided:
a) they write to the actual filesystem (config path).
b) they run gcc --version and clang --version in a subprocess which introduces a depenceny to the host system and there is not deterministic.

local scanner

before_each(function()
require("plenary.reload").reload_module("cmake-tools.scanner")
scanner = require("cmake-tools.scanner")
end)

it("returns a table", function()
local kits = scanner.scan_for_kits()
assert.is_table(kits)
end)

it("finds a kit when gcc is installed", function()
if vim.fn.executable("gcc") ~= 1 then
pending("gcc not available on this system")
return
end
local kits = scanner.scan_for_kits()
assert.is_true(#kits > 0)

local gcc_kit
for _, kit in ipairs(kits) do
if kit.compilers and kit.compilers.C and kit.compilers.C:find("gcc") then
gcc_kit = kit
break
end
end
assert.is_not_nil(gcc_kit, "expected a kit with a gcc C compiler")
end)

it("kit has a name field", function()
local kits = scanner.scan_for_kits()
for _, kit in ipairs(kits) do
assert.is_string(kit.name)
assert.is_true(#kit.name > 0)
end
end)

it("kit compilers.C is a non-empty path", function()
local kits = scanner.scan_for_kits()
for _, kit in ipairs(kits) do
assert.is_string(kit.compilers.C)
assert.is_true(#kit.compilers.C > 0)
end
end)

it("kit compilers.CXX is set when the cxx companion exists", function()
local kits = scanner.scan_for_kits()
for _, kit in ipairs(kits) do
if kit.compilers.CXX then
assert.is_string(kit.compilers.CXX)
assert.is_true(#kit.compilers.CXX > 0)
end
end
end)

it("kit linker is a path string when set", function()
local kits = scanner.scan_for_kits()
for _, kit in ipairs(kits) do
if kit.linker then
assert.is_string(kit.linker)
assert.is_true(#kit.linker > 0)
end
end
end)

it("finds a clang kit when clang is installed", function()
if vim.fn.executable("clang") ~= 1 then
pending("clang not available on this system")
return
end
local kits = scanner.scan_for_kits()
local clang_kit
for _, kit in ipairs(kits) do
if kit.compilers and kit.compilers.C and kit.compilers.C:find("clang") then
clang_kit = kit
break
end
end
assert.is_not_nil(clang_kit, "expected a kit with a clang C compiler")
if clang_kit.compilers.CXX then
assert.is_true(clang_kit.compilers.CXX:find("clang++") ~= nil)
end
end)

it("toolchainFile is a string path when set", function()
local kits = scanner.scan_for_kits()
for _, kit in ipairs(kits) do
if kit.toolchainFile then
assert.is_string(kit.toolchainFile)
assert.is_true(vim.fn.filereadable(kit.toolchainFile) == 1)
end
end
end)
end)

describe("kits", function()
it("parse from global file", function()
local kit = require("cmake-tools.kits")
local const = require("cmake-tools.const")
local kits = kit.get(const.cmake_kits_path, vim.loop.cwd())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test requires the other tests to have written the file, making tests order dependent. That should be avoided

assert(#kits > 0)
end)
end)