diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..28134c7 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,23 @@ +environment: + # The package name + PACKAGE: CMakeBuilder + SUBLIME_TEXT_VERSION : "3" + +install: + - ps: appveyor DownloadFile "https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/appveyor.ps1" + - ps: .\appveyor.ps1 "bootstrap" -verbose + # install Package Control + - ps: .\appveyor.ps1 "install_package_control" -verbose + +build: off + +test_script: + + # run tests with test coverage report + - ps: .\appveyor.ps1 "run_tests" -coverage -verbose + +after_test: + - "SET PYTHON=C:\Python33" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - pip install codecov + - codecov diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..bc31973 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = /*/tests/* \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9041b72 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# Stuff not being exported to ZIP and therefore +# prevented from being packed into LSP.sublime-package + +## git +.github/ export-ignore +gh-pages/ export-ignore +*.git export-ignore +*.gitignore export-ignore +*.gitattributes export-ignore + +## unit testing +tests/ export-ignore +unittesting.json export-ignore + +## other configs +stubs/ export-ignore +.travis.yml export-ignore +.appveyor.yml export-ignore +.coveragerc export-ignore +*.ini export-ignore +*.cfg export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee7b732 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.coverage +.vagrant +*.pyc +tests/*build* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..35ad896 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +env: + global: + - PACKAGE=CMakeBuilder # Package name + - SUBLIME_TEXT_VERSION="3" + # use UNITTESTING_TAG to specific tag of UnitTesting + # - UNITTESTING_TAG="master" + +# mutliple os matrix +# https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages) +matrix: + include: + - os: linux + language: python + python: 3.3 + - os: osx + language: generic + + +before_install: + - curl -OL https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/travis.sh + # enable gui, see https://docs.travis-ci.com/user/gui-and-headless-browsers + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then + export DISPLAY=:99.0; + sh -e /etc/init.d/xvfb start; + fi + +install: + # bootstrap the testing environment + - sh travis.sh bootstrap + # install Package Control and package denepdencies + - sh travis.sh install_package_control + +script: + # run tests with test coverage report + - sh travis.sh run_tests --coverage + # testing syntax_test files + # - sh travis.sh run_syntax_tests + +after_success: + # remove the following if `coveralls` is not needed + - if [ "$TRAVIS_OS_NAME" == "osx" ]; then + brew update; + brew install python3; + pip3 install python-coveralls; + pip3 install codecov; + fi + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then + pip install python-coveralls; + pip install codecov; + fi + - coveralls + - codecov + +notifications: + email: false diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build new file mode 100644 index 0000000..f36ea9b --- /dev/null +++ b/CMakeBuilder.sublime-build @@ -0,0 +1,10 @@ +{ + "target": "cmake_build", + "selector": "source.cmake | source.c | source.c++", + "variants": + { + "name": "Select & Build Target", + "selector": "source.cmake | source.c | source.c++", + "select_target": true + } +} diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index 4b7ba6b..5b9790c 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -1,6 +1,6 @@ [ { - "command": "cmake_open_build_folder", + "command": "cmake_open_build_folder", "caption": "CMakeBuilder: Browse Build Folder…" }, { @@ -8,7 +8,7 @@ "caption": "CMakeBuilder: Configure" }, { - "command": "cmake_write_build_targets", + "command": "cmake_write_build_targets", "caption": "CMakeBuilder: Write Build Targets to Sublime Project File" }, { @@ -30,5 +30,37 @@ { "command": "cmake_diagnose", "caption": "CMakeBuilder: Diagnose (What Should I Do?)" + }, + { + "command": "cmake_set_target", + "caption": "CMakeBuilder: Set Target" + }, + { + "command": "cmake_reveal_include_directories", + "caption": "CMakeBuilder: Reveal Include Directories" + }, + { + "command": "cmake_dump_file_system_watchers", + "caption": "CMakeBuilder: Dump File System Watchers" + }, + { + "command": "cmake_dump_inputs", + "caption": "CMakeBuilder: Dump CMake Inputs" + }, + { + "command": "cmake_set_global_setting", + "caption": "CMakeBuilder: Set Global Setting" + }, + { + "command": "cmake_show_configure_output", + "caption": "CMakeBuilder: Show Configure Output" + }, + { + "command": "cmake_switch_scheme", + "caption": "CMakeBuilder: Switch Scheme" + }, + { + "command": "cmake_restart_server", + "caption": "CMakeBuilder: Restart Server For This Project" } ] diff --git a/CMakeBuilder.sublime-settings b/CMakeBuilder.sublime-settings index 7ae99a9..9fde24c 100644 --- a/CMakeBuilder.sublime-settings +++ b/CMakeBuilder.sublime-settings @@ -1,23 +1,44 @@ { - // These are the default settings. They are located in - // + // These are the default settings. They are located in + // // (Installed) Packages/CMakeBuilder/CMakeBuilder.sublime-settings // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // + // // You should not edit this file, as it gets overwritten after every update. // Instead, if you want to override the default settings, create a new file // in Packages/User/CMakeBuilder.sublime-settings, and copy and paste over // from this file. Then change what you want. - // + // // If you came here from - // + // // Preferences -> Package Settings -> CMakeBuilder -> Settings, // // then Sublime Text has already opened a "user" file for you to the right // of this view in which you may override settings. - + //========================================================================== - + + // If there's a compile_commands.json file generated with + // CMAKE_EXPORT_COMPILE_COMMANDS, do we want to copy it over to the source + // directory? This is useful for, for instance, clangd. + // See: https://clang.llvm.org/extra/clangd.html + // See: https://clang.llvm.org/docs/JSONCompilationDatabase.html + // See: https://cmake.org/cmake/help/v3.5/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html + "copy_compile_commands_to_project_path": false, + + // Wether to auto-update "ecc_flags_sources" upon a succesful configure + // to point to the compilation database. + "auto_update_EasyClangComplete_compile_commands_location": false, + + // Wether to auto-update the "compile_commands" key upon a succesful + // configure to point to the compilation database. + "auto_update_compile_commands_project_setting": false, + + // Set this to true to always open an output panel when the server starts + // to configure the project. If false, the output panel will only display + // when an error occurs. + "server_configure_verbose": false, + // If the command "CMakeBuilder: Configure" exited with status 0, should we // write/update build targets in the sublime project file immediately? "write_build_targets_after_successful_configure": true, @@ -35,7 +56,7 @@ // the "Configure" command will run. "configure_on_save": true, - // The command line arguments that are passed to CTest when you run the + // The command line arguments that are passed to CTest when you run the // command "CMakeBuilder: Run CTest". "ctest_command_line_args": "--output-on-failure", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 964efb2..0507a14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,10 @@ Thank you for showing interest in contributing to CMakeBuilder. # Style Guide -The style is loosely based on [PEP8][1]. +Use the LSP plugin (search for it on Package Control) together with the +python-language-server when you make changes to this plugin. You can install +python-language-server with pip3. When you're done with the changes, format +the document. Please trim whitespace and make sure views end with an empty line. # Things That Need Attention More platform-specific generators are very welcome. See the folder "Generators". - -[1]: https://www.python.org/dev/peps/pep-0008/ diff --git a/Main.sublime-menu b/Main.sublime-menu index 6b168bd..bda97f4 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -5,7 +5,7 @@ "id": "tools", "children": [ - { + { "caption": "CMakeBuilder", "id": "build", "mnemonic": "C", @@ -18,8 +18,8 @@ "mnemonic": "D" }, { - "command": "cmake_configure", - "mnemonic": "C" + "command": "cmake_configure", + "mnemonic": "C" }, { "command": "cmake_write_build_targets", @@ -33,7 +33,7 @@ "command": "cmake_edit_cache" }, { - "command": "cmake_clear_cache" + "command": "cmake_clear_cache" }, { "command": "cmake_run_ctest", @@ -42,6 +42,32 @@ { "command": "cmake_new_project", "mnemonic": "N" + }, + { + "command": "cmake_set_target", + "mnemonic": "T" + }, + { + "command": "cmake_reveal_include_directories", + "mnemonic": "I" + }, + { + "command": "cmake_dump_file_system_watchers", + }, + { + "command": "cmake_dump_inputs" + }, + { + "command": "cmake_set_global_setting" + }, + { + "command": "cmake_show_configure_output" + }, + { + "command": "cmake_switch_scheme" + }, + { + "command": "cmake_restart_server" } ] } @@ -50,17 +76,17 @@ { "caption": "Preferences", "id": "preferences", - "children": + "children": [ { "caption": "Package Settings", "id": "package-settings", - "children": + "children": [ { "caption": "CMakeBuilder", "id": "cmakebuilder", - "children": + "children": [ { "command": "open_url", diff --git a/README.md b/README.md index 850b447..f4d9b57 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,149 @@ Run the command and look for CMakeBuilder. +# WANTED: Testers for Version 2.0 + +Version 2.0 has major differences, because we'll utilize the cmake server for +version 2. It is currently in an alpha state. You can make Package Control +download the alpha version by opening the User settings of Package Control and +adding the following JSON list at the top-level JSON dictionary: + +```javascript + "install_prereleases": + [ + "CMakeBuilder + ], +``` + +The alpha version should work out-of-the-box for OSX and Linux, but is not +recommended for Windows users at this point. I would appreciate it if you report +any issues that you may find. + +# Major Changes Between V1 and V2 + +Version 2 of CMakeBuilder has cmake-server functionality. There are some major +changes in how you specify your cmake dict in your project settings. **Please +read the documentation for version 2.0**. + +## Version 2.0 + +Version 2.0 has server functionality. You need at least CMake 3.7 for this. +What follows is the documentation for version 2.0.0 and higher. + +## TL;DR + +1. Open a `.sublime-project`. + +2. Add this to the project file in your `"settings"`: + + ```javascript + "cmake": + { + "schemes": + [ + { + "name": "Debug", + // this assumes your project file is at the root of your project + // folder. If it's not, try using ${folder} instead of ${project_path} + "build_folder": "${project_path}/build" + } + ] + } + ``` + +3. Save your project, and *open it if you haven't already*. So, from the + command-line you would do `subl path/to/projectfile.sublime-project` to open + your project. + +4. You will be presented with a quick-panel that asks you to select the CMake + generator. Choose one. + +5. CMakeBuilder will start configuring your CMake project. Look at Sublime's + status bar to see its progress going from 0% to 100%. + +6. Once the project is configured, select the CMakeBuilder build system in + Tools -> Build Systems -> CMakeBuilder. + +7. Press CTRL+B to build. + +### The CMake Dictionary Version 2.0.0 and Higher + +By "CMake dictionary" we mean the JSON dictionary that you define in your +`"settings"` of your sublime project file with key `"cmake"`. The CMake +dictionary accepts the following keys: + +* `schemes` [required] + + A JSON-list of JSON-dictionaries that define your possible *schemes*. + A *scheme* is our way to organize Debug/Release/MinSizeRel builds etc. + +Each *scheme* is required to be a JSON-dictionary. It's possible keys are: + +* `name` [required] + + A string that represents this scheme. For instance, "Debug" or "Release". + +* `build_folder` [required] + + A string pointing to the directory where you want to build the project. A + good first choice is `${project_path}/build`. + +* `command_line_overrides` [optional] + + A dictionary where each value is either a string or a boolean. The key-value + pairs are passed to the CMake invocation when you run `cmake_configure` as + `-D` options. For example, if you have the key-value pair `"MY_VAR": "BLOB"` + in the dictionary, the CMake invocation will contain `-DMY_VAR=BLOB`. Boolean + values are converted to `ON` or `OFF`. For instance, if you have the key-value + pair `"BUILD_SHARED_LIBS": true`in the dictionary, the CMake invocation will + contain `-DBUILD_SHARED_LIBS=ON`. + +## Example Project File for Version 2.0.0 and Higher + +Here is an example Sublime project to get you started. + +```javascript +{ + "folders": + [ + { + "path": "." + } + ], + "settings": + { + "cmake": + { + "schemes": + [ + { + "build_folder": "${project_path}/build/debug", + "command_line_overrides": + { + "BUILD_SHARED_LIBS": true, + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": true + } + } + ] + } + } +} + +``` + +## Version 1.0.1 and Lower + +Version 1.0.1 and lower do not have server functionality. What follows is the +documentation for version 1.0.1 and lower. + ## TL;DR 1. Open a `.sublime-project`. 2. Add this to the project file in your `"settings"`: - ```json + ```javascript "cmake": { "build_folder": "${project_path}/build" @@ -28,7 +164,7 @@ and look for CMakeBuilder. from the command palette. 4. Check out your new build system in your `.sublime-project`. If no new build - system was created, you can also run the command "CMakeBuilder: Write Build + system was created, you can also run the command "CMakeBuilder: Write Build Targets to Sublime Project File" from the command palette. 5. Press CTRL + B or + B. @@ -38,10 +174,10 @@ and look for CMakeBuilder. ## Reference -### The CMake Dictionary +### The CMake Dictionary Version 1.0.1 and Lower -By "CMake dictionary" we mean the JSON dictionary that you define in your -`"settings"` of your sublime project file with key `"cmake"`. The CMake +By "CMake dictionary" we mean the JSON dictionary that you define in your +`"settings"` of your sublime project file with key `"cmake"`. The CMake dictionary accepts the following keys: * `build_folder` [required] @@ -68,7 +204,7 @@ dictionary accepts the following keys: * `generator` [optional] - A JSON string specifying the CMake generator. + A JSON string specifying the CMake generator. * Available generators for osx: "Ninja" and "Unix Makefiles". @@ -80,7 +216,7 @@ dictionary accepts the following keys: If no generator is specified on osx, "Unix Makefiles" is the default generator. For "Ninja", you must have ninja installed. Install it with apt. - * Available generators for windows: "Ninja", "NMake Makefiles" and + * Available generators for windows: "Ninja", "NMake Makefiles" and "Visual Studio". If no generator is specified on windows, "Visual Studio" is the default @@ -89,7 +225,7 @@ dictionary accepts the following keys: for. **Note**: If you find that the output of the NMake generator is garbled with - color escape codes, you can try to use `"CMAKE_COLOR_MAKEFILE": false` in + color escape codes, you can try to use `"CMAKE_COLOR_MAKEFILE": false` in your `command_line_overrides` dictionary. * `root_folder` [optional] @@ -100,8 +236,8 @@ dictionary accepts the following keys: * `env` [optional] - This is a dict of key-value pairs of strings. Place your environment - variables at configure time in here. For example, to select clang as + This is a dict of key-value pairs of strings. Place your environment + variables at configure time in here. For example, to select clang as your compiler if you have gcc set as default, you can use "env": { "CC": "clang", "CXX": "clang++" } @@ -138,11 +274,11 @@ Any key may be overridden by a platform-specific override. The platform keys are one of `"linux"`, `"osx"` or `"windows"`. For an example on how this works, see below. -## Example Project File +## Example Project File for Version 1.0.1 and Lower Here is an example Sublime project to get you started. -```json +```javascript { "folders": [ @@ -204,7 +340,7 @@ menu at the top of the window. * `configure_on_save` : JSON bool - If true, will run the `cmake_configure` command whenever you save a + If true, will run the `cmake_configure` command whenever you save a CMakeLists.txt file or CMakeCache.txt file. * `write_build_targets_after_successful_configure` : JSON bool @@ -215,7 +351,7 @@ menu at the top of the window. * `silence_developer_warnings` : JSON bool - If true, will add the option `-Wno-dev` to the CMake invocation of the + If true, will add the option `-Wno-dev` to the CMake invocation of the `cmake_configure` command. * `always_clear_cache_before_configure` : JSON bool @@ -225,7 +361,7 @@ menu at the top of the window. * `ctest_command_line_args` : JSON string - Command line arguments passed to the CTest invocation when you run + Command line arguments passed to the CTest invocation when you run `cmake_run_ctest`. * `generated_name_for_build_system` : JSON string diff --git a/Syntax/Ninja.sublime-syntax b/Syntax/Ninja.sublime-syntax index 93067f1..5e5f9fb 100644 --- a/Syntax/Ninja.sublime-syntax +++ b/Syntax/Ninja.sublime-syntax @@ -5,89 +5,93 @@ scope: output.build.ninja contexts: main: - - match: ^\[(?=\d) + - match: ^(\[)(\d+)(/)(\d+)(\]) + captures: + 0: meta.block.progress.ninja + 1: punctuation.section.brackets.begin.ninja + 2: constant.numeric.integer.decimal.ninja + 3: punctuation.separator.ninja + 4: constant.numeric.integer.decimal.ninja + 5: punctuation.section.brackets.end.ninja push: expect-cmake-info-line - match: (FAILED)(\:)((?:[^\\/]*\\|/)*)(.*) captures: - 1: invalid.illegal.compilation.failed + 1: invalid.illegal.compilation.failed.ninja 2: punctuation.separator.ninja - 3: string.unquoted.filepath - 4: string.unquoted.filepath - - match: "In file included from .*" - scope: markup.quote + 3: string.unquoted.filepath.ninja + 4: string.unquoted.filepath.ninja + - match: 'In file included from (.*)' + captures: + 0: comment.line.ninja + 1: markup.italic.ninja - match: ((?:[^\\/]*\\|/)*)(.*)(:)(\d+)(:)(\d+) captures: - 2: string.unquoted.filepath - 3: punctuation.separator.compiler - 4: constant.numeric.integer - 5: punctuation.separator.compiler - 6: constant.numeric.integer + 2: string.unquoted.filepath.ninja + 3: punctuation.separator.compiler.ninja + 4: constant.numeric.integer.ninja + 5: punctuation.separator.compiler.ninja + 6: constant.numeric.integer.ninja push: expect-compiler-message - match: \^ - scope: punctuation.indicator.clang + scope: punctuation.definition.arrow.clang.ninja push: - match: $ pop: true - match: '~+' - scope: punctuation.indicator.clang + scope: punctuation.definition.tilde.clang.ninja expect-cmake-info-line: - - meta_scope: meta.block.progress.cmake - - match: \] - set: - - meta_content_scope: meta.block.info.cmake - - match: $ - pop: true # back to main context - - match: (Building CX?X? object) (.*) - captures: - 1: keyword.operator.cmake - 2: string.unquoted.filepath - - match: "(Linking CX?X?(?: shared| static)? (?:library|executable)) (.*)" - captures: - 1: keyword.control.cmake - 2: string.unquoted.filepath - - match: \d+ - scope: constant.numeric.integer + - meta_content_scope: meta.block.info.cmake.ninja + - match: $ + pop: true # back to main context + - match: (Building CX?X? object) (.*) + captures: + 1: keyword.operator.cmake.ninja + 2: string.unquoted.filepath.cmake.ninja + - match: "(Linking CX?X?(?: shared| static)? (?:library|executable)) (.*)" + captures: + 1: keyword.control.cmake.ninja + 2: string.unquoted.filepath.cmake.ninja expect-compiler-message: - meta_scope: meta.block.compiler.diagnostic - match: ':' - scope: punctuation.separator + scope: punctuation.separator.ninja set: - - meta_content_scope: meta.block.compiler.diagnostic + - meta_content_scope: meta.block.compiler.diagnostic.ninja - match: \s*(warning) captures: - 1: markup.changed + 1: markup.changed.ninja set: - - meta_content_scope: meta.block.compiler.diagnostic + - meta_content_scope: meta.block.compiler.diagnostic.ninja - match: ':' - scope: punctuation.separator + scope: punctuation.separator.ninja set: - - meta_content_scope: meta.block.compiler.diagnostic + - meta_content_scope: meta.block.compiler.diagnostic.ninja - match: (.+) - scope: meta.block.compiler.diagnostic markup.changed + scope: meta.block.compiler.diagnostic.ninja markup.changed.ninja pop: true - match: \s*(error) captures: - 1: markup.deleted + 1: markup.deleted.ninja set: - - meta_content_scope: meta.block.compiler.diagnostic + - meta_content_scope: meta.block.compiler.diagnostic.ninja - match: ':' - scope: punctuation.separator + scope: punctuation.separator.ninja set: - match: (.+) - scope: markup.deleted + scope: markup.deleted.ninja pop: true - match: \s*(note) captures: - 1: markup.quote + 1: markup.quote.ninja set: - - meta_content_scope: meta.block.compiler.diagnostic + - meta_content_scope: meta.block.compiler.diagnostic.ninja - match: ':' - scope: punctuation.separator + scope: punctuation.separator.ninja set: - match: (.+) - scope: markup.quote + scope: markup.quote.ninja pop: true - match: . pop: true diff --git a/__init__.py b/__init__.py index 7a16d39..292b9d9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,4 @@ -__version__ = "1.0.0" -__version_info__ = (1,0,0) +__version__ = "2.0.0" +__version_info__ = (2, 0, 0) from CMakeBuilder.commands import * -from CMakeBuilder.event_listeners import * diff --git a/commands/__init__.py b/commands/__init__.py index d9bd733..b3971bb 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,21 +1,20 @@ +from .build import CmakeBuildCommand, CmakeExecCommand from .clear_cache import CmakeClearCacheCommand from .configure import CmakeConfigureCommand +from .configure2 import CmakeConfigure2Command from .diagnose import CmakeDiagnoseCommand +from .dump_file_system_watchers import CmakeDumpFileSystemWatchersCommand +from .dump_inputs import CmakeDumpInputsCommand from .edit_cache import CmakeEditCacheCommand from .insert_diagnosis import CmakeInsertDiagnosisCommand from .new_project import CmakeNewProjectCommand from .open_build_folder import CmakeOpenBuildFolderCommand +from .reveal_include_directories import CmakeRevealIncludeDirectories from .run_ctest import CmakeRunCtestCommand +from .set_global_setting import CmakeSetGlobalSettingCommand +from .set_target import CmakeSetTargetCommand from .write_build_targets import CmakeWriteBuildTargetsCommand - -__all__ = [ - 'CmakeClearCacheCommand' - , 'CmakeConfigureCommand' - , 'CmakeDiagnoseCommand' - , 'CmakeEditCacheCommand' - , 'CmakeInsertDiagnosisCommand' - , 'CmakeNewProjectCommand' - , 'CmakeOpenBuildFolderCommand' - , 'CmakeRunCtestCommand' - , 'CmakeWriteBuildTargetsCommand' -] +from .show_configure_output import CmakeShowConfigureOutputCommand +from .switch_scheme import CmakeSwitchSchemeCommand +from .command import ServerManager +from .command import CmakeRestartServerCommand diff --git a/commands/build.py b/commands/build.py new file mode 100644 index 0000000..7edc42b --- /dev/null +++ b/commands/build.py @@ -0,0 +1,137 @@ +import sublime +import Default.exec +import os +from .command import CmakeCommand, ServerManager +from ..support import capabilities + + +class CmakeExecCommand(Default.exec.ExecCommand): + + def run(self, window_id, **kwargs): + self.server = ServerManager.get(sublime.Window(window_id)) + if not self.server: + sublime.error_message("Unable to locate server!") + return + self.server.is_building = True + super().run(**kwargs) + + def on_finished(self, proc): + super().on_finished(proc) + self.server.is_building = False + + +class CmakeBuildCommand(CmakeCommand): + + def run(self, select_target=False): + if not capabilities("serverMode"): + sublime.error_message("You need CMake 3.7 or higher. It's " + "possible that you selected the 'CMake' " + "build system in the Tools menu. This build " + "system is only available when CMakeBuilder " + "is running in 'Server' mode. Server mode " + "was added to CMake in version 3.7. If you " + "want to use CMakeBuilder, select your " + "build system generated in your project " + "file instead.") + return + if not self.is_enabled(): + sublime.error_message("Cannot build a CMake target!") + return + path = os.path.join(self.server.cmake.build_folder, + "CMakeFiles", + "CMakeBuilder", + "active_target.txt") + if os.path.exists(path): + with open(path, "r") as f: + active_target = f.read() + else: + active_target = None + if select_target or active_target is None: + if not self.server.targets: + sublime.error_message( + "No targets found. Did you configure the project?") + self.items = [ + [t.name, t.type, t.directory] for t in self.server.targets] + self.window.show_quick_panel(self.items, self._on_done) + else: + self._on_done(active_target) + + def _on_done(self, index): + if isinstance(index, str): + self.window.run_command("cmake_set_target", {"name": index}) + target = None + for t in self.server.targets: + if t.name == index: + target = t + break + elif isinstance(index, int): + if index == -1: + return + target = self.server.targets[index] + self.window.run_command("cmake_set_target", {"index": index}) + else: + sublime.error_message("Unknown type: " + type(index)) + return + if target.type == "RUN": + self._handle_run_target(target) + else: + self.window.run_command( + "cmake_exec", + { + "window_id": self.window.id(), + "cmd": target.cmd(), + "file_regex": self.server.cmake.file_regex, + "syntax": self.server.cmake.syntax, + "working_dir": self.server.cmake.build_folder + } + ) + + def _handle_run_target(self, target): + if sublime.platform() in ("linux", "osx"): + prefix = "./" + else: + prefix = "" + cmd = None + for t in self.server.targets: + if t.name == target.name[len("Run: "):]: + cmd = t.cmd() + break + if not cmd: + sublime.error_message("Failed to find corresponding build " + 'target for "run" target ' + + target.name) + return + try: + if sublime.platform() in ("linux", "osx"): + cmd = ["/bin/bash", + "-l", + "-c", + "{} && cd {} && {}".format(" ".join(cmd), + t.directory, + prefix + t.fullname)] + elif sublime.platform() == "windows": + raise ImportError + else: + raise ImportError + self._handle_run_target_terminal_view_route(cmd) + except ImportError: + self.window.run_command( + "cmake_exec", + { + "window_id": self.window.id(), + "cmd": cmd, + "working_dir": target.directory + } + ) + except Exception as e: + sublime.error_message("Unknown exception: " + str(e)) + raise e + + def _handle_run_target_terminal_view_route(self, cmd): + import TerminalView # will throw if not present + assert TerminalView + self.window.run_command( + "terminal_view_exec", { + "cmd": cmd, + "working_dir": self.server.cmake.build_folder + }) diff --git a/commands/clear_cache.py b/commands/clear_cache.py index bf7fd7c..a9a890b 100644 --- a/commands/clear_cache.py +++ b/commands/clear_cache.py @@ -20,12 +20,13 @@ def is_enabled(self): return False return True - def description(self): + @classmethod + def description(cls): return 'Clear Cache' def run(self, with_confirmation=True): build_folder = sublime.expand_variables( - self.window.project_data()["settings"]["cmake"]["build_folder"], + self.window.project_data()["settings"]["cmake"]["build_folder"], self.window.extract_variables()) files_to_remove = [] dirs_to_remove = [] @@ -50,21 +51,21 @@ def append_file_to_remove(relative_name): panel = self.window.create_output_panel('files_to_be_deleted') - self.window.run_command('show_panel', + self.window.run_command('show_panel', {'panel': 'output.files_to_be_deleted'}) - panel.run_command('insert', + panel.run_command('insert', {'characters': 'Files to remove:\n' + '\n'.join(files_to_remove + dirs_to_remove)}) def on_done(selected): if selected != 0: return self.remove(files_to_remove, dirs_to_remove) - panel.run_command('append', + panel.run_command('append', {'characters': '\nCleared CMake cache files!', 'scroll_to_end': True}) - self.window.show_quick_panel(['Do it', 'Cancel'], on_done, + self.window.show_quick_panel(['Do it', 'Cancel'], on_done, sublime.KEEP_OPEN_ON_FOCUS_LOST) def remove(self, files_to_remove, dirs_to_remove): @@ -79,4 +80,4 @@ def remove(self, files_to_remove, dirs_to_remove): except Exception as e: sublime.error_message('Cannot remove '+directory) - + diff --git a/commands/command.py b/commands/command.py new file mode 100644 index 0000000..c34a31c --- /dev/null +++ b/commands/command.py @@ -0,0 +1,377 @@ +import sublime_plugin +import sublime +import os +import pickle +from ..support.server import Server +from ..support import capabilities +from ..support import get_setting + + +def _configure(window): + try: + cmake = window.project_data()["settings"]["cmake"] + build_folder = cmake["build_folder"] + build_folder = sublime.expand_variables( + build_folder, window.extract_variables()) + if os.path.exists(build_folder): + window.run_command("cmake_configure") + except Exception: + pass + + +class CmakeCommand(sublime_plugin.WindowCommand): + + def is_enabled(self): + self.server = ServerManager.get(self.window) + return (self.server is not None and + super(CmakeCommand, self).is_enabled()) + + +class CmakeRestartServerCommand(CmakeCommand): + + def run(self): + try: + window_id = self.window.id() + ServerManager._servers.pop(window_id, None) + self.window.focus_view(self.window.active_view()) + except Exception as e: + sublime.errror_message(str(e)) + + @classmethod + def description(cls): + return "Restart Server For This Project" + + +class CMakeSettings(object): + + __slots__ = ("source_folder", "build_folder", "build_folder_pre_expansion", + "generator", "toolset", "platform", "command_line_overrides", + "file_regex", "syntax") + + """docstring for CMakeSettings""" + def __init__(self): + super(CMakeSettings, self).__init__() + self.source_folder = "" + self.build_folder = "" + self.build_folder_pre_expansion = "" + self.generator = "" + self.toolset = "" + self.platform = "" + self.command_line_overrides = {} + self.file_regex = "" + self.syntax = "" + + def cmd(self, target): + return target.cmd() + + +class ServerManager(sublime_plugin.EventListener): + """Manages the bijection between cmake-enabled projects and server + objects.""" + + _servers = {} + + @classmethod + def get(cls, window): + return cls._servers.get(window.id(), None) + + def __init__(self): + self.__class__._is_selecting = False + self.__class__.generator = "" + self.__class__.source_folder = "" + self.__class__.build_folder = "" + self.__class__.toolset = "" + self.__class__.platform = "" + + @classmethod + def on_load(cls, view): + if not capabilities("serverMode"): + print("CMakeBuilder: cmake is not capable of server mode") + return + if cls._is_selecting: + # User is busy entering stuff + return + if not capabilities("serverMode"): + print("CMakeBuilder: cmake is not capable of server mode") + return + # Check if there's a server running for this window. + cls.window = view.window() + if not cls.window: + return + server = cls.get(cls.window) + if server: + return + + # No server running. Check if there are build settings. + data = cls.window.project_data() + if not data: + return + settings = data.get("settings", None) + if not settings or not isinstance(settings, dict): + return + cmake = settings.get("cmake", None) + if not cmake or not isinstance(cmake, dict): + return + cls.schemes = cmake.get("schemes", None) + if (not cls.schemes or + not isinstance(cls.schemes, list) or + len(cls.schemes) == 0): + return + + # At this point we found schemes. Let's check if there's a + # CMakeLists.txt file to be found somewhere up the directory tree. + if not view.file_name(): + return + cmake_file = os.path.dirname(view.file_name()) + cmake_file = os.path.join(cmake_file, "CMakeLists.txt") + while not os.path.isfile(cmake_file): + cmake_file = os.path.dirname(os.path.dirname(cmake_file)) + if os.path.dirname(cmake_file) == cmake_file: + # We're at the root of the filesystem. + cmake_file = None + break + cmake_file = os.path.join(cmake_file, "CMakeLists.txt") + if not cmake_file: + # Not a cmake project + return + # We found a CMakeLists.txt file, but we might be embedded into a + # larger project. Find the true root file. + while True: + old_cmake_file = cmake_file + cmake_file = cmake_file = os.path.dirname( + os.path.dirname(cmake_file)) + cmake_file = os.path.join(cmake_file, "CMakeLists.txt") + while not os.path.isfile(cmake_file): + cmake_file = os.path.dirname(os.path.dirname(cmake_file)) + if os.path.dirname(cmake_file) == cmake_file: + # We're at the root of the filesystem. + cmake_file = None + break + cmake_file = os.path.join(cmake_file, "CMakeLists.txt") + if not cmake_file: + # We found the actual root of the project earlier. + cmake_file = old_cmake_file + break + cls.source_folder = os.path.dirname(cmake_file) + print("found source folder:", cls.source_folder) + + # At this point we have a bunch of schemes and we have a source folder. + cls.items = [] + for scheme in cls.schemes: + if not isinstance(scheme, dict): + sublime.error_message("Please make sure all of your schemes " + "are JSON dictionaries.") + cls.items.append(["INVALID SCHEME", ""]) + continue + name = scheme.get("name", "Untitled Scheme") + build_folder = scheme.get("build_folder", "${project_path}/build") + variables = cls.window.extract_variables() + build_folder = sublime.expand_variables(build_folder, variables) + cls.items.append([name, build_folder]) + if len(cls.schemes) == 0: + print("found schemes dict, but it is empty") + return + cls._is_selecting = True + if len(cls.schemes) == 1: + # Select the only scheme possible. + cls._on_done_select_scheme(0) + else: + # Ask the user what he/she wants. + cls.window.show_quick_panel(cls.items, + cls._on_done_select_scheme) + + @classmethod + def _on_done_select_scheme(cls, index): + if index == -1: + cls._is_selecting = False + return + cls.name = cls.items[index][0] + if cls.name == "INVALID SCHEME": + cls._is_selecting = False + return + cls.build_folder_pre_expansion = cls.schemes[index]["build_folder"] + cls.build_folder = sublime.expand_variables( + cls.build_folder_pre_expansion, cls.window.extract_variables()) + cls.command_line_overrides = cls.schemes[index].get( + "command_line_overrides", {}) + cls._select_generator() + + @classmethod + def _select_generator(cls): + if cls.generator: + cls._select_toolset() + return + cls.items = [] + for g in capabilities("generators"): + platform_support = bool(g["platformSupport"]) + toolset_support = bool(g["toolsetSupport"]) + platform_support = "Platform support: {}".format(platform_support) + toolset_support = "Toolset support: {}".format(toolset_support) + cls.items.append([g["name"], platform_support, toolset_support]) + if len(cls.items) == 1: + cls._on_done_select_generator(0) + else: + cls.window.show_quick_panel(cls.items, + cls._on_done_select_generator) + + @classmethod + def _on_done_select_generator(cls, index): + if index == -1: + cls._is_selecting = False + return + cls.generator = cls.items[index][0] + platform_support = cls.items[index][1] + toolset_support = cls.items[index][2] + cls.platform_support = True if "True" in platform_support else False + cls.toolset_support = True if "True" in toolset_support else False + print("CMakeBuilder: Selected generator is", cls.generator) + if cls.platform_support: + text = "Platform for {} (Press Enter for default): ".format( + cls.generator) + print("CMakeBuilder: Presenting input panel for platform.") + cls.window.show_input_panel(text, "", + cls._on_done_select_platform, + None, None) + elif cls.toolset_support: + cls._select_toolset() + else: + cls._run_configure_with_new_settings() + + @classmethod + def _select_toolset(cls): + if cls.toolset: + print("CMakeBuilder: toolset already present:", cls.toolset) + return + print("CMakeBuilder: Presenting input panel for toolset.") + text = "Toolset for {}: (Press Enter for default): ".format( + cls.generator) + cls.window.show_input_panel(text, "", cls._on_done_select_toolset, + None, None) + + @classmethod + def _on_done_select_platform(cls, platform): + cls.platform = platform + print("CMakeBuilder: Selected platform is", cls.platform) + if cls.toolset_support: + cls._select_toolset() + else: + cls._run_configure_with_new_settings() + + @classmethod + def _on_done_select_toolset(cls, toolset): + cls.toolset = toolset + print("CMakeBuilder: Selected toolset is", cls.toolset) + cls._run_configure_with_new_settings() + + @classmethod + def _run_configure_with_new_settings(cls): + cls._is_selecting = False + cmake_settings = CMakeSettings() + cmake_settings.source_folder = cls.source_folder + cmake_settings.build_folder = cls.build_folder + + cmake_settings.build_folder_pre_expansion = \ + cls.build_folder_pre_expansion + + cmake_settings.generator = cls.generator + cmake_settings.platform = cls.platform + cmake_settings.toolset = cls.toolset + cmake_settings.command_line_overrides = cls.command_line_overrides + + if sublime.platform() in ("osx", "linux"): + cmake_settings.file_regex = \ + r'(.+[^:]):(\d+):(\d+): (?:fatal )?((?:error|warning): .+)$' + if "Makefile" in cls.generator: + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Make.sublime-syntax" + elif "Ninja" in cls.generator: + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Ninja.sublime-syntax" + else: + print("CMakeBuilder: Warning: Generator", cls.generator, + "will not have syntax highlighting in the output panel.") + elif sublime.platform() == "windows": + if "Ninja" in cls.generator: + cmake_settings.file_regex = r'^(.+)\((\d+)\):() (.+)$' + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Ninja+CL.sublime-syntax" + elif "Visual Studio" in cls.generator: + cmake_settings.file_regex = \ + (r'^ (.+)\((\d+)\)(): ((?:fatal )?(?:error|warning) ', + r'\w+\d\d\d\d: .*) \[.*$') + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Visual_Studio.sublime-syntax" + elif "NMake" in cls.generator: + cmake_settings.file_regex = r'^(.+)\((\d+)\):() (.+)$' + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Make.sublime-syntax" + else: + print("CMakeBuilder: Warning: Generator", cls.generator, + "will not have syntax highlighting in the output panel.") + else: + sublime.error_message("Unknown platform: " + sublime.platform()) + return + path = os.path.join(cls.build_folder, "CMakeFiles", "CMakeBuilder") + os.makedirs(path, exist_ok=True) + path = os.path.join(path, "settings.pickle") + + # Unpickle the settings first, if there are any. + if os.path.isfile(path): + old_settings = pickle.load(open(path, "rb")) + if (old_settings.generator != cmake_settings.generator or + old_settings.platform != cmake_settings.platform or + old_settings.toolset != cmake_settings.toolset): + print("CMakeBuilder: clearing cache for mismatching generator") + try: + os.remove(os.path.join(cmake_settings.build_folder, + "CMakeCache.txt")) + except Exception as e: + sublime.error_message(str(e)) + return + pickle.dump(cmake_settings, open(path, "wb")) + server = Server(cls.window, cmake_settings) + cls.source_folder = "" + cls.build_folder = "" + cls.build_folder_pre_expansion = "" + cls.generator = "" + cls.platform = "" + cls.toolset = "" + cls.items = [] + cls.schemes = [] + cls.command_line_overrides = {} + cls._servers[cls.window.id()] = server + + @classmethod + def on_activated(cls, view): + cls.on_load(view) + try: + server = cls.get(view.window()) + path = os.path.join(server.cmake.build_folder, + "CMakeFiles", + "CMakeBuilder", + "active_target.txt") + with open(path, "r") as f: + active_target = f.read() + view.set_status("cmake_active_target", "TARGET: " + active_target) + except Exception as e: + view.erase_status("cmake_active_target") + + @classmethod + def on_post_save(cls, view): + if not view: + return + if not get_setting(view, "configure_on_save", False): + return + name = view.file_name() + if not name: + return + if name.endswith(".sublime-project"): + server = cls.get(view.window()) + if not server: + _configure(view.window()) + else: + view.window().run_command("cmake_clear_cache", + {"with_confirmation": False}) + view.window().run_command("cmake_restart_server") + elif name.endswith("CMakeLists.txt") or name.endswith("CMakeCache.txt"): + _configure(view.window()) diff --git a/commands/configure.py b/commands/configure.py index a6f8031..66a3428 100644 --- a/commands/configure.py +++ b/commands/configure.py @@ -7,6 +7,7 @@ import copy from CMakeBuilder.support import * from CMakeBuilder.generators import * +from .command import ServerManager class CmakeConfigureCommand(Default.exec.ExecCommand): """Configures a CMake project with options set in the sublime project @@ -18,15 +19,22 @@ def is_enabled(self): return True except Exception as e: return False - + def description(self): return 'Configure' def run(self, write_build_targets=False, silence_dev_warnings=False): + self.server = ServerManager.get(self.window) + if self.server: + self.window.run_command("cmake_configure2") + return if get_setting(self.window.active_view(), 'always_clear_cache_before_configure', False): self.window.run_command('cmake_clear_cache', args={'with_confirmation': False}) project = self.window.project_data() project_file_name = self.window.project_file_name() + if not project_file_name: + # A little more flexible + project_file_name = os.path.abspath(self.window.extract_variables()["folder"]) project_name = os.path.splitext(project_file_name)[0] project_path = os.path.dirname(project_file_name) if not os.path.isfile(os.path.join(project_path, 'CMakeLists.txt')): @@ -81,7 +89,7 @@ def run(self, write_build_targets=False, silence_dev_warnings=False): GeneratorClass = class_from_generator_string(generator) builder = None try: - builder = GeneratorClass(self.window, copy.deepcopy(cmake)) + builder = GeneratorClass(self.window) except KeyError as e: sublime.error_message('Unknown variable in cmake dictionary: {}' .format(str(e))) @@ -103,15 +111,15 @@ def run(self, write_build_targets=False, silence_dev_warnings=False): except ValueError as e: pass self.builder.on_pre_configure() - env = self.builder.env() + env = self.builder.get_env() user_env = get_cmake_value(cmake, 'env') if user_env: env.update(user_env) - super().run(shell_cmd=cmd, + super().run(shell_cmd=cmd, working_dir=root_folder, file_regex=r'CMake\s(?:Error|Warning)(?:\s\(dev\))?\sat\s(.+):(\d+)()\s?\(?(\w*)\)?:', syntax='Packages/CMakeBuilder/Syntax/Configure.sublime-syntax', env=env) - + def on_finished(self, proc): super().on_finished(proc) self.builder.on_post_configure(proc.exit_code()) diff --git a/commands/configure2.py b/commands/configure2.py new file mode 100644 index 0000000..02d46a9 --- /dev/null +++ b/commands/configure2.py @@ -0,0 +1,7 @@ +from .command import CmakeCommand + + +class CmakeConfigure2Command(CmakeCommand): + + def run(self): + self.server.configure(self.server.cmake.command_line_overrides) diff --git a/commands/diagnose.py b/commands/diagnose.py index 5eefe92..e1e3c52 100644 --- a/commands/diagnose.py +++ b/commands/diagnose.py @@ -10,12 +10,13 @@ def run(self): view.settings().set("rulers", []) view.settings().set("gutter", False) view.settings().set("draw_centered", True) - view.settings().set("syntax", + view.settings().set("syntax", "Packages/CMakeBuilder/Syntax/Diagnosis.sublime-syntax") view.set_name("CMakeBuilder Diagnosis") view.run_command("cmake_insert_diagnosis") view.set_read_only(True) sublime.active_window().focus_view(view) - def description(self): + @classmethod + def description(cls): return "Diagnose (Help! What should I do?)" diff --git a/commands/dump_file_system_watchers.py b/commands/dump_file_system_watchers.py new file mode 100644 index 0000000..c3917c3 --- /dev/null +++ b/commands/dump_file_system_watchers.py @@ -0,0 +1,12 @@ +from .command import CmakeCommand + + +class CmakeDumpFileSystemWatchersCommand(CmakeCommand): + """Prints the watched files to a new view""" + + def run(self): + self.server.file_system_watchers() + + @classmethod + def description(cls): + return "Dump File System Watchers" diff --git a/commands/dump_inputs.py b/commands/dump_inputs.py new file mode 100644 index 0000000..edd308a --- /dev/null +++ b/commands/dump_inputs.py @@ -0,0 +1,12 @@ +from .command import CmakeCommand + + +class CmakeDumpInputsCommand(CmakeCommand): + """Prints the cmake inputs to a new view""" + + def run(self): + self.server.cmake_inputs() + + @classmethod + def description(cls): + return "Dump CMake Inputs" diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 32c43e4..77c0f81 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -1,21 +1,40 @@ -import sublime, sublime_plugin, os -from CMakeBuilder.support import * +import sublime +import sublime_plugin +import os +from .command import CmakeCommand +from ..support import capabilities -class CmakeEditCacheCommand(sublime_plugin.WindowCommand): - """Edit an entry from the CMake cache.""" - def is_enabled(self): - try: + +if capabilities("serverMode"): + + + class CmakeEditCacheCommand(CmakeCommand): + + def description(self): + return "Edit Cache..." + + def run(self): + self.server.cache() + +else: + + + class CmakeEditCacheCommand(sublime_plugin.WindowCommand): + + """Edit an entry from the CMake cache.""" + def is_enabled(self): + try: + build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] + build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) + return os.path.exists(os.path.join(build_folder, "CMakeCache.txt")) + except Exception as e: + return False + + def description(self): + return "Edit Cache..." + + def run(self): build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) - return os.path.exists(os.path.join(build_folder, "CMakeCache.txt")) - except Exception as e: - return False - - def description(self): - return 'Edit Cache...' - - def run(self): - build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] - build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) - self.window.open_file(os.path.join(build_folder, "CMakeCache.txt")) - self.window.run_command("show_overlay", args={"overlay": "goto", "text": "@"}) + self.window.open_file(os.path.join(build_folder, "CMakeCache.txt")) + self.window.run_command("show_overlay", args={"overlay": "goto", "text": "@"}) diff --git a/commands/insert_diagnosis.py b/commands/insert_diagnosis.py index 6436d4f..e6e6288 100644 --- a/commands/insert_diagnosis.py +++ b/commands/insert_diagnosis.py @@ -1,6 +1,10 @@ -import sublime, sublime_plugin, subprocess, os, shutil, sys, json +import sublime +import sublime_plugin +import os +import shutil from tabulate import tabulate # dependencies.json from CMakeBuilder.support import check_output +from CMakeBuilder.support import capabilities class CmakeInsertDiagnosisCommand(sublime_plugin.TextCommand): @@ -8,7 +12,10 @@ class CmakeInsertDiagnosisCommand(sublime_plugin.TextCommand): def run(self, edit): self.error_count = 0 self._diagnose(edit) - self.view.insert(edit, self.view.size(), tabulate(self.table, headers=["CHECK", "VALUE", "SUGGESTION/FIX"], tablefmt="fancy_grid")) + self.view.insert(edit, self.view.size(), tabulate( + self.table, + headers=["CHECK", "VALUE", "SUGGESTION/FIX"], + tablefmt="fancy_grid")) def _command_exists(self, cmd): return shutil.which(cmd) is not None @@ -23,11 +30,11 @@ def _diagnose(self, edit): else: self.table.append(["cmake version", output, ""]) try: - output = json.loads(check_output("cmake -E capabilities")) - server_mode = output.get("serverMode", False) + server_mode = capabilities("serverMode") self.table.append(["server mode", server_mode, ""]) except Exception as e: - self.table.append(["server mode", False, "Have cmake version >= 3.7"]) + self.table.append(["server mode", False, + "Have cmake version >= 3.7"]) project = self.view.window().project_data() project_filename = self.view.window().project_file_name() @@ -35,48 +42,38 @@ def _diagnose(self, edit): if project_filename: self.table.append(["project file", project_filename, ""]) else: - self.table.append(["project file", "NOT FOUND", "Open a .sublime-project"]) + self.table.append(["project file", "NOT FOUND", + "Open a .sublime-project"]) self.error_count += 1 return - # cmake = project.get("cmake", None) - # if cmake: - # self._ERR(edit, "It looks like you have the cmake dictionary at the top level of your project file.") - # self._ERR(edit, "Since version 0.11.0, the cmake dict should be in the settings dict of your project file.") - # self._ERR(edit, "Please edit your project file so that the cmake dict is sitting inside your settings") - # return - cmake = project.get("settings", {}).get("cmake", None) if cmake: - cmake = sublime.expand_variables(cmake, self.view.window().extract_variables()) + cmake = sublime.expand_variables( + cmake, + self.view.window().extract_variables()) buildFolder = cmake['build_folder'] if buildFolder: - self.table.append(["cmake dictionary present in settings", True, ""]) - # self._OK(edit, 'Found CMake build folder "{}"'.format(buildFolder)) - # self._OK(edit, 'You can run the "Configure" command.') + self.table.append(["cmake dictionary present in settings", + True, ""]) cache_file = os.path.join(buildFolder, 'CMakeCache.txt') if os.path.isfile(cache_file): - self.table.append(["CMakeCache.txt file present", True, "You may run the Write Build Targets command"]) - # self._OK(edit, 'Found CMakeCache.txt file in "{}"'.format(buildFolder)) - # self._OK(edit, 'You can run the command "Write Build Targets to Sublime Project File"') - # self._OK(edit, 'If you already populated your project file with build targets, you can build your project with Sublime\'s build system. Go to Tools -> Build System and make sure your build system is selected.') + self.table.append([ + "CMakeCache.txt file present", True, + "You may run the Write Build Targets command"]) else: - self.table.append(["CMakeCache.txt file present", False, "Run the Configure command"]) + self.table.append(["CMakeCache.txt file present", False, + "Run the Configure command"]) self.error_count += 1 - # self._ERR(edit, 'No CMakeCache.txt file found in "{}"'.format(buildFolder)) - # self._ERR(edit, 'You should run the "Configure" command.') return else: - self.table.append(["build_folder present in cmake dictionary", False, "Write a build_folder key"]) + self.table.append(["build_folder present in cmake dictionary", + False, "Write a build_folder key"]) self.error_count += 1 - # self._ERR(edit, 'No build_folder present in cmake dictionary of "{}".'.format(project_filename)) - # self._ERR(edit, 'You should write a key-value pair in the "cmake" dictionary') - # self._ERR(edit, 'where the key is equal to "build_folder" and the value is the') - # self._ERR(edit, 'directory where you want to build your project.') - # self._ERR(edit, 'See the instructions at github.com/rwols/CMakeBuilder') else: - self.table.append(["cmake dictionary present in settings", False, "Create a cmake dictionary in your settings"]) + self.table.append(["cmake dictionary present in settings", False, + "Create a cmake dictionary in your settings"]) return def _printLine(self, edit, str): diff --git a/commands/new_project.py b/commands/new_project.py index 615f1e0..edde421 100644 --- a/commands/new_project.py +++ b/commands/new_project.py @@ -49,7 +49,8 @@ class CmakeNewProjectCommand(sublime_plugin.WindowCommand): """Creates a new template project and opens the project for you.""" - def description(self): + @classmethod + def description(cls): return "New Project..." def run(self): diff --git a/commands/open_build_folder.py b/commands/open_build_folder.py index 9fc3d71..14b9b11 100644 --- a/commands/open_build_folder.py +++ b/commands/open_build_folder.py @@ -1,21 +1,43 @@ -import sublime, sublime_plugin, os -from CMakeBuilder.support import * +import sublime +import sublime_plugin +import os +from .command import CmakeCommand +from ..support import capabilities -class CmakeOpenBuildFolderCommand(sublime_plugin.WindowCommand): - """Opens the build folder.""" - def is_enabled(self): - try: - build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] - build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) - return os.path.exists(build_folder) - except Exception as e: - return False +if capabilities("serverMode"): + + + class CmakeOpenBuildFolderCommand(CmakeCommand): + """Opens the build folder.""" + + @classmethod + def description(cls): + return "Browse Build Folder..." + + def run(self): + build_folder = self.server.cmake.build_folder + self.window.run_command("open_dir", args={"dir": os.path.realpath(build_folder)}) - def description(self): - return 'Browse Build Folder…' +else: - def run(self): - build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] - build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) - self.window.run_command('open_dir', args={'dir': os.path.realpath(build_folder)}) + + class CmakeOpenBuildFolderCommand(sublime_plugin.WindowCommand): + """Opens the build folder.""" + + @classmethod + def description(cls): + return "Browse Build Folder..." + + def is_enabled(self): + try: + build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] + build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) + return os.path.exists(build_folder) + except Exception as e: + return False + + def run(self): + build_folder = self.window.project_data()["settings"]["cmake"]["build_folder"] + build_folder = sublime.expand_variables(build_folder, self.window.extract_variables()) + self.window.run_command('open_dir', args={'dir': os.path.realpath(build_folder)}) diff --git a/commands/reveal_include_directories.py b/commands/reveal_include_directories.py new file mode 100644 index 0000000..a6e9c4f --- /dev/null +++ b/commands/reveal_include_directories.py @@ -0,0 +1,16 @@ +from .command import CmakeCommand + + +class CmakeRevealIncludeDirectories(CmakeCommand): + """Prints the include directories to a new view""" + + def run(self): + view = self.window.new_file() + view.set_name("Project Include Directories") + view.set_scratch(True) + for path in self.server.include_paths: + view.run_command("append", {"characters": path + "\n", "force": True}) + + @classmethod + def description(cls): + return "Reveal Include Directories" diff --git a/commands/run_ctest.py b/commands/run_ctest.py index 2343742..15e1b54 100644 --- a/commands/run_ctest.py +++ b/commands/run_ctest.py @@ -12,7 +12,8 @@ def is_enabled(self): except Exception as e: return False - def description(self): + @classmethod + def description(cls): return 'Run CTest' def run(self, test_framework='boost'): @@ -24,11 +25,11 @@ def run(self, test_framework='boost'): cmd += ' ' + command_line_args #TODO: check out google test style errors, right now I just assume # everybody uses boost unit test framework - super().run(shell_cmd=cmd, + super().run(shell_cmd=cmd, # Guaranteed to exist at this point. - working_dir=cmake.get('build_folder'), + working_dir=cmake.get('build_folder'), file_regex=r'(.+[^:]):(\d+):() (?:fatal )?((?:error|warning): .+)$', syntax='Packages/CMakeBuilder/Syntax/CTest.sublime-syntax') - + def on_finished(self, proc): super().on_finished(proc) diff --git a/commands/set_global_setting.py b/commands/set_global_setting.py new file mode 100644 index 0000000..4b16e46 --- /dev/null +++ b/commands/set_global_setting.py @@ -0,0 +1,11 @@ +from .command import CmakeCommand + + +class CmakeSetGlobalSettingCommand(CmakeCommand): + + def run(self): + self.server.global_settings() + + @classmethod + def description(cls): + return "Set Global Setting..." diff --git a/commands/set_target.py b/commands/set_target.py new file mode 100644 index 0000000..ead5fd2 --- /dev/null +++ b/commands/set_target.py @@ -0,0 +1,47 @@ +import os +import sublime +from .command import CmakeCommand + + +class CmakeSetTargetCommand(CmakeCommand): + + def run(self, index=None, name=None): + if self.server.is_configuring: + sublime.error_message("CMake is configuring, please wait.") + return + if not self.server.targets: + sublime.error_message("No targets found! " + "Did you configure the project?") + return + if name is not None: + self._on_done(name) + elif not index: + self.items = [ + [t.name, t.type, t.directory] for t in self.server.targets] + self.window.show_quick_panel(self.items, self._on_done) + else: + self._on_done(index) + + def _on_done(self, index): + if isinstance(index, str): + self._write_to_file(index) + elif index == -1: + self.window.active_view().erase_status("cmake_active_target") + else: + name = self.server.targets[index].name + self._write_to_file(name) + + def _write_to_file(self, name): + folder = os.path.join(self.server.cmake.build_folder, + "CMakeFiles", + "CMakeBuilder") + path = os.path.join(folder, "active_target.txt") + os.makedirs(folder, exist_ok=True) + with open(path, "w") as f: + f.write(name) + self.window.active_view() \ + .set_status("cmake_active_target", "TARGET: " + name) + + @classmethod + def description(cls): + return "Set Target..." diff --git a/commands/show_configure_output.py b/commands/show_configure_output.py new file mode 100644 index 0000000..76fec92 --- /dev/null +++ b/commands/show_configure_output.py @@ -0,0 +1,12 @@ +from .command import CmakeCommand + + +class CmakeShowConfigureOutputCommand(CmakeCommand): + + def run(self): + self.window.run_command("show_panel", + {"panel": "output.cmake.configure"}) + + @classmethod + def description(cls): + return "Show Configure Output" diff --git a/commands/switch_scheme.py b/commands/switch_scheme.py new file mode 100644 index 0000000..aa6ce31 --- /dev/null +++ b/commands/switch_scheme.py @@ -0,0 +1,12 @@ +from .command import CmakeCommand, ServerManager + + +class CmakeSwitchSchemeCommand(CmakeCommand): + + def run(self): + ServerManager._servers.pop(self.window.id(), None) + ServerManager.on_activated(self.window.active_view()) + + @classmethod + def description(cls): + return "Switch Scheme" diff --git a/commands/write_build_targets.py b/commands/write_build_targets.py index a67dd13..900348a 100644 --- a/commands/write_build_targets.py +++ b/commands/write_build_targets.py @@ -40,7 +40,7 @@ def run(self, open_project_file=False): GeneratorClass = class_from_generator_string(generator) try: assert cmake - builder = GeneratorClass(self.window, copy.deepcopy(cmake)) + builder = GeneratorClass(self.window) except KeyError as e: sublime.error_message('Unknown variable in cmake dictionary: {}' .format(str(e))) @@ -70,4 +70,4 @@ def run(self, open_project_file=False): self.window.open_file(self.window.project_file_name()) except Exception as e: sublime.error_message('An error occured during assigment of the sublime build system: %s' % str(e)) - + raise e diff --git a/event_listeners/__init__.py b/event_listeners/__init__.py deleted file mode 100644 index 7d77bb9..0000000 --- a/event_listeners/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .configure_on_save import ConfigureOnSave - -__all__ = ['ConfigureOnSave'] diff --git a/event_listeners/configure_on_save.py b/event_listeners/configure_on_save.py deleted file mode 100644 index 0a75352..0000000 --- a/event_listeners/configure_on_save.py +++ /dev/null @@ -1,24 +0,0 @@ -import sublime -import sublime_plugin -import functools -from CMakeBuilder.support import get_setting -from CMakeBuilder.commands import CmakeConfigureCommand - -def _configure(window): - if not CmakeConfigureCommand(window).is_enabled(): return - window.run_command("cmake_configure") - -class ConfigureOnSave(sublime_plugin.EventListener): - - def on_post_save(self, view): - if not view: - return - if not get_setting(view, "configure_on_save", False): - return - name = view.file_name() - if not name: - return - if (name.endswith("CMakeLists.txt") or - name.endswith("CMakeCache.txt") or - name.endswith(".sublime-project")): - _configure(view.window()) diff --git a/generators/__init__.py b/generators/__init__.py index 44c5379..8070bd4 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -2,35 +2,71 @@ import sys import os import glob -from CMakeBuilder.support import * +from CMakeBuilder.support import get_setting + class CMakeGenerator(object): - def __init__(self, window, cmake): + + @classmethod + def create(cls, window): + """Factory method to create a new CMakeGenerator object from a sublime + Window object.""" + data = window.project_data()["settings"]["cmake"] + generator_str = data.get("generator", None) + if not generator_str: + if sublime.platform() in ("linux", "osx"): + generator_str = "Unix Makefiles" + elif sublime.platform() == "windows": + generator_str = "Visual Studio" + else: + raise AttributeError( + "unknown sublime platform: %s" % sublime.platform()) + GeneratorClass = class_from_generator_string(generator_str) + return GeneratorClass(window) + + def __init__(self, window): super(CMakeGenerator, self).__init__() + data = window.project_data()["settings"]["cmake"] + self.build_folder_pre_expansion = data["build_folder"] + data = sublime.expand_variables(data, window.extract_variables()) + self.build_folder = self._pop(data, "build_folder") + if not self.build_folder: + raise KeyError('missing required key "build_folder"') + self.build_folder = os.path.abspath(self.build_folder)\ + .replace("\\", "/") + pfn = window.project_file_name() + if not pfn: + self.source_folder = window.extract_variables()["folder"] + else: + self.source_folder = os.path.dirname(pfn) + while os.path.isfile( + os.path.join(self.source_folder, "..", "CMakeLists.txt")): + self.source_folder = os.path.join(self.source_folder, "..") + self.source_folder = os.path.abspath(self.source_folder) + self.source_folder = self.source_folder.replace("\\", "/") + self.command_line_overrides = self._pop( + data, "command_line_overrides", {}) + self.filter_targets = self._pop(data, "filter_targets", []) + self.configurations = self._pop(data, "configurations", []) + self.env = self._pop(data, "env", {}) + self.target_architecture = self._pop( + data, "target_architecture", "x86") + self.visual_studio_versions = self._pop( + data, "visual_studio_versions", [15, 14]) self.window = window - self.cmake = cmake - try: - self.cmake_platform = self.cmake[sublime.platform()] - except Exception as e: - self.cmake_platform = None - self.build_folder_pre_expansion = self.get_cmake_key('build_folder') - assert self.build_folder_pre_expansion - self.cmake = sublime.expand_variables(self.cmake, self.window.extract_variables()) - self.build_folder = self.get_cmake_key('build_folder') - self.filter_targets = self.get_cmake_key('filter_targets') - self.command_line_overrides = self.get_cmake_key('command_line_overrides') - self.target_architecture = self.get_cmake_key('target_architecture') - self.visual_studio_versions = self.get_cmake_key('visual_studio_versions') assert self.build_folder + def _pop(self, data, key, default=None): + return data.get(key, default) + def __repr__(self): return repr(type(self)) - def env(self): - return {} # Empty dict + def get_env(self): + return {} # Empty dict def variants(self): - return [] # Empty list + return [] # Empty list def on_pre_configure(self): pass @@ -47,11 +83,13 @@ def create_sublime_build_system(self): sublime.error_message('Could not get the active view!') name = get_setting(view, 'generated_name_for_build_system') if not name: - sublime.error_message('Could not find the key "generated_name_for_build_system" in the settings!') + sublime.error_message('Could not find the key ' + '"generated_name_for_build_system"' + ' in the settings!') name = sublime.expand_variables(name, self.window.extract_variables()) build_system = { 'name': name, - 'shell_cmd': self.shell_cmd(), + 'shell_cmd': self.shell_cmd(), 'working_dir': self.build_folder_pre_expansion, 'variants': self.variants() } @@ -61,7 +99,7 @@ def create_sublime_build_system(self): syntax = self.syntax() if syntax: build_system['syntax'] = syntax - env = self.env() + env = self.get_env() if env: build_system['env'] = env return build_system @@ -69,6 +107,12 @@ def create_sublime_build_system(self): def shell_cmd(self): return 'cmake --build .' + def cmd(self, target=None): + result = ["cmake", "--build", "."] + if target: + result.extend(["--target", target.name]) + return result + def syntax(self): return None @@ -84,15 +128,19 @@ def get_cmake_key(self, key): else: return None + def get_generator_module_prefix(): return 'CMakeBuilder.generators.' + sublime.platform() + '.' + def get_module_name(generator): return get_generator_module_prefix() + generator.replace(' ', '_') + def is_valid_generator(generator): return get_module_name(generator) in sys.modules + def get_valid_generators(): module_prefix = get_generator_module_prefix() valid_generators = [] @@ -101,46 +149,59 @@ def get_valid_generators(): valid_generators.append(key[len(module_prefix):].replace('_', ' ')) return valid_generators + def class_from_generator_string(generator_string): if not generator_string: - if sublime.platform() == 'linux': + if sublime.platform() == 'linux': generator_string = 'Unix Makefiles' elif sublime.platform() == 'osx': generator_string = 'Unix Makefiles' elif sublime.platform() == 'windows': generator_string = 'Visual Studio' else: - sublime.error_message('Unknown sublime platform: {}'.format(sublime.platform())) + sublime.error_message('Unknown sublime platform: {}' + .format(sublime.platform())) return module_name = get_module_name(generator_string) - if not module_name in sys.modules: + if module_name not in sys.modules: valid_generators = get_valid_generators() - sublime.error_message('CMakeBuilder: "%s" is not a valid generator. The valid generators for this platform are: %s' % (generator_string, ', '.join(valid_generators))) + sublime.error_message('CMakeBuilder: "%s" is not a valid generator. ' + 'The valid generators for this platform are: %s' + % (generator_string, ', '.join(valid_generators)) + ) return GeneratorModule = sys.modules[module_name] GeneratorClass = None try: - GeneratorClass = getattr(GeneratorModule, generator_string.replace(' ', '_')) + GeneratorClass = getattr( + GeneratorModule, generator_string.replace(' ', '_')) except AttributeError as e: sublime.error_message('Internal error: %s' % str(e)) return GeneratorClass + def _get_pyfiles_from_dir(dir): for file in glob.iglob(dir + '/*.py'): - if not os.path.isfile(file): continue + if not os.path.isfile(file): + continue base = os.path.basename(file) - if base.startswith('__'): continue + if base.startswith('__'): + continue generator = base[:-3] yield generator + def _import_all_platform_specific_generators(): path = os.path.join(os.path.dirname(__file__), sublime.platform()) return list(_get_pyfiles_from_dir(path)) + def import_user_generators(): - path = os.path.join(sublime.packages_path(), 'User', 'generators', sublime.platform()) + path = os.path.join( + sublime.packages_path(), 'User', 'generators', sublime.platform()) return list(_get_pyfiles_from_dir(path)) + if sublime.platform() == 'linux': from .linux import * elif sublime.platform() == 'osx': @@ -149,4 +210,3 @@ def import_user_generators(): from .windows import * else: sublime.error_message('Unknown platform: %s' % sublime.platform()) - diff --git a/generators/linux/Ninja.py b/generators/linux/Ninja.py index 7a3dd47..888a08c 100644 --- a/generators/linux/Ninja.py +++ b/generators/linux/Ninja.py @@ -1,6 +1,8 @@ from CMakeBuilder.generators import CMakeGenerator import subprocess import sublime +import os + class Ninja(CMakeGenerator): @@ -14,14 +16,10 @@ def syntax(self): return 'Packages/CMakeBuilder/Syntax/Ninja.sublime-syntax' def variants(self): - env = None - if self.window.active_view(): - env = self.window.active_view().settings().get('build_env') - shell_cmd = 'cmake --build . --target help' proc = subprocess.Popen( ['/bin/bash', '-c', shell_cmd], - env=env, + env=self.get_env(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, @@ -29,27 +27,26 @@ def variants(self): outs, errs = proc.communicate() errs = errs.decode('utf-8') if errs: - sublime.error_message(errs) - return + print(errs) # terrible hack lines = outs.decode('utf-8').splitlines() - + EXCLUDES = [ 'are some of the valid targets for this Makefile:', - 'All primary targets available:', + 'All primary targets available:', 'depend', 'all (the default if no target is provided)', - 'help', - 'edit_cache', + 'help', + 'edit_cache', '.ninja'] variants = [] for target in lines: try: - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue target = target.rpartition(':')[0] - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue shell_cmd = 'cmake --build . --target {}'.format(target) variants.append({'name': target, 'shell_cmd': shell_cmd}) diff --git a/generators/linux/Unix_Makefiles.py b/generators/linux/Unix_Makefiles.py index e02f613..764192c 100644 --- a/generators/linux/Unix_Makefiles.py +++ b/generators/linux/Unix_Makefiles.py @@ -3,6 +3,7 @@ import sublime import multiprocessing + class Unix_Makefiles(CMakeGenerator): def __repr__(self): @@ -18,14 +19,10 @@ def shell_cmd(self): return 'make -j{}'.format(str(multiprocessing.cpu_count())) def variants(self): - env = None - if self.window.active_view(): - env = self.window.active_view().settings().get('build_env') - shell_cmd = 'cmake --build . --target help' proc = subprocess.Popen( ['/bin/bash', '-l', '-c', shell_cmd], - env=env, + env=self.get_env(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, @@ -40,25 +37,25 @@ def variants(self): variants = [] EXCLUDES = [ 'are some of the valid targets for this Makefile:', - 'All primary targets available:', + 'All primary targets available:', 'depend', 'all (the default if no target is provided)', - 'help', - 'edit_cache', + 'help', + 'edit_cache', '.ninja'] - + for target in lines: try: - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue target = target[4:] - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue - shell_cmd = 'make -j{} {}'.format(str(multiprocessing.cpu_count()), target) + shell_cmd = 'make -j{} {}'.format( + str(multiprocessing.cpu_count()), target) variants.append({'name': target, 'shell_cmd': shell_cmd}) except Exception as e: sublime.error_message(str(e)) # Continue anyway; we're in a for-loop return variants - diff --git a/generators/linux/__init__.py b/generators/linux/__init__.py index 776a33e..cd2b9ba 100644 --- a/generators/linux/__init__.py +++ b/generators/linux/__init__.py @@ -1 +1,3 @@ +from .Ninja import Ninja +from .Unix_Makefiles import Unix_Makefiles __all__ = ["Ninja", "Unix_Makefiles"] diff --git a/generators/osx/Ninja.py b/generators/osx/Ninja.py index 005ac41..4ee5de0 100644 --- a/generators/osx/Ninja.py +++ b/generators/osx/Ninja.py @@ -2,6 +2,7 @@ import subprocess import sublime + class Ninja(CMakeGenerator): def __repr__(self): @@ -14,14 +15,10 @@ def syntax(self): return 'Packages/CMakeBuilder/Syntax/Ninja.sublime-syntax' def variants(self): - env = None - if self.window.active_view(): - env = self.window.active_view().settings().get('build_env') - shell_cmd = 'cmake --build . --target help' proc = subprocess.Popen( ['/bin/bash', '-l', '-c', shell_cmd], - env=env, + env=self.get_env(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, @@ -29,27 +26,26 @@ def variants(self): outs, errs = proc.communicate() errs = errs.decode('utf-8') if errs: - sublime.error_message(errs) - return + print(errs) # terrible hack lines = outs.decode('utf-8').splitlines() - + EXCLUDES = [ 'are some of the valid targets for this Makefile:', - 'All primary targets available:', + 'All primary targets available:', 'depend', 'all (the default if no target is provided)', - 'help', - 'edit_cache', + 'help', + 'edit_cache', '.ninja'] variants = [] for target in lines: try: - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue target = target.rpartition(':')[0] - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue shell_cmd = 'cmake --build . --target {}'.format(target) variants.append({'name': target, 'shell_cmd': shell_cmd}) @@ -63,5 +59,3 @@ def on_data(self, proc, data): def on_finished(self, proc): pass - - diff --git a/generators/osx/Unix_Makefiles.py b/generators/osx/Unix_Makefiles.py index e02f613..764192c 100644 --- a/generators/osx/Unix_Makefiles.py +++ b/generators/osx/Unix_Makefiles.py @@ -3,6 +3,7 @@ import sublime import multiprocessing + class Unix_Makefiles(CMakeGenerator): def __repr__(self): @@ -18,14 +19,10 @@ def shell_cmd(self): return 'make -j{}'.format(str(multiprocessing.cpu_count())) def variants(self): - env = None - if self.window.active_view(): - env = self.window.active_view().settings().get('build_env') - shell_cmd = 'cmake --build . --target help' proc = subprocess.Popen( ['/bin/bash', '-l', '-c', shell_cmd], - env=env, + env=self.get_env(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, @@ -40,25 +37,25 @@ def variants(self): variants = [] EXCLUDES = [ 'are some of the valid targets for this Makefile:', - 'All primary targets available:', + 'All primary targets available:', 'depend', 'all (the default if no target is provided)', - 'help', - 'edit_cache', + 'help', + 'edit_cache', '.ninja'] - + for target in lines: try: - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue target = target[4:] - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue - shell_cmd = 'make -j{} {}'.format(str(multiprocessing.cpu_count()), target) + shell_cmd = 'make -j{} {}'.format( + str(multiprocessing.cpu_count()), target) variants.append({'name': target, 'shell_cmd': shell_cmd}) except Exception as e: sublime.error_message(str(e)) # Continue anyway; we're in a for-loop return variants - diff --git a/generators/osx/__init__.py b/generators/osx/__init__.py index 776a33e..cd2b9ba 100644 --- a/generators/osx/__init__.py +++ b/generators/osx/__init__.py @@ -1 +1,3 @@ +from .Ninja import Ninja +from .Unix_Makefiles import Unix_Makefiles __all__ = ["Ninja", "Unix_Makefiles"] diff --git a/generators/windows/NMake_Makefiles.py b/generators/windows/NMake_Makefiles.py index 12f3755..0c64fde 100644 --- a/generators/windows/NMake_Makefiles.py +++ b/generators/windows/NMake_Makefiles.py @@ -4,16 +4,17 @@ import sublime import subprocess + class NMake_Makefiles(CMakeGenerator): def __repr__(self): return 'NMake Makefiles' - def env(self): + def get_env(self): if self.visual_studio_versions: vs_versions = self.visual_studio_versions else: - vs_versions = [ 15, 14.1, 14, 13, 12, 11, 10, 9, 8 ] + vs_versions = [15, 14.1, 14, 13, 12, 11, 10, 9, 8] if self.target_architecture: arch = self.target_architecture else: @@ -23,7 +24,8 @@ def env(self): elif sublime.arch() == 'x64': host = 'amd64' else: - sublime.error_message('Unknown Sublime architecture: %s' % sublime.arch()) + sublime.error_message( + 'Unknown Sublime architecture: %s' % sublime.arch()) return if arch != host: arch = host + '_' + arch @@ -52,31 +54,29 @@ def variants(self): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW lines = subprocess.check_output( - 'cmake --build . --target help', - cwd=self.build_folder, + 'cmake --build . --target help', + cwd=self.build_folder, startupinfo=startupinfo).decode('utf-8').splitlines() - variants = [] EXCLUDES = [ 'are some of the valid targets for this Makefile:', - 'All primary targets available:', + 'All primary targets available:', 'depend', 'all (the default if no target is provided)', - 'help', - 'edit_cache', - '.ninja', + 'help', + 'edit_cache', + '.ninja', '.o', '.i', '.s'] - for target in lines: try: - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue target = target[4:] name = target - if (self.filter_targets and - not any(f in name for f in self.filter_targets)): + if (self.filter_targets and + not any(f in name for f in self.filter_targets)): continue shell_cmd = 'cmake --build . --target {}'.format(target) variants.append({'name': name, 'shell_cmd': shell_cmd}) @@ -84,4 +84,3 @@ def variants(self): sublime.error_message(str(e)) # Continue anyway; we're in a for-loop return variants - diff --git a/generators/windows/Ninja.py b/generators/windows/Ninja.py index f0118dc..94d045a 100644 --- a/generators/windows/Ninja.py +++ b/generators/windows/Ninja.py @@ -4,16 +4,17 @@ import sublime import subprocess + class Ninja(CMakeGenerator): def __repr__(self): return 'Ninja' - def env(self): + def get_env(self): if self.visual_studio_versions: vs_versions = self.visual_studio_versions else: - vs_versions = [ 15, 14.1, 14, 13, 12, 11, 10, 9, 8 ] + vs_versions = [15, 14.1, 14, 13, 12, 11, 10, 9, 8] if self.target_architecture: arch = self.target_architecture else: @@ -23,7 +24,8 @@ def env(self): elif sublime.arch() == 'x64': host = 'amd64' else: - sublime.error_message('Unknown Sublime architecture: %s' % sublime.arch()) + sublime.error_message( + 'Unknown Sublime architecture: %s' % sublime.arch()) return if arch != host: arch = host + '_' + arch @@ -44,7 +46,7 @@ def syntax(self): def file_regex(self): return r'^(.+)\((\d+)\):() (.+)$' - + def variants(self): startupinfo = None @@ -52,30 +54,29 @@ def variants(self): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW lines = subprocess.check_output( - 'cmake --build . --target help', - cwd=self.build_folder, + 'cmake --build . --target help', + cwd=self.build_folder, startupinfo=startupinfo).decode('utf-8').splitlines() variants = [] EXCLUDES = [ - 'All primary targets available:', - 'help', - 'edit_cache', + 'All primary targets available:', + 'help', + 'edit_cache', '.ninja'] - for target in lines: try: if len(target) == 0: continue - if any(exclude in target for exclude in EXCLUDES): + if any(exclude in target for exclude in EXCLUDES): continue if target.endswith(': phony'): target = target[:-len(': phony')] elif target.endswith(': CLEAN'): target = target[:-len(': CLEAN')] target = target.strip() - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue shell_cmd = 'cmake --build . --target {}'.format(target) variants.append({'name': target, 'shell_cmd': shell_cmd}) diff --git a/generators/windows/Visual_Studio.py b/generators/windows/Visual_Studio.py index 197f2f1..bf1dadf 100644 --- a/generators/windows/Visual_Studio.py +++ b/generators/windows/Visual_Studio.py @@ -1,10 +1,12 @@ from CMakeBuilder.generators import CMakeGenerator from CMakeBuilder.generators.windows.support.vcvarsall import query_vcvarsall +from CMakeBuilder.support.check_output import check_output import os import re import subprocess import sublime + class Visual_Studio(CMakeGenerator): def __repr__(self): @@ -12,9 +14,7 @@ def __repr__(self): if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - lines = subprocess.check_output( - 'cmake --help', - startupinfo=startupinfo).decode('utf-8').splitlines() + lines = check_output('cmake --help').splitlines() years = {} for line in lines: print(line) @@ -71,13 +71,15 @@ def variants(self): target = file else: target = relative + '/' + file - if (self.filter_targets and - not any(f in target for f in self.filter_targets)): + if (self.filter_targets and + not any(f in target for f in self.filter_targets)): continue if configs: for config in configs: - shell_cmd = 'cmake --build . --target {} --config {}'.format(target, config) - variants.append({'name': target + ' [' + config + ']', + shell_cmd = 'cmake --build . --target {} --config {}'\ + .format(target, config) + variants.append({ + 'name': target + ' [' + config + ']', 'shell_cmd': shell_cmd}) else: shell_cmd = 'cmake --build . --target {}'.format(target) @@ -88,13 +90,14 @@ def syntax(self): return 'Packages/CMakeBuilder/Syntax/Visual_Studio.sublime-syntax' def file_regex(self): - return r'^ (.+)\((\d+)\)(): ((?:fatal )?(?:error|warning) \w+\d\d\d\d: .*) \[.*$' + return (r'^ (.+)\((\d+)\)(): ((?:fatal )?(?:error|warning) ', + r'\w+\d\d\d\d: .*) \[.*$') - def env(self): + def get_env(self): if self.visual_studio_versions: vs_versions = self.visual_studio_versions else: - vs_versions = [ 15, 14.1, 14, 13, 12, 11, 10, 9, 8 ] + vs_versions = [15, 14.1, 14, 13, 12, 11, 10, 9, 8] if self.target_architecture: arch = self.target_architecture else: @@ -104,7 +107,8 @@ def env(self): elif sublime.arch() == 'x64': host = 'amd64' else: - sublime.error_message('Unknown Sublime architecture: %s' % sublime.arch()) + sublime.error_message( + 'Unknown Sublime architecture: %s' % sublime.arch()) return if arch != host: arch = host + '_' + arch diff --git a/generators/windows/__init__.py b/generators/windows/__init__.py index d84832d..e25361b 100644 --- a/generators/windows/__init__.py +++ b/generators/windows/__init__.py @@ -1 +1,4 @@ +from .Ninja import Ninja +from .NMake_Makefiles import NMake_Makefiles +from .Visual_Studio import Visual_Studio __all__ = ["Ninja", "NMake_Makefiles", "Visual_Studio"] diff --git a/messages/2.0.0-alpha.txt b/messages/2.0.0-alpha.txt new file mode 100644 index 0000000..bb7d903 --- /dev/null +++ b/messages/2.0.0-alpha.txt @@ -0,0 +1 @@ +- Add cmake experimental server functionality for cmake >= 3.7 diff --git a/support/__init__.py b/support/__init__.py index f9b743b..54e9340 100644 --- a/support/__init__.py +++ b/support/__init__.py @@ -1,4 +1,13 @@ -from .check_output import * -from .expand_variables import * -from .get_cmake_value import * -from .get_setting import * +from .check_output import check_output +from .expand_variables import expand_variables +from .get_cmake_value import get_cmake_value +from .get_setting import get_setting +from .capabilities import capabilities + + +__all__ = [ + "check_output", + "expand_variables", + "get_cmake_value", + "get_setting", + "capabilities"] diff --git a/support/capabilities.py b/support/capabilities.py new file mode 100644 index 0000000..7644cc4 --- /dev/null +++ b/support/capabilities.py @@ -0,0 +1,14 @@ +from .check_output import check_output +import json + +_capabilities = None + +def capabilities(key): + global _capabilities + if _capabilities is None: + try: + _capabilities = json.loads(check_output("cmake -E capabilities")) + except Exception as e: + print("CMakeBuilder: Error: Could not load cmake's capabilities") + _capabilities = {"error": None} + return _capabilities.get(key, None) diff --git a/support/check_output.py b/support/check_output.py index 0b0d4b9..d1985be 100644 --- a/support/check_output.py +++ b/support/check_output.py @@ -1,4 +1,7 @@ -import sublime, subprocess, os +import sublime +import subprocess +import os + class CheckOutputException(Exception): """Gets raised when there's a non-empty error stream.""" @@ -6,6 +9,7 @@ def __init__(self, errs): super(CheckOutputException, self).__init__() self.errs = errs + def check_output(shell_cmd, env=None, cwd=None): if sublime.platform() == "linux": cmd = ["/bin/bash", "-c", shell_cmd] @@ -15,7 +19,7 @@ def check_output(shell_cmd, env=None, cwd=None): cmd = ["/bin/bash", "-l", "-c", shell_cmd] startupinfo = None shell = False - else: # sublime.platform() == "windows" + else: # sublime.platform() == "windows" cmd = shell_cmd if os.name == "nt": startupinfo = subprocess.STARTUPINFO() diff --git a/support/db/__init__.py b/support/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/db/json.py b/support/db/json.py new file mode 100644 index 0000000..185a206 --- /dev/null +++ b/support/db/json.py @@ -0,0 +1,121 @@ +import json +import os +import re +import shlex + +from ..models import (CompileCommand, CompilationDatabaseInterface) + + +class JSONCompilationDatabase(CompilationDatabaseInterface): + def __init__(self, json_db_path): + self.json_db_path = json_db_path + + @classmethod + def probe_directory(cls, directory): + """Automatically create a CompilationDatabase from build directory.""" + db_path = os.path.join(directory, 'compile_commands.json') + if os.path.exists(db_path): + return cls(db_path) + return super(JSONCompilationDatabase, cls).probe_directory(directory) + + def get_compile_commands(self, filepath): + filepath = os.path.abspath(filepath) + for elem in self._data: + if os.path.abspath( + os.path.join(elem['directory'], elem['file'])) == filepath: + yield self._dict_to_compile_command(elem) + + def get_all_files(self): + for entry in self._data: + yield os.path.normpath( + os.path.join(entry['directory'], entry['file'])) + + def get_all_compile_commands(self): + # PERFORMANCE: I think shlex is inherently slow, + # something performing better may be necessary + return map(self._dict_to_compile_command, self._data) + + @staticmethod + def _dict_to_compile_command(d): + command = d['command'] + if isinstance(command, str): + return CompileCommand(d['directory'], d['file'], shlex.split(command)) + elif isinstance(command, list): + return CompileCommand(d['directory'], d['file'], command) + else: + raise Exception("Unknown type: {}".format(command.__class__)) + + @property + def _data(self): + if not hasattr(self, '__data'): + with open(self.json_db_path) as f: + self.__data = json.load(f) + return self.__data + + +def command_to_json(commands): + cmd_line = '"' + for i, command in enumerate(commands): + if i != 0: + cmd_line += ' ' + has_space = re.search(r"\s", command) is not None + # reader now accepts simple quotes, so we need to support them here too + has_simple_quote = "'" in command + need_quoting = has_space or has_simple_quote + if need_quoting: + cmd_line += r'\"' + cmd_line += command.replace("\\", r'\\\\').replace(r'"', r'\\\"') + if need_quoting: + cmd_line += r'\"' + return cmd_line + '"' + + +def str_to_json(s): + return '"{}"'.format(s.replace("\\", "\\\\").replace('"', r'\"')) + + +def compile_command_to_json(compile_command): + return r'''{{ + "directory": {}, + "command": {}, + "file": {} +}}'''.format( + str_to_json(compile_command.directory), + command_to_json(compile_command.command), + str_to_json(compile_command.file)) + + +class JSONCompileCommandSerializer(object): + def __init__(self, fp): + self.fp = fp + self.__count = 0 + + def __enter__(self): + self.fp.write('[\n') + return self + + def serialize(self, compile_command): + if self.__count != 0: + self.fp.write(',\n\n') + self.fp.write(compile_command_to_json(compile_command)) + self.__count += 1 + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.__count != 0: + self.fp.write('\n') + self.fp.write(']\n') + + +def compile_commands_to_json(compile_commands, fp): + """ + Dump Json. + + Parameters + ---------- + compile_commands : CompileCommand iterable + fp + A file-like object, JSON is written to this element. + """ + with JSONCompileCommandSerializer(fp) as serializer: + for compile_command in compile_commands: + serializer.serialize(compile_command) diff --git a/support/db/memory.py b/support/db/memory.py new file mode 100644 index 0000000..5af0390 --- /dev/null +++ b/support/db/memory.py @@ -0,0 +1,23 @@ +import os + +from ..models import CompilationDatabaseInterface + + +class InMemoryCompilationDatabase(CompilationDatabaseInterface): + def __init__(self, compile_commands=None): + if compile_commands is None: + self.compile_commands = [] + else: + self.compile_commands = compile_commands + + def get_compile_commands(self, filepath): + filepath = os.path.abspath(filepath) + for compile_command in self.compile_commands: + if compile_command.normfile == filepath: + yield compile_command + + def get_all_files(self): + return (c.normfile for c in self.compile_commands) + + def get_all_compile_commands(self): + return iter(self.compile_commands) diff --git a/support/expand_variables.py b/support/expand_variables.py index e480c80..a0d324f 100644 --- a/support/expand_variables.py +++ b/support/expand_variables.py @@ -1,5 +1,6 @@ import string + def expand_variables(the_dict, the_vars): if not the_dict: return diff --git a/support/get_cmake_value.py b/support/get_cmake_value.py index 03d575d..ec83498 100644 --- a/support/get_cmake_value.py +++ b/support/get_cmake_value.py @@ -1,5 +1,6 @@ import sublime + def get_cmake_value(the_dict, key): if not the_dict: return None diff --git a/support/get_setting.py b/support/get_setting.py index 113f63c..126d4bb 100644 --- a/support/get_setting.py +++ b/support/get_setting.py @@ -1,5 +1,6 @@ import sublime + def get_setting(view, key, default=None): if view: settings = view.settings() diff --git a/support/headerdb.py b/support/headerdb.py new file mode 100644 index 0000000..fecded4 --- /dev/null +++ b/support/headerdb.py @@ -0,0 +1,280 @@ +import os +import re + +from .db.memory import InMemoryCompilationDatabase +from .db.json import JSONCompilationDatabase + + +def sanitize_compile_options(compile_command): + filename = os.path.splitext(compile_command.file)[1] + file_norm = compile_command.normfile + adjusted = [] + i = 0 + command = compile_command.command + while i < len(command): + # end of options, skip all positional arguments (source files) + if command[i] == "--": + break + # strip -c + if command[i] == "-c": + i += 1 + continue + # strip -o and -o + if command[i].startswith("-o"): + if command[i] == "-o": + i += 2 + else: + i += 1 + continue + # skip input file + if command[i].endswith(filename): + arg_norm = os.path.normpath( + os.path.join(compile_command.directory, command[i])) + if file_norm == arg_norm: + i += 1 + continue + adjusted.append(command[i]) + i += 1 + return adjusted + + +def mimic_path_relativity(path, other, default_dir): + """If 'other' file is relative, make 'path' relative, otherwise make it + absolute. + + """ + if os.path.isabs(other): + return os.path.join(default_dir, path) + if os.path.isabs(path): + return os.path.relpath(path, default_dir) + return path + + +def derive_compile_command(header_file, reference): + return { + "directory": + reference.directory, + "file": + mimic_path_relativity(header_file, reference.file, + reference.directory), + "command": + sanitize_compile_options(reference) + } + + +def get_file_includes(path): + """Returns a tuple of (quote, filename). + + Quote is one of double quote mark '\"' or opening angle bracket '<'. + """ + includes = [] + with open(path, "rb") as istream: + include_pattern = re.compile( + br'\s*#\s*include\s+(?P["<])(?P.+?)[">]') + for b_line in istream: + b_match = re.match(include_pattern, b_line) + if b_match: + u_quote = b_match.group('quote').decode('ascii') + try: + u_filename = b_match.group('filename').decode('utf-8') + except UnicodeDecodeError: + u_filename = b_match.group('filename').decode('latin-1') + includes.append((u_quote, u_filename)) + return includes + + +def extract_include_dirs(compile_command): + header_search_path = [] + i = 0 + command = sanitize_compile_options(compile_command) + while i < len(command): + # -I and -I + if command[i].startswith("-I"): + if command[i] == "-I": + i += 1 + header_search_path.append(command[i]) + else: + header_search_path.append(command[i][2:]) + i += 1 + return [ + os.path.join(compile_command.directory, p) for p in header_search_path + ] + + +def get_implicit_header_search_path(compile_command): + return os.path.dirname( + os.path.join(compile_command.directory, compile_command.file)) + + +SUBWORD_SEPARATORS_RE = re.compile("[^A-Za-z0-9]") + +# The comment is shitty because I don't fully understand what is going on. +# Shamelessly stolen, then modified from: +# - http://stackoverflow.com/a/29920015/951426 +SUBWORD_CAMEL_SPLIT_RE = re.compile(r""" +.+? # capture text instead of discarding (#1) +( + (?:(?<=[a-z0-9])) # non-capturing positive lookbehind assertion + (?=[A-Z]) # match first uppercase letter without consuming +| + (?<=[A-Z]) # an upper char should prefix + (?=[A-Z][a-z0-9]) # an upper char, lookahead assertion: does not + # consume the char +| +$ # ignore capture text #1 +)""", re.VERBOSE) + + +def subword_split(name): + """Split name into subword. + + Split camelCase, lowercase_underscore, and alike into an array of word. + + Subword is the vocabulary stolen from Emacs subword-mode: + https://www.gnu.org/software/emacs/manual/html_node/ccmode/Subword-Movement.html + + """ + words = [] + for camel_subname in re.split(SUBWORD_SEPARATORS_RE, name): + matches = re.finditer(SUBWORD_CAMEL_SPLIT_RE, camel_subname) + words.extend([m.group(0) for m in matches]) + return words + + +# Code shamelessly stolen from: http://stackoverflow.com/a/24547864/951426 +def lcsubstring_length(a, b): + """Find the length of the longuest contiguous subsequence of subwords. + + The name is a bit of a misnomer. + + """ + table = {} + k = 0 + for i, ca in enumerate(a, 1): + for j, cb in enumerate(b, 1): + if ca == cb: + table[i, j] = table.get((i - 1, j - 1), 0) + 1 + if table[i, j] > k: + k = table[i, j] + return k + + +def score_other_file(a, b): + """Score the similarity of the given file to the other file. + + Paths are expected absolute and normalized. + """ + a_dir, a_filename = os.path.split(os.path.splitext(a)[0]) + a_subwords = subword_split(a_filename) + b_dir, b_filename = os.path.split(os.path.splitext(b)[0]) + b_subwords = subword_split(b_filename) + + score = 0 + + # score subword + # if a.cpp and b.cpp includes a_private.hpp, a.cpp should score better + subseq_length = lcsubstring_length(a_subwords, b_subwords) + score += 10 * subseq_length + # We also penalize the length of the mismatch + # + # For example: + # include/String.hpp + # include/SmallString.hpp + # test/StringTest.cpp + # test/SmallStringTest.cpp + # + # Here we prefer String.hpp to get the compile options of StringTest over + # the one of SmallStringTest. + score -= 10 * (len(a_subwords) + len(b_subwords) - 2 * subseq_length) + + if a_dir == b_dir: + score += 50 + + return score + + +class _Data(object): + __slots__ = ['score', 'compile_command', 'db_idx'] + + def __init__(self, score=0, compile_command=None, db_idx=-1): + self.score = score + if compile_command is None: + self.compile_command = {} + else: + self.compile_command = compile_command + self.db_idx = db_idx + + +def _make_headerdb1(compile_commands_iter, db_files, db_idx, header_mapping): + for compile_command in compile_commands_iter: + if isinstance(compile_command, dict): + compile_command = JSONCompilationDatabase._dict_to_compile_command( + compile_command) + implicit_search_path = get_implicit_header_search_path(compile_command) + header_search_paths = extract_include_dirs(compile_command) + src_file = compile_command.normfile + for quote, filename in get_file_includes(src_file): + header_abspath = None + score = 0 + if quote == '"': + candidate = os.path.normpath( + os.path.join(implicit_search_path, filename)) + if os.path.isfile(candidate): + header_abspath = candidate + if not header_abspath: + for search_path in header_search_paths: + candidate = os.path.normpath( + os.path.join(search_path, filename)) + if os.path.isfile(candidate): + header_abspath = candidate + break + else: + continue + norm_abspath = os.path.normpath(header_abspath) + # skip files already present in the database + if norm_abspath in db_files: + continue + score = score_other_file(src_file, norm_abspath) + try: + data = header_mapping[norm_abspath] + except KeyError: + data = _Data(score=(score - 1)) + header_mapping[norm_abspath] = data + if score > data.score: + data.score = score + data.compile_command = derive_compile_command( + norm_abspath, compile_command) + data.db_idx = db_idx + + +def make_headerdb(layers): + databases_len = len(layers[0]) + complementary_databases = [ + InMemoryCompilationDatabase() for _ in range(databases_len) + ] + + db_files = set() + for layer in layers: + for database in layer: + db_files.update(database.get_all_files()) + + # loop until there is nothing more to resolve + # we first get the files directly included by the compilation database + # then the files directly included by these files and so on + while True: + # mapping of
-> _Data + db_update = {} + for layer in layers: + for db_idx, database in enumerate(layer): + _make_headerdb1(database.get_all_compile_commands(), db_files, + db_idx, db_update) + if not db_update: + break + layers = [[ + InMemoryCompilationDatabase() for _ in range(databases_len) + ]] + for k, v in db_update.items(): + db_files.add(k) + for db_list in (layers[0], complementary_databases): + db_list[v.db_idx].compile_commands.append(v.compile_command) + return complementary_databases diff --git a/support/models.py b/support/models.py new file mode 100644 index 0000000..b94802f --- /dev/null +++ b/support/models.py @@ -0,0 +1,71 @@ +import os +import json + + +class CompileCommand(object): + + __slots__ = ("directory", "file", "command") + + def __init__(self, directory, file, command): + self.directory = directory + self.file = file + self.command = command + + @property + def normfile(self): + return os.path.normpath(os.path.join(self.directory, self.file)) + + def __repr__(self): + return '{{directory:"{}",file:"{}",command:"{}"}}'.format( + self.directory, self.file, self.command) + + def __str__(self): + return self.__repr__() + + def _as_tuple(self): + return (self.directory, self.file, self.command) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._as_tuple() == other._as_tuple() + raise NotImplemented() + + def __ne__(self, other): + return not self == other + + class JSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, CompileCommand): + return o.__repr__() + else: + return super().default(o) + + +class CompilationDatabaseInterface(object): + @classmethod + def probe_directory(cls, directory): + """Probe compilation database for a specific directory. + + Should return an instance of the compilation database + if the directory contains a database. + If the directory does not contain a database, + a FileNotFoundError should be raised (the default action if not + overriden). + """ + raise FileNotFoundError( + "{}: compilation databases not found".format(directory)) + + def get_compile_commands(self, filepath): + """Get the compile commands for the given file. + + Return an iterable of CompileCommand. + """ + raise NotImplemented() + + def get_all_files(self): + """Return an iterable of path strings.""" + raise NotImplemented() + + def get_all_compile_commands(self): + """Return an iterable of CompileCommand.""" + raise NotImplemented() diff --git a/support/server.py b/support/server.py new file mode 100644 index 0000000..0e713c0 --- /dev/null +++ b/support/server.py @@ -0,0 +1,443 @@ +import Default.exec +import json +import sublime +import copy +import time +import os +import threading +import shutil +from .headerdb import make_headerdb +from .db.json import JSONCompilationDatabase +from .models import CompileCommand + + +class Target(object): + + __slots__ = ("name", "fullname", "type", "directory", "config") + + def __init__(self, name, fullname, type, directory, config): + self.name = name + self.fullname = fullname + self.type = type + self.directory = directory + self.config = config + + def __hash__(self): + return hash(self.name) + + def cmd(self): + result = ["cmake", "--build", "."] + if self.type == "ALL": + return result + result.extend(["--target", self.name]) + if self.config: + result.extend["--config", self.config] + return result + + +class Server(Default.exec.ProcessListener): + def __init__(self, + window, + cmake_settings, + experimental=True, + debug=True, + protocol=(1, 0), + env={}): + self.window = window + self.cmake = cmake_settings + self.experimental = experimental + self.protocol = protocol + self.supported_protocols = None + self.is_configuring = False + self.is_building = False # maintained by CmakeBuildCommand + self.data_parts = '' + self.inside_json_object = False + self.include_paths = set() + self.targets = None + cmd = ["cmake", "-E", "server"] + if experimental: + cmd.append("--experimental") + if debug: + cmd.append("--debug") + self.proc = Default.exec.AsyncProcess( + cmd=cmd, shell_cmd=None, listener=self, env=env) + + def __del__(self): + if self.proc: + self.proc.kill() + + _BEGIN_TOKEN = '[== "CMake Server" ==[' + _END_TOKEN = ']== "CMake Server" ==]' + + def on_data(self, _, data): + data = data.decode("utf-8").strip() + if data.startswith("CMake Error:"): + sublime.error_message(data) + return + + while data: + if self.inside_json_object: + end_index = data.find(self.__class__._END_TOKEN) + if end_index == -1: + # This is okay, wait for more data. + self.data_parts += data + else: + self.data_parts += data[0:end_index] + data = data[end_index + len(self.__class__._END_TOKEN):] + self.__flush_the_data() + else: # not inside json object + begin_index = data.find(self.__class__._BEGIN_TOKEN) + if begin_index == -1: + sublime.error_message("Received unknown data part: " + + data) + data = None + else: + begin_token_end = begin_index + len( + self.__class__._BEGIN_TOKEN) + end_index = data.find(self.__class__._END_TOKEN, + begin_token_end) + if end_index == -1: + # This is okay, wait for more data. + self.data_parts += data[begin_token_end:] + data = None + self.inside_json_object = True + else: + self.data_parts += data[begin_token_end:end_index] + data = data[ + end_index + len(self.__class__._END_TOKEN):] + self.__flush_the_data() + + def __flush_the_data(self): + d = json.loads(self.data_parts) + self.data_parts = "" + self.inside_json_object = False + self.receive_dict(d) + + def on_finished(self, _): + self.window.status_message("CMake Server has quit (exit code {})" + .format(self.proc.exit_code())) + + def send(self, data): + while not hasattr(self, "proc"): + time.sleep(0.01) # terrible hack :( + self.proc.proc.stdin.write(data) + self.proc.proc.stdin.flush() + + def send_dict(self, thedict): + data = b'\n[== "CMake Server" ==[\n' + data += json.dumps(thedict).encode('utf-8') + b'\n' + data += b'\n]== "CMake Server" ==]\n' + self.send(data) + + def send_handshake(self): + self.protocol = {"major": 1, "minor": 0, "isExperimental": True} + self.send_dict({ + "type": "handshake", + "protocolVersion": self.protocol, + "sourceDirectory": self.cmake.source_folder, + "buildDirectory": self.cmake.build_folder, + "generator": self.cmake.generator, + "platform": self.cmake.platform, + "toolset": self.cmake.toolset + }) + + def set_global_setting(self, key, value): + self.send_dict({"type": "setGlobalSettings", key: value}) + + def configure(self, cache_arguments={}): + if self.is_configuring: + return + self.is_configuring = True + self.bad_configure = False + window = self.window + view = window.create_output_panel("cmake.configure", True) + view.settings().set("result_file_regex", r'CMake\s(?:Error|Warning)' + r'(?:\s\(dev\))?\sat\s(.+):(\d+)()\s?\(?(\w*)\)?:') + view.settings().set("result_base_dir", self.cmake.source_folder) + view.set_syntax_file( + "Packages/CMakeBuilder/Syntax/Configure.sublime-syntax") + settings = sublime.load_settings("CMakeBuilder.sublime-settings") + if settings.get("server_configure_verbose", False): + window.run_command("show_panel", + {"panel": "output.cmake.configure"}) + overrides = copy.deepcopy(self.cmake.command_line_overrides) + overrides.update(cache_arguments) + ovr = [] + for key, value in overrides.items(): + if type(value) is bool: + value = "ON" if value else "OFF" + ovr.append("-D{}={}".format(key, value)) + self.send_dict({"type": "configure", "cacheArguments": ovr}) + + def compute(self): + self.send_dict({"type": "compute"}) + + def codemodel(self): + self.send_dict({"type": "codemodel"}) + + def cache(self): + self.send_dict({"type": "cache"}) + + def file_system_watchers(self): + self.send_dict({"type": "fileSystemWatchers"}) + + def cmake_inputs(self): + self.send_dict({"type": "cmakeInputs"}) + + def global_settings(self): + self.send_dict({"type": "globalSettings"}) + + def receive_dict(self, thedict): + t = thedict.pop("type") + if t == "hello": + self.supported_protocols = thedict.pop("supportedProtocolVersions") + self.send_handshake() + elif t == "reply": + self.receive_reply(thedict) + elif t == "error": + self.receive_error(thedict) + elif t == "progress": + self.receive_progress(thedict) + elif t == "message": + self.receive_message(thedict) + elif t == "signal": + self.receive_signal(thedict) + else: + print('CMakeBuilder: Received unknown type "{}"'.format(t)) + print(thedict) + + def receive_reply(self, thedict): + reply = thedict["inReplyTo"] + if reply == "handshake": + self.window.status_message( + "CMake server protocol {}.{}, handshake is OK" + .format(self.protocol["major"], self.protocol["minor"])) + self.configure() + elif reply == "setGlobalSettings": + self.window.status_message("Global CMake setting is modified") + elif reply == "configure": + if self.bad_configure: + self.is_configuring = False + self.window.status_message( + "Some errors occured during configure!") + else: + self.window.status_message("Project is configured") + elif reply == "compute": + self.window.status_message("Project is generated") + self.is_configuring = False + self.codemodel() + elif reply == "fileSystemWatchers": + self.dump_to_new_view(thedict, "File System Watchers") + elif reply == "cmakeInputs": + self.dump_to_new_view(thedict, "CMake Inputs") + elif reply == "globalSettings": + # thedict.pop("inReplyTo") + thedict.pop("cookie") + thedict.pop("capabilities") + self.items = [] + self.types = [] + for k, v in thedict.items(): + if type(v) in (dict, list): + continue + self.items.append([str(k), str(v)]) + self.types.append(type(v)) + window = self.window + + def on_done(index): + if index == -1: + return + key = self.items[index][0] + old_value = self.items[index][1] + value_type = self.types[index] + + def on_done_input(new_value): + if value_type is bool: + new_value = bool(new_value) + self.set_global_setting(key, new_value) + + window.show_input_panel('new value for "' + key + '": ', + old_value, on_done_input, None, None) + + window.show_quick_panel(self.items, on_done) + elif reply == "codemodel": + configurations = thedict.pop("configurations") + self.include_paths = set() + self.targets = set() + for config in configurations: + # name = config.pop("name") + projects = config.pop("projects") + for project in projects: + targets = project.pop("targets") + for target in targets: + target_type = target.pop("type") + target_name = target.pop("name") + try: + target_fullname = target.pop("fullName") + except KeyError as e: + target_fullname = target_name + target_dir = target.pop("buildDirectory") + self.targets.add( + Target(target_name, target_fullname, target_type, + target_dir, "")) + if target_type == "EXECUTABLE": + self.targets.add( + Target("Run: " + target_name, target_fullname, + "RUN", target_dir, "")) + file_groups = target.pop("fileGroups", []) + for file_group in file_groups: + include_paths = file_group.pop("includePath", []) + for include_path in include_paths: + path = include_path.pop("path", None) + if path: + self.include_paths.add(path) + self.targets.add( + Target("BUILD ALL", "BUILD ALL", "ALL", + self.cmake.build_folder, "")) + self.targets = list(self.targets) + path = os.path.join(self.cmake.build_folder, + "compile_commands.json") + if os.path.isfile(path): + self.handle_compdb() + elif reply == "cache": + cache = thedict.pop("cache") + self.items = [] + for item in cache: + t = item["type"] + if t in ("INTERNAL", "STATIC"): + continue + try: + docstring = item["properties"]["HELPSTRING"] + except Exception as e: + docstring = "" + key = item["key"] + value = item["value"] + self.items.append( + [key + " [" + t.lower() + "]", value, docstring]) + + def on_done(index): + if index == -1: + return + item = self.items[index] + key = item[0].split(" ")[0] + old_value = item[1] + + def on_done_input(new_value): + self.configure({key: value}) + + self.window.show_input_panel('new value for "' + key + '": ', + old_value, on_done_input, None, + None) + + self.window.show_quick_panel(self.items, on_done) + else: + print("received unknown reply type:", reply) + + def handle_compdb(self): + db = JSONCompilationDatabase.probe_directory(self.cmake.build_folder) + headerdb = make_headerdb([[db]])[0] + db = list(db._data) + db.extend(headerdb.get_all_compile_commands()) + path = os.path.join(self.cmake.build_folder, "compile_commands.json") + with open(path, "w") as f: + json.dump( + db, + f, + check_circular=False, + indent=2, + cls=CompileCommand.JSONEncoder) + data = self.window.project_data() + settings = sublime.load_settings("CMakeBuilder.sublime-settings") + setting = "auto_update_EasyClangComplete_compile_commands_location" + if settings.get(setting, False): + data["settings"]["ecc_flags_sources"] = [{ + "file": + "compile_commands.json", + "search_in": + self.cmake.build_folder_pre_expansion + }] + setting = "auto_update_compile_commands_project_setting" + if settings.get(setting, False): + data["settings"]["compile_commands"] = \ + self.cmake.build_folder_pre_expansion + setting = "copy_compile_commands_to_project_path" + if settings.get(setting, False): + destination = os.path.join(self.cmake.source_folder, + "compile_commands.json") + shutil.copyfile(path, destination) + self.window.set_project_data(data) + + def receive_error(self, thedict): + reply = thedict["inReplyTo"] + msg = thedict["errorMessage"] + if reply in ("configure", "compute"): + self.window.status_message(msg) + if self.is_configuring: + self.is_configuring = False + else: + sublime.error_message("{} (in reply to {})".format(msg, reply)) + + def receive_progress(self, thedict): + view = self.window.active_view() + minimum = thedict["progressMinimum"] + maximum = thedict["progressMaximum"] + current = thedict["progressCurrent"] + if maximum == current: + view.erase_status("cmake_" + thedict["inReplyTo"]) + if thedict["inReplyTo"] == "configure" and not self.bad_configure: + self.compute() + else: + status = "{0} {1:.0f}%".format( + thedict["progressMessage"], + 100.0 * (float(current) / float(maximum - minimum))) + view.set_status("cmake_" + thedict["inReplyTo"], status) + + def receive_message(self, thedict): + window = self.window + if thedict["inReplyTo"] in ("configure", "compute"): + name = "cmake.configure" + else: + name = "cmake." + thedict["inReplyTo"] + view = window.find_output_panel(name) + assert view + settings = sublime.load_settings("CMakeBuilder.sublime-settings") + if settings.get("server_configure_verbose", False): + window.run_command("show_panel", + {"panel": "output.{}".format(name)}) + view.run_command("append", { + "characters": thedict["message"] + "\n", + "force": True, + "scroll_to_end": True + }) + self._check_for_errors_in_configure(view) + + _signal_lock = threading.Lock() + + def receive_signal(self, thedict): + with self.__class__._signal_lock: + if (thedict["name"] == "dirty" and not self.is_configuring + and not self.is_building): + self.configure() + else: + print("received signal") + print(thedict) + + def dump_to_new_view(self, thedict, name): + view = self.window.new_file() + view.set_scratch(True) + view.set_name(name) + thedict.pop("inReplyTo") + thedict.pop("cookie") + view.run_command("append", { + "characters": json.dumps(thedict, indent=2), + "force": True + }) + view.set_read_only(True) + view.set_syntax_file("Packages/JavaScript/JSON.sublime-syntax") + + def _check_for_errors_in_configure(self, view): + scopes = view.find_by_selector("invalid.illegal") + errorcount = len(scopes) + if errorcount > 0: + self.bad_configure = True + self.window.run_command("show_panel", + {"panel": "output.cmake.configure"}) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..606090c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,13 @@ +# Unit Testing + +We use https://github.com/randy3k/UnitTesting for the tests. + +Download that package from Package Control, then run + + UnitTesting: Test Current Project + +or + + UnitTesting: Test Current File + +To add a new test, inherit from TestCase defined in fixtures.py. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..35da521 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,73 @@ +"""Defines TestCase""" +import unittesting +import os +import sublime +from CMakeBuilder import ServerManager + + +class TestCase(unittesting.helpers.TempDirectoryTestCase): + """ + TempDirectoryTestCase is a subclass of DeferrableTestCase which creates and + opens a temp directory before running the test case and close the window + when the test case finishes running. + + See: + https://github.com/divmain/GitSavvy/blob/master/tests/test_git/common.py + https://github.com/randy3k/UnitTesting/blob/master/unittesting/helpers.py + """ + + @classmethod + def setUpClass(cls): + """Prepares a class for a test case involving project files.""" + yield from super(TestCase, cls).setUpClass() + assert hasattr(cls, "cmake_settings") + assert hasattr(cls, "files") + assert hasattr(cls, "window") + assert isinstance(cls.files, list) + assert isinstance(cls.window, sublime.Window) + data = cls.window.project_data() + assert data["folders"][0]["path"] is not None + data["settings"] = {} + data["settings"]["cmake"] = cls.cmake_settings + cls.window.set_project_data(data) + for pair in cls.files: + assert isinstance(pair, tuple) + assert len(pair) == 2 + assert isinstance(pair[0], str) + assert isinstance(pair[1], str) + path = os.path.join(cls._temp_dir, pair[0]) + content = pair[1] + with open(path, "w") as f: + f.write(content) + + +class ServerTestCase(TestCase): + + @classmethod + def setUpClass(cls): + yield from super(ServerTestCase, cls).setUpClass() + ServerManager.build_folder_pre_expansion = cls.cmake_settings["schemes"][0]["build_folder"] + ServerManager.build_folder = sublime.expand_variables(ServerManager.build_folder_pre_expansion, cls.window.extract_variables()) + ServerManager.source_folder = cls.window.extract_variables()["folder"] + ServerManager.command_line_overrides = { + "CMAKE_EXPORT_COMPILE_COMMANDS": True + } + print(cls.window.extract_variables(), + ServerManager.build_folder_pre_expansion, + ServerManager.build_folder, ServerManager.source_folder) + if sublime.platform() in ("osx", "linux"): + ServerManager.generator = "Unix Makefiles" + else: + ServerManager.generator = "NMake Makefiles" + # ServerManager._run_configure_with_new_settings() + # assert ServerManager.get(cls.window) is not None + + @classmethod + def tearDownClass(cls): + server = ServerManager._servers.pop(cls.window.id(), None) + assert server is not None + super(ServerTestCase, cls).tearDownClass() + + @classmethod + def get_server(cls): + return ServerManager.get(cls.window) diff --git a/tests/test_configure.py b/tests/test_configure.py new file mode 100644 index 0000000..a598e1e --- /dev/null +++ b/tests/test_configure.py @@ -0,0 +1,20 @@ +from CMakeBuilder.tests.fixtures import TestCase + + +class TestConfigure(TestCase): + + cmake_settings = { + "build_folder": "$folder/build" + } + + files = [ + ("CMakeLists.txt", r""" +cmake_minimum_required(VERSION 2.8 FATAL_ERROR) +project(foo) +message(STATUS "okay") +""") + ] + + def test_configure(self): + # self.window.run_command("cmake_configure") + self.assertTrue(True) diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..59f3a82 --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,29 @@ +from CMakeBuilder.tests.fixtures import ServerTestCase +import CMakeBuilder +import sublime + + +if CMakeBuilder.support.capabilities("serverMode"): + + class TestServer(ServerTestCase): + + cmake_settings = { + "schemes": [ + { + "name": "Debug", + "build_folder": "${folder}/build" + } + ] + } + + files = [ + ("CMakeLists.txt", r""" + cmake_minimum_required(VERSION 2.8 FATAL_ERROR) + project(foo) + message(STATUS "okay") +""") + ] + + # def test_server(self): + # server = self.get_server() + # self.assertTrue(server is not None) diff --git a/unittesting.json b/unittesting.json new file mode 100644 index 0000000..9d4e572 --- /dev/null +++ b/unittesting.json @@ -0,0 +1,5 @@ +{ + "tests_dir" : "tests", + "pattern" : "test_*", + "deferred": true +}