-
Notifications
You must be signed in to change notification settings - Fork 106
Add automatic kit scanning functionality #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 25 commits
822b317
09afee7
206038a
e4f2c2d
52f1efc
4481b50
e58c87d
a1bc307
adeb9e5
e0bd683
a6602cd
b71824b
49f717d
6f259b5
3022b5c
e547e0d
e44aae1
dd64e52
9bf4e1b
8d06f2d
ea837cc
8975ae3
12d1018
9f1c58b
47b60a8
e3723fe
9d46a1e
75c8279
684d9eb
2898fd0
02a80c1
b83a051
757cd4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| words = ["cxx"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| scanner.scan_for_kits() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this has to stay If so, do we need
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| local constants = require("cmake-tools.const") | ||
| local scanner = {} | ||
|
drook207 marked this conversation as resolved.
|
||
|
|
||
| local C_COMPILERS = { "gcc", "clang" } | ||
| local TOOLCHAIN_SEARCH_PATHS = { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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 | ||
|
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" }, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure, but I think clang might also use |
||
| } | ||
| 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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is actually wrong for lld. See It is always a cross-linker, meaning that it always supports all the above targets however it was built. In fact, we don’t provide a build-time option to enable/disable each target. This should make it easy to use our linker as part of a cross-compile toolchain.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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() | ||
|
drook207 marked this conversation as resolved.
|
||
| vim.notify("Scanning for kits…") | ||
|
|
||
| local executables = get_path_executables() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be called unconditionally. |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ local Types = { | |
| "ANOTHER_JOB_RUNNING", | ||
| "CMAKE_RUN_FAILED", | ||
| "SETTINGS_ALREADY_OPENED", | ||
| "CANNOT_FIND_CMAKE_KITS", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this return type is not used, is it? |
||
| } | ||
|
|
||
| Types[0] = "SUCCESS" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| -- scripts/scan_kits.lua | ||
|
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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| -- tests/scanner_spec.lua | ||
| describe("scanner", function() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests have sideeffects which should be avoided: |
||
| 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()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment.
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?