444 lines
14 KiB
VimL
444 lines
14 KiB
VimL
|
" ==============================================================================
|
||
|
" Location: autoload/cmake/buildsys.vim
|
||
|
" Description: Functions for generating the buildsystem
|
||
|
" ==============================================================================
|
||
|
|
||
|
let s:buildsys = {}
|
||
|
let s:buildsys.cmake_version = 0
|
||
|
let s:buildsys.project_root = ''
|
||
|
let s:buildsys.current_config = ''
|
||
|
let s:buildsys.path_to_current_config = ''
|
||
|
let s:buildsys.configs = []
|
||
|
let s:buildsys.targets = []
|
||
|
|
||
|
let s:logger = cmake#logger#Get()
|
||
|
let s:statusline = cmake#statusline#Get()
|
||
|
let s:system = cmake#system#Get()
|
||
|
let s:terminal = cmake#terminal#Get()
|
||
|
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
" Private functions
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
|
||
|
" Find project root by looking for g:cmake_root_markers upwards.
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" escaped path to the root of the project
|
||
|
"
|
||
|
function! s:FindProjectRoot() abort
|
||
|
let l:root = getcwd()
|
||
|
let l:escaped_cwd = fnameescape(getcwd())
|
||
|
for l:marker in g:cmake_root_markers
|
||
|
" Search CWD upward for l:marker, assuming it is a file.
|
||
|
let l:marker_path = findfile(l:marker, l:escaped_cwd . ';' . $HOME)
|
||
|
if len(l:marker_path) > 0
|
||
|
" If found, strip l:marker from it.
|
||
|
let l:root = fnamemodify(l:marker_path, ':h')
|
||
|
break
|
||
|
endif
|
||
|
" Search CWD upward for l:marker, assuming it is a directory.
|
||
|
let l:marker_path = finddir(l:marker, l:escaped_cwd . ';' . $HOME)
|
||
|
if len(l:marker_path) > 0
|
||
|
" If found, strip l:marker from it.
|
||
|
let l:root = fnamemodify(l:marker_path, ':h')
|
||
|
break
|
||
|
endif
|
||
|
endfor
|
||
|
return l:root
|
||
|
endfunction
|
||
|
|
||
|
" Get absolute path to location where the build directory is located.
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" path to build directory location
|
||
|
"
|
||
|
function! s:GetBuildDirLocation() abort
|
||
|
return s:system.Path(
|
||
|
\ [s:buildsys.project_root, g:cmake_build_dir_location], v:false)
|
||
|
endfunction
|
||
|
|
||
|
" Find CMake variable in list of options.
|
||
|
"
|
||
|
" Params:
|
||
|
" opts : List
|
||
|
" list of options
|
||
|
" variable : String
|
||
|
" variable to find
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" value of the CMake variable, or an empty string if the variable was
|
||
|
" not found
|
||
|
"
|
||
|
" Example:
|
||
|
" to find the variable 'CMAKE_BUILD_TYPE', which would be passed by the user
|
||
|
" as '-D CMAKE_BUILD_TYPE=<value>', call
|
||
|
" s:FindVarInOpts(opts, 'CMAKE_BUILD_TYPE')
|
||
|
"
|
||
|
function! s:FindVarInOpts(opts, variable) abort
|
||
|
if len(a:opts) > 0
|
||
|
" Search the list of command-line options for an entry matching
|
||
|
" '-D <variable>=<value>' or '-D <variable>:<type>=<value>' or
|
||
|
" '-D<variable>=<value>' or '-D<variable>:<type>=<value>'.
|
||
|
let l:opt = matchstr(a:opts, '\m\C-D\s*' . a:variable)
|
||
|
" If found, return the value, otherwise return an empty string.
|
||
|
if len(l:opt) > 0
|
||
|
return split(l:opt, '=')[1]
|
||
|
else
|
||
|
return ''
|
||
|
endif
|
||
|
endif
|
||
|
endfunction
|
||
|
|
||
|
" Process build configuration.
|
||
|
"
|
||
|
" Params:
|
||
|
" opts : List
|
||
|
" list of options
|
||
|
"
|
||
|
function! s:ProcessBuildConfig(opts) abort
|
||
|
let l:config = s:buildsys.current_config
|
||
|
" Check if the first entry of the list of command-line options starts with a
|
||
|
" letter (and not with a dash), in which case the user will have passed the
|
||
|
" name of the build configuration as the first option.
|
||
|
if (len(a:opts) > 0) && (match(a:opts[0], '\m\C^\w') >= 0)
|
||
|
" Update build config name and remove from list of options.
|
||
|
let l:config = a:opts[0]
|
||
|
call s:SetCurrentConfig(l:config)
|
||
|
call remove(a:opts, 0)
|
||
|
endif
|
||
|
" If the list of command-line options does not contain an explicit value for
|
||
|
" the 'CMAKE_BUILD_TYPE' variable, add it.
|
||
|
if s:FindVarInOpts(a:opts, 'CMAKE_BUILD_TYPE') ==# ''
|
||
|
call add(a:opts, '-D CMAKE_BUILD_TYPE=' . l:config)
|
||
|
endif
|
||
|
endfunction
|
||
|
|
||
|
" Get list of command-line options from string of arguments.
|
||
|
"
|
||
|
" Params:
|
||
|
" argstring : String
|
||
|
" string containing command-line arguments
|
||
|
"
|
||
|
" Returns:
|
||
|
" List
|
||
|
" list of unprocessed command-line options
|
||
|
"
|
||
|
" Example:
|
||
|
" an argument string like the following
|
||
|
" 'Debug -D VAR_A=1 -DVAR_B=0 -Wdev -U VAR_C'
|
||
|
" results in a list of options like the following
|
||
|
" ['Debug', '-D VAR_A=1', '-DVAR_B=0', '-Wdev', '-U VAR_C']
|
||
|
"
|
||
|
function! s:ArgStringToOptList(argstring) abort
|
||
|
let l:opts = []
|
||
|
for l:arg in split(a:argstring)
|
||
|
" If list of options is empty, append first argument.
|
||
|
if len(l:opts) == 0
|
||
|
call add(l:opts, l:arg)
|
||
|
" If argument starts with a dash, append it to the list of options.
|
||
|
elseif match(l:arg, '\m\C^-') >= 0
|
||
|
call add(l:opts, l:arg)
|
||
|
" If argument does not start with a dash, it must belong to the last
|
||
|
" option that was added to the list, thus extend that option.
|
||
|
else
|
||
|
let l:opts[-1] = join([l:opts[-1], l:arg])
|
||
|
endif
|
||
|
endfor
|
||
|
return l:opts
|
||
|
endfunction
|
||
|
|
||
|
" Process string of arguments and return parsed options.
|
||
|
"
|
||
|
" Params:
|
||
|
" argstring : String
|
||
|
" string containing command-line arguments
|
||
|
"
|
||
|
" Returns:
|
||
|
" Dictionary
|
||
|
" opts : List
|
||
|
" list of options
|
||
|
" source_dir : String
|
||
|
" path to source directory
|
||
|
" build_dir : String
|
||
|
" path to build directory
|
||
|
"
|
||
|
function! s:ProcessArgString(argstring) abort
|
||
|
let l:opts = s:ArgStringToOptList(a:argstring)
|
||
|
call s:ProcessBuildConfig(l:opts)
|
||
|
" If compile commands are to be exported, and the
|
||
|
" 'CMAKE_EXPORT_COMPILE_COMMANDS' variable is not set, set it.
|
||
|
if g:cmake_link_compile_commands
|
||
|
if s:FindVarInOpts(l:opts, 'CMAKE_EXPORT_COMPILE_COMMANDS') ==# ''
|
||
|
call add(l:opts, '-D CMAKE_EXPORT_COMPILE_COMMANDS=ON')
|
||
|
endif
|
||
|
endif
|
||
|
" Set source and build directories. Must be done after processing the build
|
||
|
" configuration so that the current build configuration is up to date before
|
||
|
" setting the build directory.
|
||
|
let l:source_dir = s:system.Path([s:buildsys.project_root], v:true)
|
||
|
let l:build_dir = s:system.Path([s:buildsys.path_to_current_config], v:true)
|
||
|
" Return dictionary of options.
|
||
|
let l:optdict = {}
|
||
|
let l:optdict.opts = l:opts
|
||
|
let l:optdict.source_dir = l:source_dir
|
||
|
let l:optdict.build_dir = l:build_dir
|
||
|
return l:optdict
|
||
|
endfunction
|
||
|
|
||
|
" Refresh list of build configuration directories.
|
||
|
"
|
||
|
function! s:RefreshConfigs() abort
|
||
|
" List of directories inside of which a CMakeCache file is found.
|
||
|
let l:cache_dirs = findfile(
|
||
|
\ 'CMakeCache.txt',
|
||
|
\ s:GetBuildDirLocation() . '/**1',
|
||
|
\ -1)
|
||
|
" Transform paths to just names of directories. These will be the names of
|
||
|
" existing configuration directories.
|
||
|
call map(l:cache_dirs, {_, val -> fnamemodify(val, ':h:t')})
|
||
|
let s:buildsys.configs = l:cache_dirs
|
||
|
call s:logger.LogDebug('Build configs: %s', s:buildsys.configs)
|
||
|
endfunction
|
||
|
|
||
|
" Callback for RefreshTargets().
|
||
|
"
|
||
|
function! s:RefreshTargetsCb(...) abort
|
||
|
let l:data = s:system.ExtractStdoutCallbackData(a:000)
|
||
|
for l:line in l:data
|
||
|
if match(l:line, '\m\C\.\.\.\s') == 0
|
||
|
let l:target = split(l:line)[1]
|
||
|
let s:buildsys.targets += [l:target]
|
||
|
endif
|
||
|
endfor
|
||
|
endfunction
|
||
|
|
||
|
" Refresh list of available CMake targets.
|
||
|
"
|
||
|
function! s:RefreshTargets() abort
|
||
|
let s:buildsys.targets = []
|
||
|
let l:build_dir = s:buildsys.path_to_current_config
|
||
|
let l:command = [g:cmake_command,
|
||
|
\ '--build', l:build_dir,
|
||
|
\ '--target', 'help'
|
||
|
\ ]
|
||
|
call s:system.JobRun(
|
||
|
\ l:command, v:true, function('s:RefreshTargetsCb'), v:null, v:false)
|
||
|
endfunction
|
||
|
|
||
|
" Check if build configuration directory exists.
|
||
|
"
|
||
|
" Params:
|
||
|
" config : String
|
||
|
" configuration to check
|
||
|
"
|
||
|
" Returns:
|
||
|
" Boolean
|
||
|
" v:true if the build configuration exists, v:false otherwise
|
||
|
"
|
||
|
function! s:ConfigExists(config) abort
|
||
|
return index(s:buildsys.configs, a:config) >= 0
|
||
|
endfunction
|
||
|
|
||
|
" Set current build configuration.
|
||
|
"
|
||
|
" Params:
|
||
|
" config : String
|
||
|
" build configuration name
|
||
|
"
|
||
|
function! s:SetCurrentConfig(config) abort
|
||
|
let s:buildsys.current_config = a:config
|
||
|
let l:path = s:system.Path([s:GetBuildDirLocation(), a:config], v:false)
|
||
|
let s:buildsys.path_to_current_config = l:path
|
||
|
call s:logger.LogInfo('Current config: %s (%s)',
|
||
|
\ s:buildsys.current_config,
|
||
|
\ s:buildsys.path_to_current_config
|
||
|
\ )
|
||
|
call s:statusline.SetBuildInfo(s:buildsys.current_config)
|
||
|
endfunction
|
||
|
|
||
|
" Link compile commands from source directory to build directory.
|
||
|
"
|
||
|
function! s:LinkCompileCommands() abort
|
||
|
if !g:cmake_link_compile_commands
|
||
|
return
|
||
|
endif
|
||
|
let l:target = s:system.Path(
|
||
|
\ [s:buildsys.path_to_current_config, 'compile_commands.json'],
|
||
|
\ v:true
|
||
|
\ )
|
||
|
let l:link = s:system.Path(
|
||
|
\ [s:buildsys.project_root, 'compile_commands.json'],
|
||
|
\ v:true,
|
||
|
\ )
|
||
|
let l:command = [g:cmake_command, '-E', 'create_symlink', l:target, l:link]
|
||
|
call s:system.JobRun(l:command, v:true, v:null, v:null, v:false)
|
||
|
endfunction
|
||
|
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
" Public functions
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
|
||
|
" Generate a buildsystem for the project using CMake.
|
||
|
"
|
||
|
" Params:
|
||
|
" clean : Boolean
|
||
|
" whether to clean before generating
|
||
|
" argstring : String
|
||
|
" build configuration and additional CMake options
|
||
|
"
|
||
|
function! s:buildsys.Generate(clean, argstring) abort
|
||
|
call s:logger.LogDebug('Invoked: buildsys.Generate(%s, %s)',
|
||
|
\ a:clean, string(a:argstring))
|
||
|
let l:command = [g:cmake_command]
|
||
|
let l:optdict = s:ProcessArgString(a:argstring)
|
||
|
" Construct command.
|
||
|
call extend(l:command, g:cmake_generate_options)
|
||
|
call extend(l:command, l:optdict.opts)
|
||
|
if l:self.cmake_version < 313
|
||
|
call add(l:command, '-H' . l:optdict.source_dir)
|
||
|
call add(l:command, '-B' . l:optdict.build_dir)
|
||
|
else
|
||
|
call add(l:command, '-S ' . l:optdict.source_dir)
|
||
|
call add(l:command, '-B ' . l:optdict.build_dir)
|
||
|
endif
|
||
|
" Clean project buildsystem, if requested.
|
||
|
if a:clean
|
||
|
call l:self.Clean()
|
||
|
endif
|
||
|
" Run generate command.
|
||
|
call s:terminal.Run(
|
||
|
\ l:command, 'generate',
|
||
|
\ [
|
||
|
\ function('s:RefreshConfigs'),
|
||
|
\ function('s:RefreshTargets'),
|
||
|
\ function('s:LinkCompileCommands')
|
||
|
\ ],
|
||
|
\ [function('s:RefreshConfigs')],
|
||
|
\ [], []
|
||
|
\ )
|
||
|
endfunction
|
||
|
|
||
|
" Clean buildsystem.
|
||
|
"
|
||
|
function! s:buildsys.Clean() abort
|
||
|
call s:logger.LogDebug('Invoked: buildsys.Clean()')
|
||
|
if isdirectory(l:self.path_to_current_config)
|
||
|
call delete(l:self.path_to_current_config, 'rf')
|
||
|
endif
|
||
|
call s:RefreshConfigs()
|
||
|
endfunction
|
||
|
|
||
|
" Set current build configuration after checking that the configuration exists.
|
||
|
"
|
||
|
" Params:
|
||
|
" config : String
|
||
|
" build configuration name
|
||
|
"
|
||
|
function! s:buildsys.Switch(config) abort
|
||
|
call s:logger.LogDebug('Invoked: buildsys.Switch(%s)', a:config)
|
||
|
" Check that config exists.
|
||
|
if !s:ConfigExists(a:config)
|
||
|
call s:logger.EchoError(
|
||
|
\ "Build configuration '%s' not found, run ':CMakeGenerate %s'",
|
||
|
\ a:config, a:config)
|
||
|
call s:logger.LogError(
|
||
|
\ "Build configuration '%s' not found, run ':CMakeGenerate %s'",
|
||
|
\ a:config, a:config)
|
||
|
return
|
||
|
endif
|
||
|
call s:SetCurrentConfig(a:config)
|
||
|
call s:LinkCompileCommands()
|
||
|
endfunction
|
||
|
|
||
|
" Get list of configuration directories (containing a buildsystem).
|
||
|
"
|
||
|
" Returns:
|
||
|
" List
|
||
|
" list of existing configuration directories
|
||
|
"
|
||
|
function! s:buildsys.GetConfigs() abort
|
||
|
return l:self.configs
|
||
|
endfunction
|
||
|
|
||
|
" Get list of available build targets.
|
||
|
"
|
||
|
" Returns:
|
||
|
" List
|
||
|
" list of available build targets
|
||
|
"
|
||
|
function! s:buildsys.GetTargets() abort
|
||
|
if len(l:self.targets) == 0
|
||
|
call s:RefreshTargets()
|
||
|
endif
|
||
|
return l:self.targets
|
||
|
endfunction
|
||
|
|
||
|
" Get current build configuration.
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" build configuration
|
||
|
"
|
||
|
function! s:buildsys.GetCurrentConfig() abort
|
||
|
return l:self.current_config
|
||
|
endfunction
|
||
|
|
||
|
" Get path to CMake source directory of current project.
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" path to CMake source directory
|
||
|
"
|
||
|
function! s:buildsys.GetSourceDir() abort
|
||
|
return l:self.project_root
|
||
|
endfunction
|
||
|
|
||
|
" Get path to current build configuration.
|
||
|
"
|
||
|
" Returns:
|
||
|
" String
|
||
|
" path to build configuration
|
||
|
"
|
||
|
function! s:buildsys.GetPathToCurrentConfig() abort
|
||
|
return l:self.path_to_current_config
|
||
|
endfunction
|
||
|
|
||
|
" Get buildsys 'object'.
|
||
|
"
|
||
|
function! cmake#buildsys#Get() abort
|
||
|
return s:buildsys
|
||
|
endfunction
|
||
|
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
" Initialization
|
||
|
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||
|
|
||
|
function! s:GetCMakeVersionCb(...) abort
|
||
|
let l:data = s:system.ExtractStdoutCallbackData(a:000)
|
||
|
for l:line in l:data
|
||
|
if match(l:line, '\m\C^cmake\S* version') == 0
|
||
|
let l:version_str = split(split(l:line)[2], '\.')
|
||
|
let l:major = str2nr(l:version_str[0])
|
||
|
let l:minor = str2nr(l:version_str[1])
|
||
|
let s:buildsys.cmake_version = l:major * 100 + l:minor
|
||
|
break
|
||
|
endif
|
||
|
endfor
|
||
|
endfunction
|
||
|
|
||
|
" Get CMake version. The version is stored as MAJOR * 100 + MINOR (e.g., version
|
||
|
" 3.13.3 would result in 313).
|
||
|
let s:command = [g:cmake_command, '--version']
|
||
|
call s:system.JobRun(
|
||
|
\ s:command, v:true, function('s:GetCMakeVersionCb'), v:null, v:false)
|
||
|
|
||
|
" Must be done before any other initial configuration.
|
||
|
let s:buildsys.project_root = s:system.Path([s:FindProjectRoot()], v:false)
|
||
|
call s:logger.LogInfo('Project root: %s', s:buildsys.project_root)
|
||
|
|
||
|
call s:SetCurrentConfig(g:cmake_default_config)
|
||
|
call s:RefreshConfigs()
|