From 822b3177e1eb4016c5bf5d92cf37dae17c911e42 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 25 Oct 2025 23:19:53 +0200 Subject: [PATCH 01/29] Add basic implementation for kit scanner --- lua/cmake-tools/init.lua | 1 + lua/cmake-tools/scanner.lua | 181 ++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 lua/cmake-tools/scanner.lua diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index 042d767b..e375c392 100644 --- a/lua/cmake-tools/init.lua +++ b/lua/cmake-tools/init.lua @@ -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") diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua new file mode 100644 index 00000000..68331994 --- /dev/null +++ b/lua/cmake-tools/scanner.lua @@ -0,0 +1,181 @@ +local scanner = {} +-- Configuration +scanner.HOME = os.getenv("HOME") or os.getenv("USERPROFILE") +scanner.KITS_FILE = scanner.HOME .. "/.config/cmake-tools/cmake-kits.json" + +--Helper functions +-- Simple JSON encoder +function scanner.json_encode(obj, indent) + indent = indent or 0 + local spaces = string.rep(" ", indent) + + if type(obj) == "table" then + local is_array = #obj > 0 + local result = "{\n" + local first = true + + for k, v in pairs(obj) do + if not first then + result = result .. ",\n" + end + first = false + + if is_array then + result = result .. spaces .. " " .. scanner.json_encode(v, indent + 1) + else + result = result .. spaces .. ' "' .. k .. '": ' .. scanner.json_encode(v, indent + 1) + end + end + + return result .. "\n" .. spaces .. "}" + elseif type(obj) == "string" then + return '"' .. obj:gsub("\\", "\\\\"):gsub('"', '\\"') .. '"' + elseif type(obj) == "number" or type(obj) == "boolean" then + return tostring(obj) + elseif obj == nil then + return "null" + end +end + +function scanner.execute_command(cmd) + local handle = io.popen(cmd .. "2>&1") + if handle == nil then + return false, -1, "" + end + local result = handle:read("*a") + if result == nil then + result = "" + end + local success, _, exit_code = handle:close() + if success == nil then + success = false + end + return success, exit_code, result +end + +function scanner.file_exists(path) + local file = io.open(path, "r") + if file then + file:close() + return true + else + return false + end +end + +function scanner.split_path(path_env) + local paths = {} + local sep = package.config:sub(1, 1) == "\\" and ";" or ":" + for path in string.gmatch(path_env, "([^" .. sep .. "]+)") do + table.insert(paths, path) + end + return paths +end + +function scanner.get_gcc_version(gcc_path) + local success, exit_code, output = scanner.execute_command('"' .. gcc_path .. '" --version') + if not success or exit_code ~= 0 then + return nil + end + local version_line = output:match("gcc version ([%d%.]+)") + return version_line +end + +function scanner.get_clang_version(clang_path) + local success, exit_code, output = scanner.execute_command('"' .. clang_path .. '" --version') + if not success or exit_code ~= 0 then + return nil + end + local version_line = output:match("clang version ([%d%.]+)") + return version_line +end + +function scanner.find_compiler_pair(dir, c_compiler) + local base_name = c_compiler:match("([^/\\]+)$") + local cxx_name + if base_name:match("gcc") then + cxx_name = base_name:gsub("gcc", "g++") + elseif base_name:match("clang") then + cxx_name = base_name:gsub("clang", "clang++") + else + return nil + end + local cxx_path = dir .. "/" .. cxx_name + if scanner.file_exists(cxx_path) then + return cxx_path + end + return nil +end + +function scanner.ensure_directory(path) + local lfs = require("lfs") + local pattern = "(.*/)" + local dir = path:match(pattern) + if dir then + lfs.mkdir(dir) + end +end + +function scanner.save_kits(kits, filepath) + scanner.ensure_directory(filepath) + local file = io.open(filepath, "w") + if not file then + error("Failed to open file for writing: " .. filepath) + return false + end + local json_content = scanner.json_encode(kits) + file:write(json_content) + file:close() + return true +end + +-- Main fucntion to scan for kits +function scanner.scan_for_kits() + local kits = {} + + local path_env = os.getenv("PATH") or "" + local paths = scanner.split_path(path_env) + + for _, dir in ipairs(paths) do + local gcc_path = dir .. "/gcc" + if scanner.file_exists(gcc_path) then + local gcc_version = scanner.get_gcc_version(gcc_path) + local gxx_path = scanner.find_compiler_pair(dir, gcc_path) + if gxx_path then + local kit = { + name = "GCC " .. (gcc_version or "unknown"), + compilers = { + C = gcc_path, + CXX = gxx_path, + }, + } + table.insert(kits, kit) + end + end + + local clang_path = dir .. "/clang" + if scanner.file_exists(clang_path) then + local clang_version = scanner.get_clang_version(clang_path) + local clangxx_path = scanner.find_compiler_pair(dir, clang_path) + if clangxx_path then + local kit = { + name = "Clang " .. (clang_version or "unknown"), + compilers = { + C = clang_path, + CXX = clangxx_path, + }, + } + table.insert(kits, kit) + end + end + end + + if #kits == 0 then + print("No compilers found in PATH.") + return {} + end + scanner.save_kits(kits, scanner.KITS_FILE) + return kits +end + +return scanner From 09afee7e9245a4f22638e6445ea61adc23223ea0 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 25 Oct 2025 23:40:02 +0200 Subject: [PATCH 02/29] Fix call to make directory --- lua/cmake-tools/scanner.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 68331994..5d824994 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -108,11 +108,10 @@ function scanner.find_compiler_pair(dir, c_compiler) end function scanner.ensure_directory(path) - local lfs = require("lfs") local pattern = "(.*/)" local dir = path:match(pattern) if dir then - lfs.mkdir(dir) + os.execute('mkdir -p "' .. dir .. '"') end end From 206038a4f6fff1768c029d3a97279d40270cb106 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Mon, 27 Oct 2025 21:52:12 +0100 Subject: [PATCH 03/29] Add global config dir for kit file --- lua/cmake-tools/const.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmake-tools/const.lua b/lua/cmake-tools/const.lua index 55f41d96..70809702 100644 --- a/lua/cmake-tools/const.lua +++ b/lua/cmake-tools/const.lua @@ -21,7 +21,7 @@ local const = { -- none: this will make this option ignored target = vim.loop.cwd(), -- 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_kits_path = "~/.config/cmake-tools/", -- this is used to specify global cmake kits path, see CMakeKits for detailed usage cmake_variants_message = { short = { show = true }, -- whether to show short message long = { show = true, max_length = 40 }, -- whether to show long message From e4f2c2d8b27aa615f8f628876c8fc994d3884d2b Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Mon, 27 Oct 2025 21:53:27 +0100 Subject: [PATCH 04/29] Fix version parsing --- lua/cmake-tools/scanner.lua | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 5d824994..ac7bd8ab 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -38,7 +38,7 @@ function scanner.json_encode(obj, indent) end function scanner.execute_command(cmd) - local handle = io.popen(cmd .. "2>&1") + local handle = io.popen(cmd .. " 2>&1") if handle == nil then return false, -1, "" end @@ -46,11 +46,12 @@ function scanner.execute_command(cmd) if result == nil then result = "" end - local success, _, exit_code = handle:close() + local success, exit_type, exit_code = handle:close() + -- io.popen's close() returns: true on success, or nil, "exit", code on failure if success == nil then - success = false + return false, exit_code or -1, result end - return success, exit_code, result + return true, 0, result end function scanner.file_exists(path) @@ -74,16 +75,18 @@ end function scanner.get_gcc_version(gcc_path) local success, exit_code, output = scanner.execute_command('"' .. gcc_path .. '" --version') - if not success or exit_code ~= 0 then + if output == nil then return nil end - local version_line = output:match("gcc version ([%d%.]+)") - return version_line + -- Try multiple patterns to match different gcc output formats + local version = output:match("gcc%s+%(GCC%)%s+([%d%.]+)") -- "gcc (GCC) 15.2.1" + or output:match("gcc version ([%d%.]+)") -- "gcc version 11.4.0" + return version end function scanner.get_clang_version(clang_path) - local success, exit_code, output = scanner.execute_command('"' .. clang_path .. '" --version') - if not success or exit_code ~= 0 then + local success, exit_code, output = scanner.execute_command('"' .. clang_path .. '" --version ') + if output == nil then return nil end local version_line = output:match("clang version ([%d%.]+)") @@ -142,7 +145,7 @@ function scanner.scan_for_kits() local gxx_path = scanner.find_compiler_pair(dir, gcc_path) if gxx_path then local kit = { - name = "GCC " .. (gcc_version or "unknown"), + name = "GCC-" .. (gcc_version or "unknown"), compilers = { C = gcc_path, CXX = gxx_path, @@ -158,7 +161,7 @@ function scanner.scan_for_kits() local clangxx_path = scanner.find_compiler_pair(dir, clang_path) if clangxx_path then local kit = { - name = "Clang " .. (clang_version or "unknown"), + name = "Clang-" .. (clang_version or "unknown"), compilers = { C = clang_path, CXX = clangxx_path, From 52f1efc9d173c99ec9ad08a36aeca9fc43cbce8f Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Mon, 27 Oct 2025 21:53:57 +0100 Subject: [PATCH 05/29] Add linker and toolchain to global kit file --- lua/cmake-tools/scanner.lua | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index ac7bd8ab..9e68ae69 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -110,6 +110,25 @@ function scanner.find_compiler_pair(dir, c_compiler) return nil end +function scanner.find_linker_pair(dir, linker_name) + if not linker_name then + return nil + end + local linker_path = dir .. "/" .. linker_name + if scanner.file_exists(linker_path) then + return linker_path + end + return nil +end + +function scanner.get_toolchain_file() + local toolchainFile = os.getenv("CMAKE_TOOLCHAIN_FILE") + if toolchainFile and scanner.file_exists(toolchainFile) then + return toolchainFile + end + return nil +end + function scanner.ensure_directory(path) local pattern = "(.*/)" local dir = path:match(pattern) @@ -139,6 +158,11 @@ function scanner.scan_for_kits() local paths = scanner.split_path(path_env) for _, dir in ipairs(paths) do + local linker_path = scanner.find_linker_pair(dir, "lld") + if linker_path == nil then + linker_path = scanner.find_linker_pair(dir, "ld") + end + local toolchainFile = scanner.get_toolchain_file() local gcc_path = dir .. "/gcc" if scanner.file_exists(gcc_path) then local gcc_version = scanner.get_gcc_version(gcc_path) @@ -150,6 +174,8 @@ function scanner.scan_for_kits() C = gcc_path, CXX = gxx_path, }, + linker = (linker_path or ""), + toolchainFile = (toolchainFile or ""), } table.insert(kits, kit) end @@ -166,6 +192,8 @@ function scanner.scan_for_kits() C = clang_path, CXX = clangxx_path, }, + linker = (linker_path or ""), + toolchainFile = (toolchainFile or ""), } table.insert(kits, kit) end From 4481b506e22fce7f75dc3ee71c6952aa5e9755f2 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Mon, 27 Oct 2025 21:54:38 +0100 Subject: [PATCH 06/29] Add automatic kit scanning --- lua/cmake-tools/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index e375c392..0bc291c1 100644 --- a/lua/cmake-tools/init.lua +++ b/lua/cmake-tools/init.lua @@ -206,6 +206,9 @@ 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) + if not kits_config then + kits_config = scanner.scan_for_kits() + end if kits_config and not config.kit then return cmake.select_kit(function(result) if not result:is_ok() then From e58c87d74b87142a736da7a086b6072725b61787 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Mon, 27 Oct 2025 21:55:06 +0100 Subject: [PATCH 07/29] Add user command for kit scanning --- lua/cmake-tools/init.lua | 23 +++++++++++++++++++++++ lua/cmake-tools/types.lua | 1 + plugin/cmake-tools.lua | 9 +++++++++ 3 files changed, 33 insertions(+) diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index 0bc291c1..ae954fd6 100644 --- a/lua/cmake-tools/init.lua +++ b/lua/cmake-tools/init.lua @@ -746,6 +746,29 @@ function cmake.select_build_type(callback) ) end +function cmake.scan_for_kits(callback) + callback = type(callback) == "function" and callback + or function(result) + if result:is_ok() then + cmake.generate({ bang = false, fargs = {} }, nil) + end + end + if check_active_job_and_notify(callback) then + return + end + + if get_cmake_configuration_or_notify(callback) == nil then + return + end + + local kits = scanner.scan_for_kits() + if kits then + callback(Result:new(Types.SUCCESS, nil, nil)) + else + callback(Result:new_error(Types.CANNOT_FIND_CMAKE_KITS, "Cannot find CMakeKits file")) + end +end + function cmake.select_kit(callback) callback = type(callback) == "function" and callback or function(result) diff --git a/lua/cmake-tools/types.lua b/lua/cmake-tools/types.lua index 41e748ce..e748a7ab 100644 --- a/lua/cmake-tools/types.lua +++ b/lua/cmake-tools/types.lua @@ -18,6 +18,7 @@ local Types = { "ANOTHER_JOB_RUNNING", "CMAKE_RUN_FAILED", "SETTINGS_ALREADY_OPENED", + "CANNOT_FIND_CMAKE_KITS", } Types[0] = "SUCCESS" diff --git a/plugin/cmake-tools.lua b/plugin/cmake-tools.lua index d9fa0f20..ffc2274b 100644 --- a/plugin/cmake-tools.lua +++ b/plugin/cmake-tools.lua @@ -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 + 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( From a1bc307cdfa277de01bc9bd12e5fb86e8ea13d2a Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Wed, 12 Nov 2025 21:15:31 +0100 Subject: [PATCH 08/29] Use path from const table --- lua/cmake-tools/scanner.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 9e68ae69..dd622f09 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -1,7 +1,7 @@ +local const = require("cmake-tools.const") local scanner = {} -- Configuration -scanner.HOME = os.getenv("HOME") or os.getenv("USERPROFILE") -scanner.KITS_FILE = scanner.HOME .. "/.config/cmake-tools/cmake-kits.json" +scanner.KITS_FILE = const.cmake_kits_path --Helper functions -- Simple JSON encoder From adeb9e56f8d738a30225fd037483aa8929ad1eef Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Wed, 12 Nov 2025 21:25:25 +0100 Subject: [PATCH 09/29] Use vim mkdir builtin --- lua/cmake-tools/scanner.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index dd622f09..4e400d85 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -133,7 +133,7 @@ function scanner.ensure_directory(path) local pattern = "(.*/)" local dir = path:match(pattern) if dir then - os.execute('mkdir -p "' .. dir .. '"') + vim.fn.mkdir(dir, "p") end end From e0bd683b28eb31da851b94db34b9756e48609b9e Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Tue, 20 Jan 2026 21:39:45 +0100 Subject: [PATCH 10/29] Fix: Config path for cmake kits Tilde was not exapanded correctly, that results in creating the kits file in creating the whole path in $PWD --- lua/cmake-tools/const.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmake-tools/const.lua b/lua/cmake-tools/const.lua index 70809702..f8793f87 100644 --- a/lua/cmake-tools/const.lua +++ b/lua/cmake-tools/const.lua @@ -21,7 +21,7 @@ local const = { -- none: this will make this option ignored target = vim.loop.cwd(), -- path to directory, this is used only if action == "soft_link" or action == "copy" }, - cmake_kits_path = "~/.config/cmake-tools/", -- this is used to specify global cmake kits path, see CMakeKits for detailed usage + 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 cmake_variants_message = { short = { show = true }, -- whether to show short message long = { show = true, max_length = 40 }, -- whether to show long message From a6602cdccc9338cf098ed0131561b0d48ddd4af0 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Tue, 20 Jan 2026 21:40:23 +0100 Subject: [PATCH 11/29] Feature: Use vim builtin json functions --- lua/cmake-tools/scanner.lua | 123 ++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 70 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 4e400d85..f58d7521 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -1,43 +1,7 @@ -local const = require("cmake-tools.const") local scanner = {} --- Configuration -scanner.KITS_FILE = const.cmake_kits_path --Helper functions --- Simple JSON encoder -function scanner.json_encode(obj, indent) - indent = indent or 0 - local spaces = string.rep(" ", indent) - - if type(obj) == "table" then - local is_array = #obj > 0 - local result = "{\n" - local first = true - - for k, v in pairs(obj) do - if not first then - result = result .. ",\n" - end - first = false - - if is_array then - result = result .. spaces .. " " .. scanner.json_encode(v, indent + 1) - else - result = result .. spaces .. ' "' .. k .. '": ' .. scanner.json_encode(v, indent + 1) - end - end - - return result .. "\n" .. spaces .. "}" - elseif type(obj) == "string" then - return '"' .. obj:gsub("\\", "\\\\"):gsub('"', '\\"') .. '"' - elseif type(obj) == "number" or type(obj) == "boolean" then - return tostring(obj) - elseif obj == nil then - return "null" - end -end - -function scanner.execute_command(cmd) +local function execute_command(cmd) local handle = io.popen(cmd .. " 2>&1") if handle == nil then return false, -1, "" @@ -46,15 +10,18 @@ function scanner.execute_command(cmd) if result == nil then result = "" end - local success, exit_type, exit_code = handle:close() - -- io.popen's close() returns: true on success, or nil, "exit", code on failure + local success, exit_code = handle:close() if success == nil then return false, exit_code or -1, result end return true, 0, result end -function scanner.file_exists(path) +local function file_exists(path) + if path == nil then + vim.notify("Path is empty", vim.log.levels.ERROR) + return + end local file = io.open(path, "r") if file then file:close() @@ -64,7 +31,7 @@ function scanner.file_exists(path) end end -function scanner.split_path(path_env) +local function split_path(path_env) local paths = {} local sep = package.config:sub(1, 1) == "\\" and ";" or ":" for path in string.gmatch(path_env, "([^" .. sep .. "]+)") do @@ -73,8 +40,8 @@ function scanner.split_path(path_env) return paths end -function scanner.get_gcc_version(gcc_path) - local success, exit_code, output = scanner.execute_command('"' .. gcc_path .. '" --version') +local function get_gcc_version(gcc_path) + local success, exit_code, output = execute_command('"' .. gcc_path .. '" --version') if output == nil then return nil end @@ -84,8 +51,8 @@ function scanner.get_gcc_version(gcc_path) return version end -function scanner.get_clang_version(clang_path) - local success, exit_code, output = scanner.execute_command('"' .. clang_path .. '" --version ') +local function get_clang_version(clang_path) + local success, exit_code, output = execute_command('"' .. clang_path .. '" --version ') if output == nil then return nil end @@ -93,7 +60,7 @@ function scanner.get_clang_version(clang_path) return version_line end -function scanner.find_compiler_pair(dir, c_compiler) +local function find_compiler_pair(dir, c_compiler) local base_name = c_compiler:match("([^/\\]+)$") local cxx_name if base_name:match("gcc") then @@ -104,32 +71,36 @@ function scanner.find_compiler_pair(dir, c_compiler) return nil end local cxx_path = dir .. "/" .. cxx_name - if scanner.file_exists(cxx_path) then + if file_exists(cxx_path) then return cxx_path end return nil end -function scanner.find_linker_pair(dir, linker_name) +local function find_linker_pair(dir, linker_name) if not linker_name then return nil end local linker_path = dir .. "/" .. linker_name - if scanner.file_exists(linker_path) then + if file_exists(linker_path) then return linker_path end return nil end -function scanner.get_toolchain_file() +local function get_toolchain_file() local toolchainFile = os.getenv("CMAKE_TOOLCHAIN_FILE") - if toolchainFile and scanner.file_exists(toolchainFile) then + if toolchainFile and file_exists(toolchainFile) then return toolchainFile end return nil end -function scanner.ensure_directory(path) +local function ensure_directory(path) + if not path then + vim.notify("Path is empty", vim.log.levels.ERROR) + return + end local pattern = "(.*/)" local dir = path:match(pattern) if dir then @@ -137,36 +108,40 @@ function scanner.ensure_directory(path) end end -function scanner.save_kits(kits, filepath) - scanner.ensure_directory(filepath) +local function save_kits(kits, filepath) + ensure_directory(filepath) local file = io.open(filepath, "w") if not file then - error("Failed to open file for writing: " .. filepath) + vim.notify("Failed to open file for writing: " .. filepath, vim.log.levels.ERROR) + return false + end + if not kits then + vim.notify("Can not encode data to json because it is nil", vim.log.levels.ERROR) return false end - local json_content = scanner.json_encode(kits) + local json_content = vim.json.encode(kits) file:write(json_content) file:close() return true end --- Main fucntion to scan for kits +-- Main function to scan for kits function scanner.scan_for_kits() local kits = {} - + local const = require("cmake-tools.const") local path_env = os.getenv("PATH") or "" - local paths = scanner.split_path(path_env) + local paths = split_path(path_env) for _, dir in ipairs(paths) do - local linker_path = scanner.find_linker_pair(dir, "lld") + local linker_path = find_linker_pair(dir, "lld") if linker_path == nil then - linker_path = scanner.find_linker_pair(dir, "ld") + linker_path = find_linker_pair(dir, "ld") end - local toolchainFile = scanner.get_toolchain_file() + local toolchainFile = get_toolchain_file() local gcc_path = dir .. "/gcc" - if scanner.file_exists(gcc_path) then - local gcc_version = scanner.get_gcc_version(gcc_path) - local gxx_path = scanner.find_compiler_pair(dir, gcc_path) + if file_exists(gcc_path) then + local gcc_version = get_gcc_version(gcc_path) + local gxx_path = find_compiler_pair(dir, gcc_path) if gxx_path then local kit = { name = "GCC-" .. (gcc_version or "unknown"), @@ -182,9 +157,9 @@ function scanner.scan_for_kits() end local clang_path = dir .. "/clang" - if scanner.file_exists(clang_path) then - local clang_version = scanner.get_clang_version(clang_path) - local clangxx_path = scanner.find_compiler_pair(dir, clang_path) + if file_exists(clang_path) then + local clang_version = get_clang_version(clang_path) + local clangxx_path = find_compiler_pair(dir, clang_path) if clangxx_path then local kit = { name = "Clang-" .. (clang_version or "unknown"), @@ -201,10 +176,18 @@ function scanner.scan_for_kits() end if #kits == 0 then - print("No compilers found in PATH.") + vim.notify("No compilers found in PATH.", vim.log.levels.WARN) + return {} + end + vim.notify("Found kits", vim.log.levels.INFO) + if const.cmake_kits_path == nil then + vim.notify( + "local const variable is nil, it seems that the required module could not be loaded", + vim.log.levels.ERROR + ) return {} end - scanner.save_kits(kits, scanner.KITS_FILE) + save_kits(kits, const.cmake_kits_path) return kits end From b71824b0a45b9c1bf3a3b7aac251e54dbace3a19 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Tue, 20 Jan 2026 21:41:41 +0100 Subject: [PATCH 12/29] Feature: Add tests - Add a test for kit scanning - Add a test that checks if the file is present and if the kits could be registered --- scripts/scan_kits.lua | 11 +++++++++++ tests/scanner_spec.lua | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 scripts/scan_kits.lua create mode 100644 tests/scanner_spec.lua diff --git a/scripts/scan_kits.lua b/scripts/scan_kits.lua new file mode 100644 index 00000000..72443548 --- /dev/null +++ b/scripts/scan_kits.lua @@ -0,0 +1,11 @@ +-- scripts/scan_kits.lua +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() diff --git a/tests/scanner_spec.lua b/tests/scanner_spec.lua new file mode 100644 index 00000000..fb5831d2 --- /dev/null +++ b/tests/scanner_spec.lua @@ -0,0 +1,17 @@ +-- tests/scanner_spec.lua +describe("scanner", function() + it("scan for kits", function() + local scanner = require("cmake-tools.scanner") + local kits = scanner.scan_for_kits() + assert(#kits > 0) + 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()) + assert(#kits > 0) + end) +end) From 3022b5c289b0bcd7eed376bc872ee2599a8499e3 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Tue, 20 Jan 2026 21:58:01 +0100 Subject: [PATCH 13/29] Fix: Make regex more simple --- lua/cmake-tools/scanner.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index f58d7521..4ab9fe61 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -46,7 +46,7 @@ local function get_gcc_version(gcc_path) return nil end -- Try multiple patterns to match different gcc output formats - local version = output:match("gcc%s+%(GCC%)%s+([%d%.]+)") -- "gcc (GCC) 15.2.1" + local version = output:match("gcc[%s%a]([%d%.]+)") -- "gcc (GCC) 15.2.1" or output:match("gcc version ([%d%.]+)") -- "gcc version 11.4.0" return version end From e547e0d85ebb12e72e4fcb473e7cbea64ac832ea Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Tue, 20 Jan 2026 21:58:27 +0100 Subject: [PATCH 14/29] Fix: Use built in functions --- lua/cmake-tools/scanner.lua | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 4ab9fe61..5f42ba9b 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -17,20 +17,6 @@ local function execute_command(cmd) return true, 0, result end -local function file_exists(path) - if path == nil then - vim.notify("Path is empty", vim.log.levels.ERROR) - return - end - local file = io.open(path, "r") - if file then - file:close() - return true - else - return false - end -end - local function split_path(path_env) local paths = {} local sep = package.config:sub(1, 1) == "\\" and ";" or ":" @@ -71,7 +57,7 @@ local function find_compiler_pair(dir, c_compiler) return nil end local cxx_path = dir .. "/" .. cxx_name - if file_exists(cxx_path) then + if vim.fn.filereadable(cxx_path) then return cxx_path end return nil @@ -82,7 +68,7 @@ local function find_linker_pair(dir, linker_name) return nil end local linker_path = dir .. "/" .. linker_name - if file_exists(linker_path) then + if vim.fn.filereadable(linker_path) then return linker_path end return nil @@ -90,7 +76,7 @@ end local function get_toolchain_file() local toolchainFile = os.getenv("CMAKE_TOOLCHAIN_FILE") - if toolchainFile and file_exists(toolchainFile) then + if toolchainFile and vim.fn.filereadable(toolchainFile) then return toolchainFile end return nil @@ -139,7 +125,7 @@ function scanner.scan_for_kits() end local toolchainFile = get_toolchain_file() local gcc_path = dir .. "/gcc" - if file_exists(gcc_path) then + if vim.fn.filereadable(gcc_path) then local gcc_version = get_gcc_version(gcc_path) local gxx_path = find_compiler_pair(dir, gcc_path) if gxx_path then @@ -157,7 +143,7 @@ function scanner.scan_for_kits() end local clang_path = dir .. "/clang" - if file_exists(clang_path) then + if vim.fn.filereadable(clang_path) then local clang_version = get_clang_version(clang_path) local clangxx_path = find_compiler_pair(dir, clang_path) if clangxx_path then From e44aae17cb98988ec3a4248fb170fe9c2f835e7a Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Fri, 10 Apr 2026 20:49:08 +0200 Subject: [PATCH 15/29] Refactor: Improve kit detection --- codebook.toml | 1 + lua/cmake-tools/scanner.lua | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 codebook.toml diff --git a/codebook.toml b/codebook.toml new file mode 100644 index 00000000..5fdb2fef --- /dev/null +++ b/codebook.toml @@ -0,0 +1 @@ +words = ["cxx"] diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 5f42ba9b..572e2cc2 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -2,19 +2,17 @@ local scanner = {} --Helper functions local function execute_command(cmd) - local handle = io.popen(cmd .. " 2>&1") - if handle == nil then - return false, -1, "" + print(cmd) + + if cmd == nil then + return false, -1, nil end - local result = handle:read("*a") + local result = vim.system(cmd, { text = true }):wait() if result == nil then - result = "" - end - local success, exit_code = handle:close() - if success == nil then - return false, exit_code or -1, result + return false, -1, nil end - return true, 0, result + print(result.stdout) + return true, result.code, result.stdout end local function split_path(path_env) @@ -27,7 +25,7 @@ local function split_path(path_env) end local function get_gcc_version(gcc_path) - local success, exit_code, output = execute_command('"' .. gcc_path .. '" --version') + local success, exit_code, output = execute_command({ "gcc", "--version" }) if output == nil then return nil end @@ -38,7 +36,8 @@ local function get_gcc_version(gcc_path) end local function get_clang_version(clang_path) - local success, exit_code, output = execute_command('"' .. clang_path .. '" --version ') + local success, exit_code, output = execute_command({ "clang", "--version" }) + if output == nil then return nil end From 9bf4e1b75e0f1638ded2c8e93487f0bb76b2292c Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 10:06:37 +0200 Subject: [PATCH 16/29] Refactor: Reimplement scanner function - Complete reimplementation of the scanner functionality using nvim's internal functions and a more generic approach. - Add possibility to detect cross platform compiler - Make the scanner more extensible - Add unit tests for the new functions --- lua/cmake-tools/scanner.lua | 287 +++++++++++++++++++----------------- tests/scanner_spec.lua | 95 +++++++++++- 2 files changed, 243 insertions(+), 139 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 572e2cc2..e58fd520 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -1,178 +1,193 @@ +local constants = require("cmake-tools.const") local scanner = {} ---Helper functions -local function execute_command(cmd) - print(cmd) +local C_COMPILERS = { "gcc", "clang" } +local TOOLCHAIN_SEARCH_PATHS = { + vim.fn.expand("~/.cmake/toolchains"), + "/usr/share/cmake/toolchains", + "/usr/local/share/cmake/toolchains", + "/etc/cmake/toolchains", +} - if cmd == nil then - return false, -1, nil - end - local result = vim.system(cmd, { text = true }):wait() - if result == nil then - return false, -1, nil +local function toolchain_candidates(prefix) + if prefix == "" then + return {} end - print(result.stdout) - return true, result.code, result.stdout -end -local function split_path(path_env) - local paths = {} - local sep = package.config:sub(1, 1) == "\\" and ";" or ":" - for path in string.gmatch(path_env, "([^" .. sep .. "]+)") do - table.insert(paths, path) - end - return paths + -- strip trailing dash for the filename + local triplet = prefix:gsub("%-$", "") + + return { + triplet .. ".cmake", + triplet .. "-toolchain.cmake", + "toolchain-" .. triplet .. ".cmake", + } end -local function get_gcc_version(gcc_path) - local success, exit_code, output = execute_command({ "gcc", "--version" }) - if output == nil then +local function find_toolchain_file(prefix) + local candidates = toolchain_candidates(prefix) + if #candidates == 0 then return nil end - -- Try multiple patterns to match different gcc output formats - local version = output:match("gcc[%s%a]([%d%.]+)") -- "gcc (GCC) 15.2.1" - or output:match("gcc version ([%d%.]+)") -- "gcc version 11.4.0" - return version -end -local function get_clang_version(clang_path) - local success, exit_code, output = execute_command({ "clang", "--version" }) + 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 - if output == nil then - return nil + return nil +end +local function match_c_compiler(exe) + for _, c_name in ipairs(C_COMPILERS) do + local prefix = exe:match("^(.+%-)" .. c_name .. "$") + if prefix then + return prefix, c_name + end + -- Without prefix: "gcc" + if exe == c_name then + return "", c_name + end end - local version_line = output:match("clang version ([%d%.]+)") - return version_line + return nil, nil end - -local function find_compiler_pair(dir, c_compiler) - local base_name = c_compiler:match("([^/\\]+)$") - local cxx_name - if base_name:match("gcc") then - cxx_name = base_name:gsub("gcc", "g++") - elseif base_name:match("clang") then - cxx_name = base_name:gsub("clang", "clang++") - else +local function derive_toolchain(prefix, c_name) + local map = { + gcc = { cxx = "g++", linker = "ld" }, + clang = { cxx = "clang++", linker = "lld" }, + } + local companions = map[c_name] + if not companions then return nil end - local cxx_path = dir .. "/" .. cxx_name - if vim.fn.filereadable(cxx_path) then - return cxx_path - end - return nil + + return { + c = (prefix or "") .. c_name, + cxx = (prefix or "") .. companions.cxx, + linker = (prefix or "") .. companions.linker, + -- preserve prefix so we can use it in the kit name + prefix = prefix or "", + } end -local function find_linker_pair(dir, linker_name) - if not linker_name then - return nil +local function get_path_executables() + local path_dirs = vim.split(vim.env.PATH or "", ":", { plain = true }) + local executables = {} + for _, dir in ipairs(path_dirs) do + local entries = vim.fn.readdir(dir) -- returns {} on error / missing dir + for _, entry in ipairs(entries) do + local full = dir .. "/" .. entry + if vim.fn.executable(full) == 1 then + executables[entry] = true -- deduplicate by name + end + end end - local linker_path = dir .. "/" .. linker_name - if vim.fn.filereadable(linker_path) then - return linker_path + return executables +end +local function discover_toolchains(executables) + local seen = {} + local chains = {} + + for exe in pairs(executables) do + 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 nil + vim.notify("Discovered toolchains: " .. vim.inspect(chains)) + return chains end -local function get_toolchain_file() - local toolchainFile = os.getenv("CMAKE_TOOLCHAIN_FILE") - if toolchainFile and vim.fn.filereadable(toolchainFile) then - return toolchainFile +local function check_executable_exists(compiler) + if not compiler or compiler == "" then + return nil end - return nil + local exists = vim.fn.executable(compiler) == 1 + return exists end -local function ensure_directory(path) - if not path then - vim.notify("Path is empty", vim.log.levels.ERROR) - return - end - local pattern = "(.*/)" - local dir = path:match(pattern) - if dir then - vim.fn.mkdir(dir, "p") +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 save_kits(kits, filepath) - ensure_directory(filepath) - local file = io.open(filepath, "w") - if not file then - vim.notify("Failed to open file for writing: " .. filepath, vim.log.levels.ERROR) - return false - end - if not kits then - vim.notify("Can not encode data to json because it is nil", vim.log.levels.ERROR) - return false +local function get_compiler_version(compiler) + if not compiler or compiler == "" then + return nil end - local json_content = vim.json.encode(kits) - file:write(json_content) - file:close() - return true + 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() + vim.notify("Scanning for kits…") + + local executables = get_path_executables() + local toolchains = discover_toolchains(executables) local kits = {} - local const = require("cmake-tools.const") - local path_env = os.getenv("PATH") or "" - local paths = split_path(path_env) - - for _, dir in ipairs(paths) do - local linker_path = find_linker_pair(dir, "lld") - if linker_path == nil then - linker_path = find_linker_pair(dir, "ld") - end - local toolchainFile = get_toolchain_file() - local gcc_path = dir .. "/gcc" - if vim.fn.filereadable(gcc_path) then - local gcc_version = get_gcc_version(gcc_path) - local gxx_path = find_compiler_pair(dir, gcc_path) - if gxx_path then - local kit = { - name = "GCC-" .. (gcc_version or "unknown"), - compilers = { - C = gcc_path, - CXX = gxx_path, - }, - linker = (linker_path or ""), - toolchainFile = (toolchainFile or ""), - } - table.insert(kits, kit) + + 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) + else + vim.notify("No C++ compiler found for: " .. tc.c) end - end - local clang_path = dir .. "/clang" - if vim.fn.filereadable(clang_path) then - local clang_version = get_clang_version(clang_path) - local clangxx_path = find_compiler_pair(dir, clang_path) - if clangxx_path then - local kit = { - name = "Clang-" .. (clang_version or "unknown"), - compilers = { - C = clang_path, - CXX = clangxx_path, - }, - linker = (linker_path or ""), - toolchainFile = (toolchainFile or ""), - } - table.insert(kits, kit) + if check_executable_exists(tc.linker) then + kit.linker = get_executable_path(tc.linker) + else + vim.notify("No linker found for: " .. tc.c) + 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 - end - end - if #kits == 0 then - vim.notify("No compilers found in PATH.", vim.log.levels.WARN) - return {} + table.insert(kits, kit) + else + vim.notify("Skipping toolchain – C compiler not found: " .. tc.c) + end end - vim.notify("Found kits", vim.log.levels.INFO) - if const.cmake_kits_path == nil then - vim.notify( - "local const variable is nil, it seems that the required module could not be loaded", - vim.log.levels.ERROR - ) - return {} + 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 - save_kits(kits, const.cmake_kits_path) + vim.notify("Scanning complete.") return kits end diff --git a/tests/scanner_spec.lua b/tests/scanner_spec.lua index fb5831d2..e0b1ac42 100644 --- a/tests/scanner_spec.lua +++ b/tests/scanner_spec.lua @@ -1,9 +1,98 @@ -- tests/scanner_spec.lua describe("scanner", function() - it("scan for kits", function() - local scanner = require("cmake-tools.scanner") + 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(#kits > 0) + 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) From ea837ccf7665444ff3243643c03702aa9360bee2 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 10:11:32 +0200 Subject: [PATCH 17/29] Refactor: Remove Debug logs --- lua/cmake-tools/scanner.lua | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index e58fd520..e5e817c3 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -14,7 +14,6 @@ local function toolchain_candidates(prefix) return {} end - -- strip trailing dash for the filename local triplet = prefix:gsub("%-$", "") return { @@ -47,7 +46,6 @@ local function match_c_compiler(exe) if prefix then return prefix, c_name end - -- Without prefix: "gcc" if exe == c_name then return "", c_name end @@ -68,7 +66,6 @@ local function derive_toolchain(prefix, c_name) c = (prefix or "") .. c_name, cxx = (prefix or "") .. companions.cxx, linker = (prefix or "") .. companions.linker, - -- preserve prefix so we can use it in the kit name prefix = prefix or "", } end @@ -77,11 +74,11 @@ local function get_path_executables() local path_dirs = vim.split(vim.env.PATH or "", ":", { plain = true }) local executables = {} for _, dir in ipairs(path_dirs) do - local entries = vim.fn.readdir(dir) -- returns {} on error / missing dir + 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 -- deduplicate by name + executables[entry] = true end end end @@ -104,7 +101,6 @@ local function discover_toolchains(executables) end end end - vim.notify("Discovered toolchains: " .. vim.inspect(chains)) return chains end @@ -156,14 +152,10 @@ function scanner.scan_for_kits() if has_cxx then kit.compilers.CXX = get_executable_path(tc.cxx) - else - vim.notify("No C++ compiler found for: " .. tc.c) end if check_executable_exists(tc.linker) then kit.linker = get_executable_path(tc.linker) - else - vim.notify("No linker found for: " .. tc.c) end local toolchain_file = find_toolchain_file(tc.prefix) if toolchain_file then @@ -174,10 +166,7 @@ function scanner.scan_for_kits() vim.notify("No toolchain file found for prefix: " .. tc.prefix, vim.log.levels.WARN) end end - table.insert(kits, kit) - else - vim.notify("Skipping toolchain – C compiler not found: " .. tc.c) end end local json_kits = vim.fn.json_encode(kits) From 8975ae359e63645d7b4b5cfdb0c360a56428c77a Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 20:38:56 +0200 Subject: [PATCH 18/29] feat(constants): Add cmake-tools config path --- lua/cmake-tools/const.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/cmake-tools/const.lua b/lua/cmake-tools/const.lua index aeb35e50..76390de3 100644 --- a/lua/cmake-tools/const.lua +++ b/lua/cmake-tools/const.lua @@ -24,6 +24,7 @@ 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_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 cmake_variants_message = { short = { show = true }, -- whether to show short message From 12d1018b352cfd63b87cd53c322c373667ad5dba Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 20:39:25 +0200 Subject: [PATCH 19/29] fix(scanner): Create config dir if not exists --- lua/cmake-tools/scanner.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index e5e817c3..0dba8de7 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -169,6 +169,9 @@ function scanner.scan_for_kits() 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) From 9f1c58becc536fd867ca4c9de82a84641c298f7b Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 21:35:47 +0200 Subject: [PATCH 20/29] fix(scanner):Move initial scan for kits to setup --- lua/cmake-tools/init.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index 22545cae..cdc4d19f 100644 --- a/lua/cmake-tools/init.lua +++ b/lua/cmake-tools/init.lua @@ -66,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 + scanner.scan_for_kits() + end end ---@param callback fun(result: cmake.Result) @@ -215,10 +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) - if not kits_config then - kits_config = scanner.scan_for_kits() - end + local kits_config = kits.parse(const.cmake_config_path, config.cwd) if kits_config and not config.kit then return cmake.select_kit(function(result) if not result:is_ok() then From 47b60a81e1c6b0875ecbe1b8f66948b474e4445f Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Sat, 11 Apr 2026 21:36:35 +0200 Subject: [PATCH 21/29] fix(autocommands): Add missing implementation for scan for kits --- lua/cmake-tools/init.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index cdc4d19f..9efb440b 100644 --- a/lua/cmake-tools/init.lua +++ b/lua/cmake-tools/init.lua @@ -774,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 From e3723fee94510905211e0c1a01ad0c3e60f87896 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 20:38:57 +0200 Subject: [PATCH 22/29] refactor: Use stdpath(config) for path expansion Refactor: Change cmake kits path back to nil --- lua/cmake-tools/const.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/cmake-tools/const.lua b/lua/cmake-tools/const.lua index 76390de3..eb732360 100644 --- a/lua/cmake-tools/const.lua +++ b/lua/cmake-tools/const.lua @@ -24,8 +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_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 + cmake_config_path = vim.fn.stdpath("config") .. "/cmake-tools/", -- this is used to specify global cmake config path + cmake_kits_path = nil, -- this is used to specify global cmake kits path, see CMakeKits for detailed usage cmake_variants_message = { short = { show = true }, -- whether to show short message long = { show = true, max_length = 40 }, -- whether to show long message From 9d46a1ecaf116c799c5367edacec018fb1cb39c7 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 20:44:41 +0200 Subject: [PATCH 23/29] refactor: Reduce noise while scanning for kits --- lua/cmake-tools/scanner.lua | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 0dba8de7..11ee2d64 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -131,8 +131,6 @@ end -- Main function to scan for kits function scanner.scan_for_kits() - vim.notify("Scanning for kits…") - local executables = get_path_executables() local toolchains = discover_toolchains(executables) local kits = {} @@ -160,11 +158,6 @@ function scanner.scan_for_kits() 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 @@ -175,11 +168,7 @@ function scanner.scan_for_kits() 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 From 75c8279b94be1eaa60bf3c4970640b236e628065 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 20:47:12 +0200 Subject: [PATCH 24/29] refactor: name loop variable more meaningfull --- lua/cmake-tools/scanner.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 11ee2d64..e4042178 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -41,13 +41,13 @@ local function find_toolchain_file(prefix) return nil end local function match_c_compiler(exe) - for _, c_name in ipairs(C_COMPILERS) do - local prefix = exe:match("^(.+%-)" .. c_name .. "$") + for _, compiler_name in ipairs(C_COMPILERS) do + local prefix = exe:match("^(.+%-)" .. compiler_name .. "$") if prefix then - return prefix, c_name + return prefix, compiler_name end - if exe == c_name then - return "", c_name + if exe == compiler_name then + return "", compiler_name end end return nil, nil From 684d9eb5a9ab5a1c9b54ec5bd92e703f7054a4ea Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 21:19:08 +0200 Subject: [PATCH 25/29] refactor: Remove unused files --- codebook.toml | 1 - scripts/scan_kits.lua | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 codebook.toml delete mode 100644 scripts/scan_kits.lua diff --git a/codebook.toml b/codebook.toml deleted file mode 100644 index 5fdb2fef..00000000 --- a/codebook.toml +++ /dev/null @@ -1 +0,0 @@ -words = ["cxx"] diff --git a/scripts/scan_kits.lua b/scripts/scan_kits.lua deleted file mode 100644 index 72443548..00000000 --- a/scripts/scan_kits.lua +++ /dev/null @@ -1,11 +0,0 @@ --- scripts/scan_kits.lua -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() From 2898fd043461213bd142e70afcb22445bdb0c347 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 21:40:58 +0200 Subject: [PATCH 26/29] fix: make path separator system dependant --- lua/cmake-tools/scanner.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index e4042178..b4c239f4 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -71,7 +71,8 @@ local function derive_toolchain(prefix, c_name) end local function get_path_executables() - local path_dirs = vim.split(vim.env.PATH or "", ":", { plain = true }) + local separator = vim.uv.os_uname().sysname == "Windows_NT" and ";" or ":" + local path_dirs = vim.split(vim.env.PATH or "", separator, { plain = true }) local executables = {} for _, dir in ipairs(path_dirs) do local entries = vim.fn.readdir(dir) From 02a80c1ebd004bdb875e4b98b03e83d378414746 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 21:41:24 +0200 Subject: [PATCH 27/29] refactor: return boolean as the function name suggests --- lua/cmake-tools/scanner.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index b4c239f4..54724a66 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -107,7 +107,7 @@ end local function check_executable_exists(compiler) if not compiler or compiler == "" then - return nil + return false end local exists = vim.fn.executable(compiler) == 1 return exists From b83a05196a3dedfae189e53b9ba9db7778880c1d Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 21:42:00 +0200 Subject: [PATCH 28/29] refactor: move executable variable to global scope fix: remove duplicated code line --- lua/cmake-tools/scanner.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 54724a66..7bc6dd7d 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -130,9 +130,9 @@ local function get_compiler_version(compiler) return version end --- Main function to scan for kits +local executables = get_path_executables() + function scanner.scan_for_kits() - local executables = get_path_executables() local toolchains = discover_toolchains(executables) local kits = {} From 757cd4a3a15416763e644466878437ae7b843fd0 Mon Sep 17 00:00:00 2001 From: Pascal Berndt Date: Thu, 16 Apr 2026 22:00:18 +0200 Subject: [PATCH 29/29] refactor: remove unnecessary conditions --- lua/cmake-tools/scanner.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua index 7bc6dd7d..592076d3 100644 --- a/lua/cmake-tools/scanner.lua +++ b/lua/cmake-tools/scanner.lua @@ -140,28 +140,24 @@ function scanner.scan_for_kits() local has_c = check_executable_exists(tc.c) local has_cxx = check_executable_exists(tc.cxx) - if has_c then - local kit = { compilers = {} } + 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") + 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) + kit.compilers.C = get_executable_path(tc.c) - if has_cxx then - kit.compilers.CXX = get_executable_path(tc.cxx) - end + kit.compilers.CXX = get_executable_path(tc.cxx) - 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 - end - table.insert(kits, kit) + 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 end + table.insert(kits, kit) end if vim.fn.isdirectory(constants.cmake_config_path) == 0 then vim.fn.mkdir(constants.cmake_config_path, "p")