aboutsummaryrefslogtreecommitdiff
path: root/lua/tasks/module/cmake.lua
blob: 5aeca25d3b009437e113d2c41d38c0143a84246c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
local Path = require('plenary.path')
local utils = require('tasks.utils')
local scandir = require('plenary.scandir')
local ProjectConfig = require('tasks.project_config')
local os = require('ffi').os:lower()
local cmake = {}

--- Parses build dir expression.
---@param dir string: Path with expressions to replace.
---@param build_type string
---@return table
local function parse_dir(dir, build_type)
  local parsed_dir = dir:gsub('{cwd}', vim.loop.cwd())
  parsed_dir = parsed_dir:gsub('{os}', os)
  parsed_dir = parsed_dir:gsub('{build_type}', build_type:lower())
  return Path:new(parsed_dir)
end

--- Returns reply directory that contains targets information.
---@param build_dir table
---@return unknown
local function get_reply_dir(build_dir) return build_dir / '.cmake' / 'api' / 'v1' / 'reply' end

--- Reads information about target.
---@param codemodel_target table
---@param reply_dir table
---@return table
local function get_target_info(codemodel_target, reply_dir) return vim.json.decode((reply_dir / codemodel_target['jsonFile']):read()) end

--- Creates query files that to acess information about targets after CMake configuration.
---@param build_dir table
---@return boolean: Returns `true` on success.
local function make_query_files(build_dir)
  local query_dir = build_dir / '.cmake' / 'api' / 'v1' / 'query'
  if not query_dir:mkdir({ parents = true }) then
    utils.notify(string.format('Unable to create "%s"', query_dir.filename), vim.log.levels.ERROR)
    return false
  end

  local codemodel_file = query_dir / 'codemodel-v2'
  if not codemodel_file:is_file() then
    if not codemodel_file:touch() then
      utils.notify(string.format('Unable to create "%s"', codemodel_file.filename), vim.log.levels.ERROR)
      return false
    end
  end
  return true
end

--- Reads targets information.
---@param reply_dir table
---@return table?
local function get_codemodel_targets(reply_dir)
  local found_files = scandir.scan_dir(reply_dir.filename, { search_pattern = 'codemodel*' })
  if #found_files == 0 then
    utils.notify('Unable to find codemodel file', vim.log.levels.ERROR)
    return nil
  end
  local codemodel = Path:new(found_files[1])
  local codemodel_json = vim.json.decode(codemodel:read())
  return codemodel_json['configurations'][1]['targets']
end

---@return table?
local function get_target_names()
  local project_config = ProjectConfig.new()
  local build_dir = parse_dir(project_config.cmake.build_dir, project_config.cmake.build_type)
  if not build_dir:is_dir() then
    utils.notify(string.format('Build directory "%s" does not exist, you need to run "configure" task first', build_dir), vim.log.levels.ERROR)
    return nil
  end

  local reply_dir = get_reply_dir(build_dir)
  local codemodel_targets = get_codemodel_targets(reply_dir)
  if not codemodel_targets then
    return nil
  end

  local targets = {}
  for _, target in ipairs(codemodel_targets) do
    local target_info = get_target_info(target, reply_dir)
    local target_name = target_info['name']
    if target_name:find('_autogen') == nil then
      table.insert(targets, target_name)
    end
  end

  return targets
end

--- Finds path to an executable.
---@param build_dir table
---@param name string
---@param reply_dir table
---@return unknown?
local function get_executable_path(build_dir, name, reply_dir)
  for _, target in ipairs(get_codemodel_targets(reply_dir)) do
    if name == target['name'] then
      local target_info = get_target_info(target, reply_dir)
      if target_info['type'] ~= 'EXECUTABLE' then
        utils.notify(string.format('Specified target "%s" is not an executable', name), vim.log.levels.ERROR)
        return nil
      end

      local target_path = Path:new(target_info['artifacts'][1]['path'])
      if not target_path:is_absolute() then
        target_path = build_dir / target_path
      end

      return target_path
    end
  end

  utils.notify(string.format('Unable to find target named "%s"', name), vim.log.levels.ERROR)
  return nil
end

--- Copies compile_commands.json file from build directory to the current working directory for LSP integration.
local function copy_compile_commands()
  local project_config = ProjectConfig.new()
  local filename = 'compile_commands.json'
  local source = parse_dir(project_config.cmake.build_dir, project_config.cmake.build_type) / filename
  local destination = Path:new(vim.loop.cwd(), filename)
  source:copy({ destination = destination.filename })
end

--- Task
---@param module_config table
---@return table?
local function configure(module_config, _)
  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)
  build_dir:mkdir({ parents = true })
  if not make_query_files(build_dir) then
    return nil
  end

  return {
    cmd = module_config.cmd,
    args = { '-B', build_dir.filename, '-D', 'CMAKE_BUILD_TYPE=' .. module_config.build_type },
    after_success = copy_compile_commands,
  }
end

--- Task
---@param module_config table
---@return table
local function build(module_config, _)
  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)

  local args = { '--build', build_dir.filename }
  if module_config.target then
    vim.list_extend(args, { '--target', module_config.target })
  end

  return {
    cmd = module_config.cmd,
    args = args,
    after_success = copy_compile_commands,
  }
end

--- Task
---@param module_config table
---@return table
local function build_all(module_config, _)
  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)

  return {
    cmd = module_config.cmd,
    args = { '--build', build_dir.filename },
    after_success = copy_compile_commands,
  }
end

--- Task
---@param module_config table
---@return table?
local function run(module_config, _)
  if not module_config.target then
    utils.notify('No selected target, please set "target" parameter', vim.log.levels.ERROR)
    return nil
  end

  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)
  if not build_dir:is_dir() then
    utils.notify(string.format('Build directory "%s" does not exist, you need to run "configure" task first', build_dir), vim.log.levels.ERROR)
    return nil
  end

  local target_path = get_executable_path(build_dir, module_config.target, get_reply_dir(build_dir))
  if not target_path then
    return
  end

  if not target_path:is_file() then
    utils.notify(string.format('Selected target "%s" is not built', target_path.filename), vim.log.levels.ERROR)
    return nil
  end

  return {
    cmd = target_path.filename,
    cwd = target_path:parent().filename,
  }
end

--- Task
---@param module_config table
---@return table?
local function debug(module_config, _)
  if module_config.build_type ~= 'Debug' and module_config.build_type ~= 'RelWithDebInfo' then
    utils.notify(
      string.format('For debugging your "build_type" param should be set to "Debug" or "RelWithDebInfo", but your current build type is "%s"'),
      module_config.build_type,
      vim.log.levels.ERROR
    )
    return nil
  end

  local command = run(module_config, nil)
  if not command then
    return nil
  end

  command.dap_name = module_config.dap_name
  return command
end

--- Task
---@param module_config table
---@return table
local function clean(module_config, _)
  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)

  return {
    cmd = module_config.cmd,
    args = { '--build', build_dir.filename, '--target', 'clean' },
    after_success = copy_compile_commands,
  }
end

--- Task
---@param module_config table
---@return table
local function open_build_dir(module_config, _)
  local build_dir = parse_dir(module_config.build_dir, module_config.build_type)

  return {
    cmd = os == 'windows' and 'start' or 'xdg-open',
    args = { build_dir.filename },
    ignore_stdout = true,
    ignore_stderr = true,
  }
end

cmake.params = {
  target = get_target_names,
  build_type = { 'Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel' },
  'cmake',
  'dap_name',
}
cmake.condition = function() return Path:new('CMakeLists.txt'):exists() end
cmake.tasks = {
  configure = configure,
  build = build,
  build_all = build_all,
  run = { build, run },
  debug = { build, debug },
  clean = clean,
  open_build_dir = open_build_dir,
}

return cmake