diff --git a/lua/cmake-tools/const.lua b/lua/cmake-tools/const.lua index 71f02535..eb732360 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.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 diff --git a/lua/cmake-tools/init.lua b/lua/cmake-tools/init.lua index 6a1007fc..9efb440b 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") @@ -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 + scanner.scan_for_kits() + end end ---@param callback fun(result: cmake.Result) @@ -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) if kits_config and not config.kit then return cmake.select_kit(function(result) if not result:is_ok() then @@ -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 diff --git a/lua/cmake-tools/scanner.lua b/lua/cmake-tools/scanner.lua new file mode 100644 index 00000000..592076d3 --- /dev/null +++ b/lua/cmake-tools/scanner.lua @@ -0,0 +1,172 @@ +local constants = require("cmake-tools.const") +local scanner = {} + +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", +} + +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 _, compiler_name in ipairs(C_COMPILERS) do + local prefix = exe:match("^(.+%-)" .. compiler_name .. "$") + if prefix then + return prefix, compiler_name + end + if exe == compiler_name then + return "", compiler_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" }, + } + 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, + prefix = prefix or "", + } +end + +local function get_path_executables() + 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) + 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 + 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 false + 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 + +local executables = get_path_executables() + +function scanner.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) + + 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) + + 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) + 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) + end + return kits +end + +return scanner 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 ccc74651..5a1a26b2 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( diff --git a/tests/scanner_spec.lua b/tests/scanner_spec.lua new file mode 100644 index 00000000..e0b1ac42 --- /dev/null +++ b/tests/scanner_spec.lua @@ -0,0 +1,106 @@ +-- tests/scanner_spec.lua +describe("scanner", function() + 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()) + assert(#kits > 0) + end) +end)