Skip to content

Commit 9bf4e1b

Browse files
committed
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
1 parent e44aae1 commit 9bf4e1b

2 files changed

Lines changed: 243 additions & 139 deletions

File tree

lua/cmake-tools/scanner.lua

Lines changed: 151 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,193 @@
1+
local constants = require("cmake-tools.const")
12
local scanner = {}
23

3-
--Helper functions
4-
local function execute_command(cmd)
5-
print(cmd)
4+
local C_COMPILERS = { "gcc", "clang" }
5+
local TOOLCHAIN_SEARCH_PATHS = {
6+
vim.fn.expand("~/.cmake/toolchains"),
7+
"/usr/share/cmake/toolchains",
8+
"/usr/local/share/cmake/toolchains",
9+
"/etc/cmake/toolchains",
10+
}
611

7-
if cmd == nil then
8-
return false, -1, nil
9-
end
10-
local result = vim.system(cmd, { text = true }):wait()
11-
if result == nil then
12-
return false, -1, nil
12+
local function toolchain_candidates(prefix)
13+
if prefix == "" then
14+
return {}
1315
end
14-
print(result.stdout)
15-
return true, result.code, result.stdout
16-
end
1716

18-
local function split_path(path_env)
19-
local paths = {}
20-
local sep = package.config:sub(1, 1) == "\\" and ";" or ":"
21-
for path in string.gmatch(path_env, "([^" .. sep .. "]+)") do
22-
table.insert(paths, path)
23-
end
24-
return paths
17+
-- strip trailing dash for the filename
18+
local triplet = prefix:gsub("%-$", "")
19+
20+
return {
21+
triplet .. ".cmake",
22+
triplet .. "-toolchain.cmake",
23+
"toolchain-" .. triplet .. ".cmake",
24+
}
2525
end
2626

27-
local function get_gcc_version(gcc_path)
28-
local success, exit_code, output = execute_command({ "gcc", "--version" })
29-
if output == nil then
27+
local function find_toolchain_file(prefix)
28+
local candidates = toolchain_candidates(prefix)
29+
if #candidates == 0 then
3030
return nil
3131
end
32-
-- Try multiple patterns to match different gcc output formats
33-
local version = output:match("gcc[%s%a]([%d%.]+)") -- "gcc (GCC) 15.2.1"
34-
or output:match("gcc version ([%d%.]+)") -- "gcc version 11.4.0"
35-
return version
36-
end
3732

38-
local function get_clang_version(clang_path)
39-
local success, exit_code, output = execute_command({ "clang", "--version" })
33+
for _, search_dir in ipairs(TOOLCHAIN_SEARCH_PATHS) do
34+
for _, filename in ipairs(candidates) do
35+
local full_path = search_dir .. "/" .. filename
36+
if vim.fn.filereadable(full_path) == 1 then
37+
return full_path
38+
end
39+
end
40+
end
4041

41-
if output == nil then
42-
return nil
42+
return nil
43+
end
44+
local function match_c_compiler(exe)
45+
for _, c_name in ipairs(C_COMPILERS) do
46+
local prefix = exe:match("^(.+%-)" .. c_name .. "$")
47+
if prefix then
48+
return prefix, c_name
49+
end
50+
-- Without prefix: "gcc"
51+
if exe == c_name then
52+
return "", c_name
53+
end
4354
end
44-
local version_line = output:match("clang version ([%d%.]+)")
45-
return version_line
55+
return nil, nil
4656
end
47-
48-
local function find_compiler_pair(dir, c_compiler)
49-
local base_name = c_compiler:match("([^/\\]+)$")
50-
local cxx_name
51-
if base_name:match("gcc") then
52-
cxx_name = base_name:gsub("gcc", "g++")
53-
elseif base_name:match("clang") then
54-
cxx_name = base_name:gsub("clang", "clang++")
55-
else
57+
local function derive_toolchain(prefix, c_name)
58+
local map = {
59+
gcc = { cxx = "g++", linker = "ld" },
60+
clang = { cxx = "clang++", linker = "lld" },
61+
}
62+
local companions = map[c_name]
63+
if not companions then
5664
return nil
5765
end
58-
local cxx_path = dir .. "/" .. cxx_name
59-
if vim.fn.filereadable(cxx_path) then
60-
return cxx_path
61-
end
62-
return nil
66+
67+
return {
68+
c = (prefix or "") .. c_name,
69+
cxx = (prefix or "") .. companions.cxx,
70+
linker = (prefix or "") .. companions.linker,
71+
-- preserve prefix so we can use it in the kit name
72+
prefix = prefix or "",
73+
}
6374
end
6475

65-
local function find_linker_pair(dir, linker_name)
66-
if not linker_name then
67-
return nil
76+
local function get_path_executables()
77+
local path_dirs = vim.split(vim.env.PATH or "", ":", { plain = true })
78+
local executables = {}
79+
for _, dir in ipairs(path_dirs) do
80+
local entries = vim.fn.readdir(dir) -- returns {} on error / missing dir
81+
for _, entry in ipairs(entries) do
82+
local full = dir .. "/" .. entry
83+
if vim.fn.executable(full) == 1 then
84+
executables[entry] = true -- deduplicate by name
85+
end
86+
end
6887
end
69-
local linker_path = dir .. "/" .. linker_name
70-
if vim.fn.filereadable(linker_path) then
71-
return linker_path
88+
return executables
89+
end
90+
local function discover_toolchains(executables)
91+
local seen = {}
92+
local chains = {}
93+
94+
for exe in pairs(executables) do
95+
local prefix, c_name = match_c_compiler(exe)
96+
if c_name then
97+
local key = prefix .. c_name
98+
if not seen[key] then
99+
seen[key] = true
100+
local chain = derive_toolchain(prefix, c_name)
101+
if chain then
102+
table.insert(chains, chain)
103+
end
104+
end
105+
end
72106
end
73-
return nil
107+
vim.notify("Discovered toolchains: " .. vim.inspect(chains))
108+
return chains
74109
end
75110

76-
local function get_toolchain_file()
77-
local toolchainFile = os.getenv("CMAKE_TOOLCHAIN_FILE")
78-
if toolchainFile and vim.fn.filereadable(toolchainFile) then
79-
return toolchainFile
111+
local function check_executable_exists(compiler)
112+
if not compiler or compiler == "" then
113+
return nil
80114
end
81-
return nil
115+
local exists = vim.fn.executable(compiler) == 1
116+
return exists
82117
end
83118

84-
local function ensure_directory(path)
85-
if not path then
86-
vim.notify("Path is empty", vim.log.levels.ERROR)
87-
return
88-
end
89-
local pattern = "(.*/)"
90-
local dir = path:match(pattern)
91-
if dir then
92-
vim.fn.mkdir(dir, "p")
119+
local function get_executable_path(compiler)
120+
if not compiler or compiler == "" then
121+
return nil
93122
end
123+
local path = vim.fn.exepath(compiler)
124+
return path
94125
end
95126

96-
local function save_kits(kits, filepath)
97-
ensure_directory(filepath)
98-
local file = io.open(filepath, "w")
99-
if not file then
100-
vim.notify("Failed to open file for writing: " .. filepath, vim.log.levels.ERROR)
101-
return false
102-
end
103-
if not kits then
104-
vim.notify("Can not encode data to json because it is nil", vim.log.levels.ERROR)
105-
return false
127+
local function get_compiler_version(compiler)
128+
if not compiler or compiler == "" then
129+
return nil
106130
end
107-
local json_content = vim.json.encode(kits)
108-
file:write(json_content)
109-
file:close()
110-
return true
131+
local version_output = vim.fn.system({ compiler, "--version" })
132+
local version = version_output:match("%d+%.%d+%.%d+")
133+
return version
111134
end
112135

113136
-- Main function to scan for kits
114137
function scanner.scan_for_kits()
138+
vim.notify("Scanning for kits…")
139+
140+
local executables = get_path_executables()
141+
local toolchains = discover_toolchains(executables)
115142
local kits = {}
116-
local const = require("cmake-tools.const")
117-
local path_env = os.getenv("PATH") or ""
118-
local paths = split_path(path_env)
119-
120-
for _, dir in ipairs(paths) do
121-
local linker_path = find_linker_pair(dir, "lld")
122-
if linker_path == nil then
123-
linker_path = find_linker_pair(dir, "ld")
124-
end
125-
local toolchainFile = get_toolchain_file()
126-
local gcc_path = dir .. "/gcc"
127-
if vim.fn.filereadable(gcc_path) then
128-
local gcc_version = get_gcc_version(gcc_path)
129-
local gxx_path = find_compiler_pair(dir, gcc_path)
130-
if gxx_path then
131-
local kit = {
132-
name = "GCC-" .. (gcc_version or "unknown"),
133-
compilers = {
134-
C = gcc_path,
135-
CXX = gxx_path,
136-
},
137-
linker = (linker_path or ""),
138-
toolchainFile = (toolchainFile or ""),
139-
}
140-
table.insert(kits, kit)
143+
144+
for _, tc in ipairs(toolchains) do
145+
local has_c = check_executable_exists(tc.c)
146+
local has_cxx = check_executable_exists(tc.cxx)
147+
148+
if has_c then
149+
local kit = { compilers = {} }
150+
151+
local version = get_compiler_version(tc.c)
152+
local prefix_label = tc.prefix ~= "" and (tc.prefix:gsub("%-$", "") .. " ") or ""
153+
kit.name = prefix_label .. tc.c .. " " .. (version or "Unknown")
154+
155+
kit.compilers.C = get_executable_path(tc.c)
156+
157+
if has_cxx then
158+
kit.compilers.CXX = get_executable_path(tc.cxx)
159+
else
160+
vim.notify("No C++ compiler found for: " .. tc.c)
141161
end
142-
end
143162

144-
local clang_path = dir .. "/clang"
145-
if vim.fn.filereadable(clang_path) then
146-
local clang_version = get_clang_version(clang_path)
147-
local clangxx_path = find_compiler_pair(dir, clang_path)
148-
if clangxx_path then
149-
local kit = {
150-
name = "Clang-" .. (clang_version or "unknown"),
151-
compilers = {
152-
C = clang_path,
153-
CXX = clangxx_path,
154-
},
155-
linker = (linker_path or ""),
156-
toolchainFile = (toolchainFile or ""),
157-
}
158-
table.insert(kits, kit)
163+
if check_executable_exists(tc.linker) then
164+
kit.linker = get_executable_path(tc.linker)
165+
else
166+
vim.notify("No linker found for: " .. tc.c)
167+
end
168+
local toolchain_file = find_toolchain_file(tc.prefix)
169+
if toolchain_file then
170+
kit.toolchainFile = toolchain_file
171+
vim.notify("Toolchain file found: " .. toolchain_file)
172+
else
173+
if tc.prefix ~= "" then
174+
vim.notify("No toolchain file found for prefix: " .. tc.prefix, vim.log.levels.WARN)
175+
end
159176
end
160-
end
161-
end
162177

163-
if #kits == 0 then
164-
vim.notify("No compilers found in PATH.", vim.log.levels.WARN)
165-
return {}
178+
table.insert(kits, kit)
179+
else
180+
vim.notify("Skipping toolchain – C compiler not found: " .. tc.c)
181+
end
166182
end
167-
vim.notify("Found kits", vim.log.levels.INFO)
168-
if const.cmake_kits_path == nil then
169-
vim.notify(
170-
"local const variable is nil, it seems that the required module could not be loaded",
171-
vim.log.levels.ERROR
172-
)
173-
return {}
183+
local json_kits = vim.fn.json_encode(kits)
184+
if json_kits then
185+
vim.fn.writefile({ json_kits }, constants.cmake_kits_path)
186+
vim.notify("Kits saved to: " .. constants.cmake_kits_path)
187+
else
188+
vim.notify("Failed to encode kits to JSON.", vim.log.levels.ERROR)
174189
end
175-
save_kits(kits, const.cmake_kits_path)
190+
vim.notify("Scanning complete.")
176191
return kits
177192
end
178193

0 commit comments

Comments
 (0)