" ============================================================================== " 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=', 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 =' or '-D :=' or " '-D=' or '-D:='. 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()