From aaa127d3e336177a1348af88bbb450f5d8576695 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 2 Jul 2017 18:54:43 +0200 Subject: [PATCH 01/64] Preliminary server functionality --- server.py | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 server.py diff --git a/server.py b/server.py new file mode 100644 index 0000000..b06f87e --- /dev/null +++ b/server.py @@ -0,0 +1,187 @@ +import Default.exec +import json +import sublime + +class CmakeServer(Default.exec.ProcessListener): + + def __init__(self, source_dir, build_dir, generator): + self.source_dir = source_dir + self.build_dir = build_dir + self.generator = generator + self.proc = Default.exec.AsyncProcess( + cmd=["cmake", "-E", "server", "--experimental", "--debug"], + shell_cmd=None, + listener=self, + env={}) + + def __del__(self): + self.proc.kill() + + def on_data(self, _, data): + import re + for piece in re.split(r'\[== "CMake Server" ==\[|]== "CMake Server" ==]', data.decode('utf-8')): + if piece == r'\n': + continue + try: + thedict = json.loads(piece) + except ValueError as e: + pass + # print(str(e),":", piece) + else: + self.receive_dict(thedict) + + # print(data) + # start = b'\n[== "CMake Server" ==[\n' + # end = b'\n]== "CMake Server" ==]\n' + # first = 0 + # while True: + # first = data.find(start, first) + len(start) + # if first == -1: + # break + # last = data.find(end, first) + # if last == -1: + # break + # print(data[first:last]) + # self.receive_dict(json.loads(data[first:last].decode('utf-8'))) + + def on_finished(self, _): + print("finished with status", self.proc.exit_status()) + + def send(self, data): + 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, source_dir, build_dir, generator): + self.send_dict({ + "type": "handshake", + "protocolVersion": self.protocols[0], + "sourceDirectory": source_dir, + "buildDirectory": build_dir, + "generator": generator + }) + + def set_global_setting(self, key, value): + self.send_dict({"type": "setGlobalSettings", key: value}) + + def set_global_setting_interactive(self): + self._global_settings_interactive = True + self.global_settings() + + def configure(self): + window = sublime.active_window() + window.create_output_panel("cmake.configure", True) + window.run_command("show_panel", {"panel": "output.cmake.configure"}) + self.send_dict({"type": "configure"}) + + 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): + print(thedict) + t = thedict["type"] + if t == "hello": + self.protocols = thedict["supportedProtocolVersions"] + print(self.protocols) + self.send_handshake(self.source_dir, self.build_dir, self.generator) + 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)) + + def receive_reply(self, thedict): + reply_to = thedict["inReplyTo"] + if reply_to == "handshake": + print("handshake is OK") + elif reply_to == "setGlobalSettings": + print("global setting was changed") + elif reply_to == "configure": + print("project is configured") + elif reply_to == "compute": + print("project is computed") + elif reply_to == "codemodel": + print(thedict) + elif reply_to == "fileSystemWatchers": + view = sublime.active_window().new_file() + view.set_scratch(True) + view.set_name("File System Watchers") + dirs = thedict["watchedDirectories"] + files = thedict["watchedFiles"] + view.run_command("append", {"characters": "watched directories:\n", "force": True}) + for d in dirs: + view.run_command("append", {"characters": "\t{}\n".format(d), "force": True}) + view.run_command("append", {"characters": "\n\nwatched files:\n", "force": True}) + for f in files: + view.run_command("append", {"characters": "\t{}\n".format(f), "force": True}) + view.set_read_only(True) + elif reply_to == "cmakeInputs": + print(thedict) + elif reply_to == "globalSettings": + pass + else: + print("received unknown reply type:", reply_to) + + def receive_error(self, thedict): + sublime.error_message("{} (in reply to {})".format(thedict["errorMessage"], thedict["inReplyTo"])) + + def receive_progress(self, thedict): + print("received progress") + print(thedict) + view = sublime.active_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": + 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): + print(thedict) + window = sublime.active_window() + if thedict["inReplyTo"] in ("configure", "compute"): + name = "cmake.configure" + else: + name = "cmake." + thedict["inReplyTo"] + view = window.find_output_panel(name) + assert view + window.run_command("show_panel", {"panel": "output.{}".format(name)}) + view = window.find_output_panel(name) + view.run_command("append", {"characters": thedict["message"] + "\n", "force": True, "scroll_to_end": True}) + + def receive_signal(self, thedict): + print("received signal") + print(thedict) From aa2ab22b31f006a6965b0c888dc4954c4cc4a117 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 3 Jul 2017 00:03:12 +0200 Subject: [PATCH 02/64] Further updates to server --- server.py | 205 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 137 insertions(+), 68 deletions(-) diff --git a/server.py b/server.py index b06f87e..389bffb 100644 --- a/server.py +++ b/server.py @@ -2,50 +2,57 @@ import json import sublime -class CmakeServer(Default.exec.ProcessListener): +class Server(Default.exec.ProcessListener): - def __init__(self, source_dir, build_dir, generator): + def __init__(self, + source_dir, + build_dir, + generator, + experimental=True, + debug=True, + protocol=(1,0), + env={}): self.source_dir = source_dir self.build_dir = build_dir self.generator = generator + self.experimental = experimental + self.protocol = protocol + self.is_configuring = False + cmd = ["cmake", "-E", "server"] + if experimental: + cmd.append("--experimental") + if debug: + cmd.append("--debug") self.proc = Default.exec.AsyncProcess( - cmd=["cmake", "-E", "server", "--experimental", "--debug"], + cmd=cmd, shell_cmd=None, listener=self, - env={}) + env=env) def __del__(self): self.proc.kill() def on_data(self, _, data): + data = data.decode("utf-8") + if data.startswith("CMake Error:"): + sublime.error_message(data) + return import re - for piece in re.split(r'\[== "CMake Server" ==\[|]== "CMake Server" ==]', data.decode('utf-8')): + for piece in re.split( + r'\[== "CMake Server" ==\[|]== "CMake Server" ==]', data): if piece == r'\n': continue try: thedict = json.loads(piece) except ValueError as e: pass - # print(str(e),":", piece) else: self.receive_dict(thedict) - - # print(data) - # start = b'\n[== "CMake Server" ==[\n' - # end = b'\n]== "CMake Server" ==]\n' - # first = 0 - # while True: - # first = data.find(start, first) + len(start) - # if first == -1: - # break - # last = data.find(end, first) - # if last == -1: - # break - # print(data[first:last]) - # self.receive_dict(json.loads(data[first:last].decode('utf-8'))) def on_finished(self, _): - print("finished with status", self.proc.exit_status()) + sublime.active_window().status_message( + "CMake Server has quit (exit code {})" + .format(self.proc.exit_code())) def send(self, data): self.proc.proc.stdin.write(data) @@ -57,25 +64,42 @@ def send_dict(self, thedict): data += b'\n]== "CMake Server" ==]\n' self.send(data) - def send_handshake(self, source_dir, build_dir, generator): + def send_handshake(self): + best_protocol = self.protocols[0] + for protocol in self.protocols: + if (protocol["major"] == self.protocol[0] and + protocol["minor"] == self.protocol[1]): + best_protocol = protocol + break + if protocol["isExperimental"] and not self.experimental: + continue + if protocol["major"] > best_protocol["major"]: + best_protocol = protocol + elif (protocol["major"] == best_protocol["major"] and + protocol["minor"] > best_protocol["minor"]): + best_protocol = protocol + self.protocol = best_protocol self.send_dict({ "type": "handshake", - "protocolVersion": self.protocols[0], - "sourceDirectory": source_dir, - "buildDirectory": build_dir, - "generator": generator + "protocolVersion": self.protocol, + "sourceDirectory": self.source_dir, + "buildDirectory": self.build_dir, + "generator": self.generator }) def set_global_setting(self, key, value): self.send_dict({"type": "setGlobalSettings", key: value}) - def set_global_setting_interactive(self): - self._global_settings_interactive = True - self.global_settings() - def configure(self): + self.is_configuring = True window = sublime.active_window() - window.create_output_panel("cmake.configure", True) + view = window.create_output_panel("cmake.configure", True) + view.settings().set( + "result_file_regex", + r'CMake\s(?:Error|Warning)(?:\s\(dev\))?\sat\s(.+):(\d+)()\s?\(?(\w*)\)?:') + view.settings().set("result_base_dir", self.source_dir) + view.set_syntax_file( + "Packages/CMakeBuilder/Syntax/Configure.sublime-syntax") window.run_command("show_panel", {"panel": "output.cmake.configure"}) self.send_dict({"type": "configure"}) @@ -98,12 +122,10 @@ def global_settings(self): self.send_dict({"type": "globalSettings"}) def receive_dict(self, thedict): - print(thedict) t = thedict["type"] if t == "hello": self.protocols = thedict["supportedProtocolVersions"] - print(self.protocols) - self.send_handshake(self.source_dir, self.build_dir, self.generator) + self.send_handshake() elif t == "reply": self.receive_reply(thedict) elif t == "error": @@ -116,45 +138,74 @@ def receive_dict(self, thedict): self.receive_signal(thedict) else: print('CMakeBuilder: Received unknown type "{}"'.format(t)) + print(thedict) def receive_reply(self, thedict): - reply_to = thedict["inReplyTo"] - if reply_to == "handshake": - print("handshake is OK") - elif reply_to == "setGlobalSettings": - print("global setting was changed") - elif reply_to == "configure": - print("project is configured") - elif reply_to == "compute": - print("project is computed") - elif reply_to == "codemodel": + reply = thedict["inReplyTo"] + if reply == "handshake": + sublime.active_window().status_message( + "CMake server {}.{} at your service!" + .format(self.protocol["major"], self.protocol["minor"])) + elif reply == "setGlobalSettings": + sublime.active_window().status_message( + "Global CMake setting is modified") + elif reply == "configure": + sublime.active_window().status_message("Project is configured") + elif reply == "compute": + sublime.active_window().status_message("Project is generated") + self.is_configuring = False + elif reply == "codemodel": print(thedict) - elif reply_to == "fileSystemWatchers": - view = sublime.active_window().new_file() - view.set_scratch(True) - view.set_name("File System Watchers") - dirs = thedict["watchedDirectories"] - files = thedict["watchedFiles"] - view.run_command("append", {"characters": "watched directories:\n", "force": True}) - for d in dirs: - view.run_command("append", {"characters": "\t{}\n".format(d), "force": True}) - view.run_command("append", {"characters": "\n\nwatched files:\n", "force": True}) - for f in files: - view.run_command("append", {"characters": "\t{}\n".format(f), "force": True}) - view.set_read_only(True) - elif reply_to == "cmakeInputs": + 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("type") + 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 = sublime.active_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": + print("received codemodel reply") print(thedict) - elif reply_to == "globalSettings": - pass else: - print("received unknown reply type:", reply_to) + print("received unknown reply type:", reply) def receive_error(self, thedict): - sublime.error_message("{} (in reply to {})".format(thedict["errorMessage"], thedict["inReplyTo"])) + reply = thedict["inReplyTo"] + msg = thedict["errorMessage"] + if reply in ("configure", "compute"): + + sublime.active_window().status_message(msg) + else: + sublime.error_message("{} (in reply to {})".format(msg, reply)) def receive_progress(self, thedict): - print("received progress") - print(thedict) view = sublime.active_window().active_view() minimum = thedict["progressMinimum"] maximum = thedict["progressMaximum"] @@ -170,7 +221,6 @@ def receive_progress(self, thedict): view.set_status("cmake_" + thedict["inReplyTo"], status) def receive_message(self, thedict): - print(thedict) window = sublime.active_window() if thedict["inReplyTo"] in ("configure", "compute"): name = "cmake.configure" @@ -180,8 +230,27 @@ def receive_message(self, thedict): assert view window.run_command("show_panel", {"panel": "output.{}".format(name)}) view = window.find_output_panel(name) - view.run_command("append", {"characters": thedict["message"] + "\n", "force": True, "scroll_to_end": True}) + view.run_command("append", + {"characters": thedict["message"] + "\n", + "force": True, + "scroll_to_end": True}) def receive_signal(self, thedict): - print("received signal") - print(thedict) + if thedict["name"] == "dirty" and not self.is_configuring: + self.configure() + else: + print("received signal") + print(thedict) + + def dump_to_new_view(self, thedict, name): + view = sublime.active_window().new_file() + view.set_scratch(True) + view.set_name(name) + thedict.pop("type") + 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") From a501678475c0b07835679549a9d851b4173f9c8f Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 3 Jul 2017 12:31:18 +0200 Subject: [PATCH 03/64] Fix on_data bugs and start work on codemodel, cache args --- server.py | 68 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/server.py b/server.py index 389bffb..fbb587b 100644 --- a/server.py +++ b/server.py @@ -18,6 +18,8 @@ def __init__(self, self.experimental = experimental self.protocol = protocol self.is_configuring = False + self.data_parts = '' + self.inside_json_object = False cmd = ["cmake", "-E", "server"] if experimental: cmd.append("--experimental") @@ -33,21 +35,20 @@ def __del__(self): self.proc.kill() def on_data(self, _, data): - data = data.decode("utf-8") + data = data.decode("utf-8").strip() if data.startswith("CMake Error:"): sublime.error_message(data) return - import re - for piece in re.split( - r'\[== "CMake Server" ==\[|]== "CMake Server" ==]', data): - if piece == r'\n': - continue - try: - thedict = json.loads(piece) - except ValueError as e: - pass - else: - self.receive_dict(thedict) + data = data.splitlines() + for piece in data: + if piece == ']== "CMake Server" ==]': + self.inside_json_object = False + self.receive_dict(json.loads(self.data_parts)) + self.data_parts = '' + if self.inside_json_object: + self.data_parts += piece + if piece == '[== "CMake Server" ==[': + self.inside_json_object = True def on_finished(self, _): sublime.active_window().status_message( @@ -90,7 +91,7 @@ def send_handshake(self): def set_global_setting(self, key, value): self.send_dict({"type": "setGlobalSettings", key: value}) - def configure(self): + def configure(self, cache_arguments=[]): self.is_configuring = True window = sublime.active_window() view = window.create_output_panel("cmake.configure", True) @@ -101,7 +102,7 @@ def configure(self): view.set_syntax_file( "Packages/CMakeBuilder/Syntax/Configure.sublime-syntax") window.run_command("show_panel", {"panel": "output.cmake.configure"}) - self.send_dict({"type": "configure"}) + self.send_dict({"type": "configure", "cacheArguments": cache_arguments}) def compute(self): self.send_dict({"type": "compute"}) @@ -154,8 +155,6 @@ def receive_reply(self, thedict): elif reply == "compute": sublime.active_window().status_message("Project is generated") self.is_configuring = False - elif reply == "codemodel": - print(thedict) elif reply == "fileSystemWatchers": self.dump_to_new_view(thedict, "File System Watchers") elif reply == "cmakeInputs": @@ -191,8 +190,41 @@ def on_done_input(new_value): None) window.show_quick_panel(self.items, on_done) elif reply == "codemodel": - print("received codemodel reply") - print(thedict) + configurations = thedict.pop("configurations") + for config in configurations: + name = config.pop("name") + projects = config.pop("projects") + for project in projects: + targets = project.pop("targets") + for target in targets: + if target["type"] == "EXECUTABLE": + print(target["fullName"], target["buildDirectory"]) + elif reply == "cache": + cache = thedict.pop("cache") + self.items = [] + for item in cache: + t = item["type"] + if t in ("INTERNAL", "STATIC"): + continue + docstring = item["properties"]["HELPSTRING"] + 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(["-D{}={}".format(key, new_value)]) + sublime.active_window().show_input_panel( + 'new value for "' + key + '": ', + old_value, + on_done_input, + None, + None) + sublime.active_window().show_quick_panel(self.items, on_done) else: print("received unknown reply type:", reply) From 9c9fccbf3b049c88400ed61ce5d834f061a6f430 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 3 Jul 2017 18:09:32 +0200 Subject: [PATCH 04/64] More work on server functionality I have to make a plan to integrate the server functionality with the old way of doing things. --- event_listeners/configure_on_save.py | 4 +- generators/__init__.py | 70 +++++++-- server.py | 209 ++++++++++++++++++++++++--- 3 files changed, 250 insertions(+), 33 deletions(-) diff --git a/event_listeners/configure_on_save.py b/event_listeners/configure_on_save.py index 0a75352..796dd4b 100644 --- a/event_listeners/configure_on_save.py +++ b/event_listeners/configure_on_save.py @@ -5,8 +5,8 @@ from CMakeBuilder.commands import CmakeConfigureCommand def _configure(window): - if not CmakeConfigureCommand(window).is_enabled(): return - window.run_command("cmake_configure") + if not CmakeConfigure2Command(window).is_enabled(): return + window.run_command("cmake_configure2") class ConfigureOnSave(sublime_plugin.EventListener): diff --git a/generators/__init__.py b/generators/__init__.py index 44c5379..9077c57 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -5,24 +5,60 @@ from CMakeBuilder.support import * 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.source_folder = os.path.dirname(window.project_file_name()) + while os.path.isfile(os.path.join(self.source_folder, "..", "CMakeLists.txt")): + self.source_folder = os.path.join(self.source_folder, "..") + 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') + # 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)) @@ -69,6 +105,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[0]]) + return result + def syntax(self): return None diff --git a/server.py b/server.py index fbb587b..b37e382 100644 --- a/server.py +++ b/server.py @@ -1,25 +1,154 @@ import Default.exec import json import sublime +import sublime_plugin +import os +import copy +import CMakeBuilder.generators + +class CmakeCommand(sublime_plugin.WindowCommand): + + def is_enabled(self): + try: + self.cmake = CMakeBuilder.generators.CMakeGenerator.create(self.window) + except Exception as e: + return False + self.server = ServerManager.get(self.window) + return self.server is not None and super(sublime_plugin.WindowCommand, self).is_enabled() + +class CmakeBuildCommand(CmakeCommand): + + def run(self): + if not self.is_enabled(): + sublime.error_message("Cannot build a CMake target!") + return + active_target = self.window.project_data().get("settings", {}).get("active_target", None) + if active_target is None: + self.items = [ [t[0], t[2], t[3]] 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 index == -1: + return + target = self.server.targets[index] + if target[2] == "RUN": + if sublime.platform() in ("linux", "osx"): + prefix = "./" + else: + prefix = "" + self.window.run_command( + "exec", { + "shell_cmd": prefix + target[1], + "working_dir": target[3] + } + ) + else: + self.window.run_command( + "exec", { + "cmd": self.cmake.cmd(target), + "file_regex": self.cmake.file_regex(), + "syntax": self.cmake.syntax(), + "working_dir": self.cmake.build_folder + } + ) + + +class CmakeConfigure2Command(CmakeCommand): + + def run(self): + self.server.configure(self.cmake.command_line_overrides) + +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}) + +class CmakeSetTarget(CmakeCommand): + + def run(self): + self.items = [ [t[0], t[2], t[3]] for t in self.server.targets ] + self.window.show_quick_panel(self.items, self._on_done) + + def _on_done(self, index): + data = self.window.project_data() + if index == -1: + data.get("settings", {}).pop("active_target", None) + else: + data["settings"]["active_target"] = index + self.window.set_project_data(data) + +class ServerManager(sublime_plugin.EventListener): + + _servers = {} + + @classmethod + def get(cls, window): + return cls._servers.get(window.id(), None) + + def on_load(self, view): + try: + window_id = view.window().id() + cmake = CMakeBuilder.generators.CMakeGenerator.create(view.window()) + except KeyError as e: + return + except AttributeError as e: + return + server = self.__class__._servers.get(window_id, None) + if not server: + try: + self.__class__._servers[window_id] = Server(cmake) + except Exception as e: + print(str(e)) + return + elif str(server.cmake) != str(cmake): + self.__class__._servers[window_id] = Server(cmake) + + on_activated = on_clone = on_load + + +class TargetDisplayer(sublime_plugin.EventListener): + + def on_load(self, view): + view.settings().add_on_change("active_target", lambda: self.check(view)) + + def check(self, view): + t = view.settings().get("active_target", None) + if not t: + view.erase_status("cmake_active_target") + return + server = ServerManager.get(view.window()) + if not server: + view.erase_status("cmake_active_target") + return + if not server.targets: + view.erase_status("cmake_active_target") + return + view.set_status("cmake_active_target", "TARGET: " + server.targets[int(t)][0]) + + on_activated = check class Server(Default.exec.ProcessListener): def __init__(self, - source_dir, - build_dir, - generator, + cmake_settings, experimental=True, debug=True, protocol=(1,0), env={}): - self.source_dir = source_dir - self.build_dir = build_dir - self.generator = generator + self.cmake = cmake_settings self.experimental = experimental self.protocol = protocol self.is_configuring = False self.data_parts = '' self.inside_json_object = False + self.include_paths = set() cmd = ["cmake", "-E", "server"] if experimental: cmd.append("--experimental") @@ -83,26 +212,35 @@ def send_handshake(self): self.send_dict({ "type": "handshake", "protocolVersion": self.protocol, - "sourceDirectory": self.source_dir, - "buildDirectory": self.build_dir, - "generator": self.generator + "sourceDirectory": self.cmake.source_folder, + "buildDirectory": self.cmake.build_folder, + "generator": str(self.cmake) }) def set_global_setting(self, key, value): self.send_dict({"type": "setGlobalSettings", key: value}) - def configure(self, cache_arguments=[]): + def configure(self, cache_arguments={}): + if self.is_configuring: + return self.is_configuring = True window = sublime.active_window() view = window.create_output_panel("cmake.configure", True) view.settings().set( "result_file_regex", r'CMake\s(?:Error|Warning)(?:\s\(dev\))?\sat\s(.+):(\d+)()\s?\(?(\w*)\)?:') - view.settings().set("result_base_dir", self.source_dir) + view.settings().set("result_base_dir", self.cmake.source_folder) view.set_syntax_file( "Packages/CMakeBuilder/Syntax/Configure.sublime-syntax") window.run_command("show_panel", {"panel": "output.cmake.configure"}) - self.send_dict({"type": "configure", "cacheArguments": cache_arguments}) + 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"}) @@ -147,6 +285,7 @@ def receive_reply(self, thedict): sublime.active_window().status_message( "CMake server {}.{} at your service!" .format(self.protocol["major"], self.protocol["minor"])) + self.configure() elif reply == "setGlobalSettings": sublime.active_window().status_message( "Global CMake setting is modified") @@ -155,6 +294,7 @@ def receive_reply(self, thedict): elif reply == "compute": sublime.active_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": @@ -191,14 +331,45 @@ def on_done_input(new_value): window.show_quick_panel(self.items, on_done) elif reply == "codemodel": configurations = thedict.pop("configurations") + self.include_paths = set() + self.targets = [] for config in configurations: name = config.pop("name") projects = config.pop("projects") for project in projects: targets = project.pop("targets") for target in targets: - if target["type"] == "EXECUTABLE": - print(target["fullName"], target["buildDirectory"]) + 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.append((target_name, target_fullname, target_type, target_dir)) + if target_type == "EXECUTABLE": + self.targets.append(("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) + data = self.cmake.window.project_data() + build_systems = data["build_systems"] + found = False + for build_system in build_systems: + if build_system.get("name", "") == "CMake": + build_system["target"] = "cmake_build" + found = True + break + if not found: + build_systems.append({"name": "CMake", "target": "cmake_build"}) + data["settings"]["compile_commands"] = self.cmake.build_folder_pre_expansion + data["settings"]["ecc_flag_sources"] = [{"file": "compile_commands.json", "search_in": self.cmake.build_folder_pre_expansion}] + data["build_systems"] = build_systems + self.cmake.window.set_project_data(data) elif reply == "cache": cache = thedict.pop("cache") self.items = [] @@ -206,7 +377,10 @@ def on_done_input(new_value): t = item["type"] if t in ("INTERNAL", "STATIC"): continue - docstring = item["properties"]["HELPSTRING"] + try: + docstring = item["properties"]["HELPSTRING"] + except Exception as e: + docstring = "" key = item["key"] value = item["value"] self.items.append([key + " [" + t.lower() + "]", value, docstring]) @@ -217,7 +391,7 @@ def on_done(index): key = item[0].split(" ")[0] old_value = item[1] def on_done_input(new_value): - self.configure(["-D{}={}".format(key, new_value)]) + self.configure({key: value}) sublime.active_window().show_input_panel( 'new value for "' + key + '": ', old_value, @@ -232,8 +406,9 @@ def receive_error(self, thedict): reply = thedict["inReplyTo"] msg = thedict["errorMessage"] if reply in ("configure", "compute"): - sublime.active_window().status_message(msg) + if self.is_configuring: + self.is_configuring = False else: sublime.error_message("{} (in reply to {})".format(msg, reply)) From d470ca656eaa47db45bb37cbc6ce8011e0940f9d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 3 Jul 2017 18:09:54 +0200 Subject: [PATCH 05/64] Add settings.py file, but this is probably deprecated already --- support/settings.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 support/settings.py diff --git a/support/settings.py b/support/settings.py new file mode 100644 index 0000000..60d41ff --- /dev/null +++ b/support/settings.py @@ -0,0 +1,41 @@ +import sublime +import CMakeBuilder.generators + +class Settings(object): + """Stores the settings defined in the project file. Base class for the + CMakeGenerator class.""" + + @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()) + generator_class = CMakeBuilder.generators.class_from_generator_string(generator_str) + return generator_class(window, data) + + def __init__(self, window): + super(CmakeSettings, self).__init__() + data = window.project_data()["settings"]["cmake"] + self.build_folder_pre = 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.source_folder = os.path.dirname(window.project_file_name()) + while os.path.isfile(os.path.join(self.source_folder, "..", "CMakeLists.txt")): + self.source_folder = os.path.join(self.source_folder, "..") + self.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", {}) + + def _pop(self, data, key, default=None): + return data.get(key, default) From d32794e3f7e6c8223e218d331aa6088c07c3593e Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 4 Jul 2017 16:31:29 +0200 Subject: [PATCH 06/64] Further improvements for server functionality I have to start working on a way to integrate the server functionality transparently for existing users --- CMakeBuilder.sublime-commands | 8 +++++ generators/__init__.py | 2 +- server.py | 68 ++++++++++++++++++++++------------- support/settings.py | 41 --------------------- 4 files changed, 52 insertions(+), 67 deletions(-) delete mode 100644 support/settings.py diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index 4b7ba6b..8646596 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -30,5 +30,13 @@ { "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" } ] diff --git a/generators/__init__.py b/generators/__init__.py index 9077c57..a958c5d 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -108,7 +108,7 @@ def shell_cmd(self): def cmd(self, target=None): result = ["cmake", "--build", "."] if target: - result.extend(["--target", target[0]]) + result.extend(["--target", target.name]) return result def syntax(self): diff --git a/server.py b/server.py index b37e382..45a6f60 100644 --- a/server.py +++ b/server.py @@ -18,13 +18,13 @@ def is_enabled(self): class CmakeBuildCommand(CmakeCommand): - def run(self): + def run(self, select=False): if not self.is_enabled(): sublime.error_message("Cannot build a CMake target!") return active_target = self.window.project_data().get("settings", {}).get("active_target", None) - if active_target is None: - self.items = [ [t[0], t[2], t[3]] for t in self.server.targets ] + if select or active_target is None: + 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) @@ -33,15 +33,15 @@ def _on_done(self, index): if index == -1: return target = self.server.targets[index] - if target[2] == "RUN": + if target.type == "RUN": if sublime.platform() in ("linux", "osx"): prefix = "./" else: prefix = "" self.window.run_command( "exec", { - "shell_cmd": prefix + target[1], - "working_dir": target[3] + "shell_cmd": prefix + target.fullname, + "working_dir": target.directory } ) else: @@ -54,7 +54,6 @@ def _on_done(self, index): } ) - class CmakeConfigure2Command(CmakeCommand): def run(self): @@ -73,17 +72,33 @@ def run(self): class CmakeSetTarget(CmakeCommand): def run(self): - self.items = [ [t[0], t[2], t[3]] for t in self.server.targets ] + self.items = [ [t.name, t.type, t.directory] for t in self.server.targets ] self.window.show_quick_panel(self.items, self._on_done) def _on_done(self, index): data = self.window.project_data() if index == -1: data.get("settings", {}).pop("active_target", None) + self.window.active_view().erase_status("cmake_active_target") else: data["settings"]["active_target"] = index + name = self.server.targets[index].name + self.window.active_view().set_status("cmake_active_target", "TARGET: " + name) self.window.set_project_data(data) +class Target(object): + + __slots__ = ("name", "fullname", "type", "directory") + + def __init__(self, name, fullname, type, directory): + self.name = name + self.fullname = fullname + self.type = type + self.directory = directory + + def __hash__(self): + return hash(self.name) + class ServerManager(sublime_plugin.EventListener): _servers = {} @@ -110,29 +125,31 @@ def on_load(self, view): elif str(server.cmake) != str(cmake): self.__class__._servers[window_id] = Server(cmake) - on_activated = on_clone = on_load - - -class TargetDisplayer(sublime_plugin.EventListener): - - def on_load(self, view): - view.settings().add_on_change("active_target", lambda: self.check(view)) - - def check(self, view): - t = view.settings().get("active_target", None) - if not t: + def on_activated(self, view): + self.on_load(view) + index = view.settings().get("active_target", None) + if not index: view.erase_status("cmake_active_target") return - server = ServerManager.get(view.window()) + server = self.__class__.get(view.window()) if not server: view.erase_status("cmake_active_target") return if not server.targets: view.erase_status("cmake_active_target") return - view.set_status("cmake_active_target", "TARGET: " + server.targets[int(t)][0]) + view.set_status("cmake_active_target", "TARGET: " + server.targets[int(index)].name) + + on_clone = on_load + + def on_window_command(self, window, command_name, command_args): + if command_name != "build" or command_args != {"select": True}: + return None + server = ServerManager.get(window) + if not server: + return None + return ("cmake_build", command_args) - on_activated = check class Server(Default.exec.ProcessListener): @@ -332,7 +349,7 @@ def on_done_input(new_value): elif reply == "codemodel": configurations = thedict.pop("configurations") self.include_paths = set() - self.targets = [] + self.targets = set() for config in configurations: name = config.pop("name") projects = config.pop("projects") @@ -346,9 +363,9 @@ def on_done_input(new_value): except KeyError as e: target_fullname = target_name target_dir = target.pop("buildDirectory") - self.targets.append((target_name, target_fullname, target_type, target_dir)) + self.targets.add(Target(target_name, target_fullname, target_type, target_dir)) if target_type == "EXECUTABLE": - self.targets.append(("Run: " + target_name, target_fullname, "RUN", target_dir)) + 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", []) @@ -357,6 +374,7 @@ def on_done_input(new_value): if path: self.include_paths.add(path) data = self.cmake.window.project_data() + self.targets = list(self.targets) build_systems = data["build_systems"] found = False for build_system in build_systems: diff --git a/support/settings.py b/support/settings.py deleted file mode 100644 index 60d41ff..0000000 --- a/support/settings.py +++ /dev/null @@ -1,41 +0,0 @@ -import sublime -import CMakeBuilder.generators - -class Settings(object): - """Stores the settings defined in the project file. Base class for the - CMakeGenerator class.""" - - @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()) - generator_class = CMakeBuilder.generators.class_from_generator_string(generator_str) - return generator_class(window, data) - - def __init__(self, window): - super(CmakeSettings, self).__init__() - data = window.project_data()["settings"]["cmake"] - self.build_folder_pre = 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.source_folder = os.path.dirname(window.project_file_name()) - while os.path.isfile(os.path.join(self.source_folder, "..", "CMakeLists.txt")): - self.source_folder = os.path.join(self.source_folder, "..") - self.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", {}) - - def _pop(self, data, key, default=None): - return data.get(key, default) From 01fff4dc130ce8a589759222fe8d1770ecf28e0d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 4 Jul 2017 22:18:35 +0200 Subject: [PATCH 07/64] Almost done with server functionality The changes should be transparent to old and new users now. Except for the fact that we don't check for the cmake capabilities yet. --- commands/__init__.py | 17 +-- commands/build.py | 59 +++++++++ commands/command.py | 68 +++++++++++ commands/configure.py | 5 + commands/configure2.py | 7 ++ commands/edit_cache.py | 5 + commands/reveal_include_directories.py | 12 ++ commands/set_target.py | 22 ++++ event_listeners/__init__.py | 2 - event_listeners/configure_on_save.py | 4 +- server.py | 162 +++---------------------- 11 files changed, 200 insertions(+), 163 deletions(-) create mode 100644 commands/build.py create mode 100644 commands/command.py create mode 100644 commands/configure2.py create mode 100644 commands/reveal_include_directories.py create mode 100644 commands/set_target.py diff --git a/commands/__init__.py b/commands/__init__.py index d9bd733..82dc7b8 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,21 +1,14 @@ +from .build import CmakeBuildCommand, CmakeExecCommand from .clear_cache import CmakeClearCacheCommand from .configure import CmakeConfigureCommand +from .configure2 import CmakeConfigure2Command from .diagnose import CmakeDiagnoseCommand 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_target import CmakeSetTargetCommand from .write_build_targets import CmakeWriteBuildTargetsCommand - -__all__ = [ - 'CmakeClearCacheCommand' - , 'CmakeConfigureCommand' - , 'CmakeDiagnoseCommand' - , 'CmakeEditCacheCommand' - , 'CmakeInsertDiagnosisCommand' - , 'CmakeNewProjectCommand' - , 'CmakeOpenBuildFolderCommand' - , 'CmakeRunCtestCommand' - , 'CmakeWriteBuildTargetsCommand' -] +from .command import ServerManager diff --git a/commands/build.py b/commands/build.py new file mode 100644 index 0000000..e50365c --- /dev/null +++ b/commands/build.py @@ -0,0 +1,59 @@ +import sublime +import Default.exec +from .command import CmakeCommand, ServerManager + +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=False): + if not self.is_enabled(): + sublime.error_message("Cannot build a CMake target!") + return + active_target = self.window.project_data().get("settings", {}).get("active_target", None) + if select or active_target is None: + 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): + self.window.run_command("cmake_set_target", {"index": index}) + if index == -1: + return + target = self.server.targets[index] + if target.type == "RUN": + if sublime.platform() in ("linux", "osx"): + prefix = "./" + else: + prefix = "" + self.window.run_command( + "cmake_exec", { + "window_id": self.window.id(), + "shell_cmd": prefix + target.fullname, + "working_dir": target.directory + } + ) + else: + self.window.run_command( + "cmake_exec", { + "window_id": self.window.id(), + "cmd": self.cmake.cmd(target), + "file_regex": self.cmake.file_regex(), + "syntax": self.cmake.syntax(), + "working_dir": self.cmake.build_folder + } + ) diff --git a/commands/command.py b/commands/command.py new file mode 100644 index 0000000..8f5e847 --- /dev/null +++ b/commands/command.py @@ -0,0 +1,68 @@ +from ..generators import CMakeGenerator +from ..server import Server +import sublime_plugin + + +class CmakeCommand(sublime_plugin.WindowCommand): + + def is_enabled(self): + try: + self.cmake = CMakeGenerator.create(self.window) + except Exception as e: + return False + self.server = ServerManager.get(self.window) + return self.server is not None and super(sublime_plugin.WindowCommand, self).is_enabled() + + +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 on_load(self, view): + try: + window_id = view.window().id() + cmake = CMakeGenerator.create(view.window()) + except KeyError as e: + return + except AttributeError as e: + return + server = self.__class__._servers.get(window_id, None) + if not server: + try: + self.__class__._servers[window_id] = Server(cmake) + except Exception as e: + print(str(e)) + return + elif str(server.cmake) != str(cmake): + self.__class__._servers[window_id] = Server(cmake) + + def on_activated(self, view): + self.on_load(view) + index = view.settings().get("active_target", None) + if not index: + view.erase_status("cmake_active_target") + return + server = self.__class__.get(view.window()) + if not server: + view.erase_status("cmake_active_target") + return + if not server.targets: + view.erase_status("cmake_active_target") + return + view.set_status("cmake_active_target", "TARGET: " + server.targets[int(index)].name) + + on_clone = on_load + + def on_window_command(self, window, command_name, command_args): + if command_name != "build" or command_args != {"select": True}: + return None + server = ServerManager.get(window) + if not server: + return None + return ("cmake_build", command_args) diff --git a/commands/configure.py b/commands/configure.py index a6f8031..e319668 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 @@ -23,6 +24,10 @@ 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() diff --git a/commands/configure2.py b/commands/configure2.py new file mode 100644 index 0000000..8d85b1c --- /dev/null +++ b/commands/configure2.py @@ -0,0 +1,7 @@ +from .command import CmakeCommand + + +class CmakeConfigure2Command(CmakeCommand): + + def run(self): + self.server.configure(self.cmake.command_line_overrides) diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 32c43e4..9ec83d2 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -1,5 +1,6 @@ import sublime, sublime_plugin, os from CMakeBuilder.support import * +from .command import ServerManager class CmakeEditCacheCommand(sublime_plugin.WindowCommand): """Edit an entry from the CMake cache.""" @@ -15,6 +16,10 @@ def description(self): return 'Edit Cache...' def run(self): + self.server = ServerManager.get(self.window) + if self.server: + self.server.cache() + return 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")) diff --git a/commands/reveal_include_directories.py b/commands/reveal_include_directories.py new file mode 100644 index 0000000..80932c1 --- /dev/null +++ b/commands/reveal_include_directories.py @@ -0,0 +1,12 @@ +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}) diff --git a/commands/set_target.py b/commands/set_target.py new file mode 100644 index 0000000..de21ed6 --- /dev/null +++ b/commands/set_target.py @@ -0,0 +1,22 @@ +from .command import CmakeCommand + + +class CmakeSetTargetCommand(CmakeCommand): + + def run(self, index=None): + if 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): + data = self.window.project_data() + if index == -1: + data.get("settings", {}).pop("active_target", None) + self.window.active_view().erase_status("cmake_active_target") + else: + data["settings"]["active_target"] = index + name = self.server.targets[index].name + self.window.active_view().set_status("cmake_active_target", "TARGET: " + name) + self.window.set_project_data(data) diff --git a/event_listeners/__init__.py b/event_listeners/__init__.py index 7d77bb9..8ab78a0 100644 --- a/event_listeners/__init__.py +++ b/event_listeners/__init__.py @@ -1,3 +1 @@ from .configure_on_save import ConfigureOnSave - -__all__ = ['ConfigureOnSave'] diff --git a/event_listeners/configure_on_save.py b/event_listeners/configure_on_save.py index 796dd4b..35ad35e 100644 --- a/event_listeners/configure_on_save.py +++ b/event_listeners/configure_on_save.py @@ -1,8 +1,8 @@ import sublime import sublime_plugin import functools -from CMakeBuilder.support import get_setting -from CMakeBuilder.commands import CmakeConfigureCommand +from ..support import get_setting +from ..commands import CmakeConfigure2Command def _configure(window): if not CmakeConfigure2Command(window).is_enabled(): return diff --git a/server.py b/server.py index 45a6f60..edb07e1 100644 --- a/server.py +++ b/server.py @@ -1,90 +1,8 @@ import Default.exec import json import sublime -import sublime_plugin -import os import copy -import CMakeBuilder.generators -class CmakeCommand(sublime_plugin.WindowCommand): - - def is_enabled(self): - try: - self.cmake = CMakeBuilder.generators.CMakeGenerator.create(self.window) - except Exception as e: - return False - self.server = ServerManager.get(self.window) - return self.server is not None and super(sublime_plugin.WindowCommand, self).is_enabled() - -class CmakeBuildCommand(CmakeCommand): - - def run(self, select=False): - if not self.is_enabled(): - sublime.error_message("Cannot build a CMake target!") - return - active_target = self.window.project_data().get("settings", {}).get("active_target", None) - if select or active_target is None: - 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 index == -1: - return - target = self.server.targets[index] - if target.type == "RUN": - if sublime.platform() in ("linux", "osx"): - prefix = "./" - else: - prefix = "" - self.window.run_command( - "exec", { - "shell_cmd": prefix + target.fullname, - "working_dir": target.directory - } - ) - else: - self.window.run_command( - "exec", { - "cmd": self.cmake.cmd(target), - "file_regex": self.cmake.file_regex(), - "syntax": self.cmake.syntax(), - "working_dir": self.cmake.build_folder - } - ) - -class CmakeConfigure2Command(CmakeCommand): - - def run(self): - self.server.configure(self.cmake.command_line_overrides) - -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}) - -class CmakeSetTarget(CmakeCommand): - - def run(self): - self.items = [ [t.name, t.type, t.directory] for t in self.server.targets ] - self.window.show_quick_panel(self.items, self._on_done) - - def _on_done(self, index): - data = self.window.project_data() - if index == -1: - data.get("settings", {}).pop("active_target", None) - self.window.active_view().erase_status("cmake_active_target") - else: - data["settings"]["active_target"] = index - name = self.server.targets[index].name - self.window.active_view().set_status("cmake_active_target", "TARGET: " + name) - self.window.set_project_data(data) class Target(object): @@ -99,57 +17,6 @@ def __init__(self, name, fullname, type, directory): def __hash__(self): return hash(self.name) -class ServerManager(sublime_plugin.EventListener): - - _servers = {} - - @classmethod - def get(cls, window): - return cls._servers.get(window.id(), None) - - def on_load(self, view): - try: - window_id = view.window().id() - cmake = CMakeBuilder.generators.CMakeGenerator.create(view.window()) - except KeyError as e: - return - except AttributeError as e: - return - server = self.__class__._servers.get(window_id, None) - if not server: - try: - self.__class__._servers[window_id] = Server(cmake) - except Exception as e: - print(str(e)) - return - elif str(server.cmake) != str(cmake): - self.__class__._servers[window_id] = Server(cmake) - - def on_activated(self, view): - self.on_load(view) - index = view.settings().get("active_target", None) - if not index: - view.erase_status("cmake_active_target") - return - server = self.__class__.get(view.window()) - if not server: - view.erase_status("cmake_active_target") - return - if not server.targets: - view.erase_status("cmake_active_target") - return - view.set_status("cmake_active_target", "TARGET: " + server.targets[int(index)].name) - - on_clone = on_load - - def on_window_command(self, window, command_name, command_args): - if command_name != "build" or command_args != {"select": True}: - return None - server = ServerManager.get(window) - if not server: - return None - return ("cmake_build", command_args) - class Server(Default.exec.ProcessListener): @@ -163,6 +30,7 @@ def __init__(self, self.experimental = experimental self.protocol = protocol self.is_configuring = False + self.is_building = False # maintained by CmakeBuildCommand self.data_parts = '' self.inside_json_object = False self.include_paths = set() @@ -197,7 +65,7 @@ def on_data(self, _, data): self.inside_json_object = True def on_finished(self, _): - sublime.active_window().status_message( + self.cmake.window.status_message( "CMake Server has quit (exit code {})" .format(self.proc.exit_code())) @@ -241,7 +109,7 @@ def configure(self, cache_arguments={}): if self.is_configuring: return self.is_configuring = True - window = sublime.active_window() + window = self.cmake.window view = window.create_output_panel("cmake.configure", True) view.settings().set( "result_file_regex", @@ -299,17 +167,17 @@ def receive_dict(self, thedict): def receive_reply(self, thedict): reply = thedict["inReplyTo"] if reply == "handshake": - sublime.active_window().status_message( + self.cmake.window.status_message( "CMake server {}.{} at your service!" .format(self.protocol["major"], self.protocol["minor"])) self.configure() elif reply == "setGlobalSettings": - sublime.active_window().status_message( + self.cmake.window.status_message( "Global CMake setting is modified") elif reply == "configure": - sublime.active_window().status_message("Project is configured") + self.cmake.window.status_message("Project is configured") elif reply == "compute": - sublime.active_window().status_message("Project is generated") + self.cmake.window.status_message("Project is generated") self.is_configuring = False self.codemodel() elif reply == "fileSystemWatchers": @@ -328,7 +196,7 @@ def receive_reply(self, thedict): continue self.items.append([str(k), str(v)]) self.types.append(type(v)) - window = sublime.active_window() + window = self.cmake.window def on_done(index): if index == -1: return @@ -410,13 +278,13 @@ def on_done(index): old_value = item[1] def on_done_input(new_value): self.configure({key: value}) - sublime.active_window().show_input_panel( + self.cmake.window.show_input_panel( 'new value for "' + key + '": ', old_value, on_done_input, None, None) - sublime.active_window().show_quick_panel(self.items, on_done) + self.cmake.window.show_quick_panel(self.items, on_done) else: print("received unknown reply type:", reply) @@ -424,14 +292,14 @@ def receive_error(self, thedict): reply = thedict["inReplyTo"] msg = thedict["errorMessage"] if reply in ("configure", "compute"): - sublime.active_window().status_message(msg) + self.cmake.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 = sublime.active_window().active_view() + view = self.cmake.window.active_view() minimum = thedict["progressMinimum"] maximum = thedict["progressMaximum"] current = thedict["progressCurrent"] @@ -446,7 +314,7 @@ def receive_progress(self, thedict): view.set_status("cmake_" + thedict["inReplyTo"], status) def receive_message(self, thedict): - window = sublime.active_window() + window = self.cmake.window if thedict["inReplyTo"] in ("configure", "compute"): name = "cmake.configure" else: @@ -461,14 +329,14 @@ def receive_message(self, thedict): "scroll_to_end": True}) def receive_signal(self, thedict): - if thedict["name"] == "dirty" and not self.is_configuring: + 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 = sublime.active_window().new_file() + view = self.cmake.window.new_file() view.set_scratch(True) view.set_name(name) thedict.pop("type") From 9b060e7c3dba6e2d54d3b3c1ea0926eee034276e Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Thu, 6 Jul 2017 12:54:50 +0200 Subject: [PATCH 08/64] Make this server thing somewhat stable Some problems are still present: The "active target" seems to be non-deterministic when restoring a session. This is probably due to how Python's set datastructure works. Another thing is that the output dir of some targets are not correct. This is a bug in cmake itself, so I can't fix that. But keep it in mind (note to self). What else? I've noticed that sometimes the decoding doesn't work and an exception gets thrown. In this case the server object gets into an unusable state and you'd have to restart Sublime to get it working again. I should probably add better exception handling so that the server object can resume operation as if nothing happened. --- CMakeBuilder.sublime-commands | 8 ++++++++ Main.sublime-menu | 14 ++++++++++++++ commands/__init__.py | 2 ++ commands/build.py | 2 +- commands/command.py | 8 ++++++-- commands/configure.py | 4 ++-- commands/dump_file_system_watchers.py | 11 +++++++++++ commands/dump_inputs.py | 11 +++++++++++ commands/reveal_include_directories.py | 3 +++ commands/set_target.py | 3 +++ generators/__init__.py | 2 +- generators/windows/NMake_Makefiles.py | 2 +- generators/windows/Ninja.py | 2 +- generators/windows/Visual_Studio.py | 2 +- server.py | 14 +++----------- support/__init__.py | 1 + support/has_server_mode.py | 17 +++++++++++++++++ 17 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 commands/dump_file_system_watchers.py create mode 100644 commands/dump_inputs.py create mode 100644 support/has_server_mode.py diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index 8646596..cf092ec 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -38,5 +38,13 @@ { "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" } ] diff --git a/Main.sublime-menu b/Main.sublime-menu index cf1ae3b..90610e7 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -42,6 +42,20 @@ { "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" } ] } diff --git a/commands/__init__.py b/commands/__init__.py index 82dc7b8..b0f564c 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -3,6 +3,8 @@ 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 diff --git a/commands/build.py b/commands/build.py index e50365c..815e630 100644 --- a/commands/build.py +++ b/commands/build.py @@ -51,7 +51,7 @@ def _on_done(self, index): self.window.run_command( "cmake_exec", { "window_id": self.window.id(), - "cmd": self.cmake.cmd(target), + "cmd": self.cmake.cmd(None if target.type == "ALL" else target), "file_regex": self.cmake.file_regex(), "syntax": self.cmake.syntax(), "working_dir": self.cmake.build_folder diff --git a/commands/command.py b/commands/command.py index 8f5e847..0e14360 100644 --- a/commands/command.py +++ b/commands/command.py @@ -1,6 +1,7 @@ +import sublime_plugin from ..generators import CMakeGenerator from ..server import Server -import sublime_plugin +from ..support import has_server_mode class CmakeCommand(sublime_plugin.WindowCommand): @@ -25,6 +26,9 @@ def get(cls, window): return cls._servers.get(window.id(), None) def on_load(self, view): + if not has_server_mode(): + print("cmake is not capable of server mode") + return try: window_id = view.window().id() cmake = CMakeGenerator.create(view.window()) @@ -60,7 +64,7 @@ def on_activated(self, view): on_clone = on_load def on_window_command(self, window, command_name, command_args): - if command_name != "build" or command_args != {"select": True}: + if command_name != "build": return None server = ServerManager.get(window) if not server: diff --git a/commands/configure.py b/commands/configure.py index e319668..4f5e746 100644 --- a/commands/configure.py +++ b/commands/configure.py @@ -86,7 +86,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))) @@ -108,7 +108,7 @@ 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, diff --git a/commands/dump_file_system_watchers.py b/commands/dump_file_system_watchers.py new file mode 100644 index 0000000..c7984f0 --- /dev/null +++ b/commands/dump_file_system_watchers.py @@ -0,0 +1,11 @@ +from .command import CmakeCommand + + +class CmakeDumpFileSystemWatchersCommand(CmakeCommand): + """Prints the watched files to a new view""" + + def run(self): + self.server.file_system_watchers() + + def description(self): + return "Dump File System Watchers" diff --git a/commands/dump_inputs.py b/commands/dump_inputs.py new file mode 100644 index 0000000..d6cbb3f --- /dev/null +++ b/commands/dump_inputs.py @@ -0,0 +1,11 @@ +from .command import CmakeCommand + + +class CmakeDumpInputsCommand(CmakeCommand): + """Prints the cmake inputs to a new view""" + + def run(self): + self.server.cmake_inputs() + + def description(self): + return "Dump CMake Inputs" diff --git a/commands/reveal_include_directories.py b/commands/reveal_include_directories.py index 80932c1..6e81953 100644 --- a/commands/reveal_include_directories.py +++ b/commands/reveal_include_directories.py @@ -10,3 +10,6 @@ def run(self): view.set_scratch(True) for path in self.server.include_paths: view.run_command("append", {"characters": path + "\n", "force": True}) + + def description(self): + return "Reveal Include Directories" diff --git a/commands/set_target.py b/commands/set_target.py index de21ed6..e6261e5 100644 --- a/commands/set_target.py +++ b/commands/set_target.py @@ -20,3 +20,6 @@ def _on_done(self, index): name = self.server.targets[index].name self.window.active_view().set_status("cmake_active_target", "TARGET: " + name) self.window.set_project_data(data) + + def description(self): + return "Set Target..." diff --git a/generators/__init__.py b/generators/__init__.py index a958c5d..aa55127 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -62,7 +62,7 @@ def _pop(self, data, key, default=None): def __repr__(self): return repr(type(self)) - def env(self): + def get_env(self): return {} # Empty dict def variants(self): diff --git a/generators/windows/NMake_Makefiles.py b/generators/windows/NMake_Makefiles.py index 12f3755..3813e33 100644 --- a/generators/windows/NMake_Makefiles.py +++ b/generators/windows/NMake_Makefiles.py @@ -9,7 +9,7 @@ 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: diff --git a/generators/windows/Ninja.py b/generators/windows/Ninja.py index f0118dc..1c1683b 100644 --- a/generators/windows/Ninja.py +++ b/generators/windows/Ninja.py @@ -9,7 +9,7 @@ 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: diff --git a/generators/windows/Visual_Studio.py b/generators/windows/Visual_Studio.py index 119fdd6..44536a2 100644 --- a/generators/windows/Visual_Studio.py +++ b/generators/windows/Visual_Studio.py @@ -85,7 +85,7 @@ def syntax(self): def file_regex(self): return r'^ (.+)\((\d+)\)(): ((?:fatal )?(?:error|warning) \w+\d\d\d\d: .*) \[.*$' - def env(self): + def get_env(self): if self.visual_studio_versions: vs_versions = self.visual_studio_versions else: diff --git a/server.py b/server.py index edb07e1..4b8cfdc 100644 --- a/server.py +++ b/server.py @@ -34,6 +34,7 @@ def __init__(self, self.data_parts = '' self.inside_json_object = False self.include_paths = set() + self.targets = None cmd = ["cmake", "-E", "server"] if experimental: cmd.append("--experimental") @@ -241,20 +242,11 @@ def on_done_input(new_value): 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)) data = self.cmake.window.project_data() self.targets = list(self.targets) - build_systems = data["build_systems"] - found = False - for build_system in build_systems: - if build_system.get("name", "") == "CMake": - build_system["target"] = "cmake_build" - found = True - break - if not found: - build_systems.append({"name": "CMake", "target": "cmake_build"}) data["settings"]["compile_commands"] = self.cmake.build_folder_pre_expansion - data["settings"]["ecc_flag_sources"] = [{"file": "compile_commands.json", "search_in": self.cmake.build_folder_pre_expansion}] - data["build_systems"] = build_systems + data["settings"]["ecc_flags_sources"] = [{"file": "compile_commands.json", "search_in": self.cmake.build_folder_pre_expansion}] self.cmake.window.set_project_data(data) elif reply == "cache": cache = thedict.pop("cache") diff --git a/support/__init__.py b/support/__init__.py index f9b743b..2bd704c 100644 --- a/support/__init__.py +++ b/support/__init__.py @@ -2,3 +2,4 @@ from .expand_variables import * from .get_cmake_value import * from .get_setting import * +from .has_server_mode import has_server_mode diff --git a/support/has_server_mode.py b/support/has_server_mode.py new file mode 100644 index 0000000..c91584d --- /dev/null +++ b/support/has_server_mode.py @@ -0,0 +1,17 @@ +from .check_output import check_output +import json + +_server_mode = None + +def has_server_mode(): + global _server_mode + if _server_mode is None: + try: + output = check_output("cmake -E capabilities") + except Exception as e: + print("CMakeBuilder: Error: Could not load cmake's capabilities") + _server_mode = False + else: + output = json.loads(output) + _server_mode = output.get("serverMode", False) + return _server_mode From cf1d976cc510854301999c054b2409b54b378e4c Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sat, 15 Jul 2017 22:10:06 +0200 Subject: [PATCH 09/64] Integer parse fix for visual studio generator --- generators/windows/Visual_Studio.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/generators/windows/Visual_Studio.py b/generators/windows/Visual_Studio.py index 44536a2..9488d1b 100644 --- a/generators/windows/Visual_Studio.py +++ b/generators/windows/Visual_Studio.py @@ -32,12 +32,17 @@ def __repr__(self): vs_versions = [15.0, 14.1, 14.0, 13.0, 12.0, 11.0, 10.0] for version in vs_versions: if version in years: - if version.is_integer(): - result += ' %i %i' % (int(version), years[version]) - else: - result += ' %0.1f %i' % (version, years[version]) - ok = True - break + if isinstance(version, int): + result += ' %i %i' % (version, years[version]) + ok = True + break + elif isinstance(version, float): + if version.is_integer(): + result += ' %i %i' % (int(version), years[version]) + else: + result += ' %0.1f %i' % (version, years[version]) + ok = True + break if not ok: raise Exception('Could not determine Visual Studio version!') if self.target_architecture: From acb8813b87298401d1faa17a0ab2b602fabb0221 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 18 Jul 2017 15:31:19 +0200 Subject: [PATCH 10/64] Add check for missing targets in build command Also, normalize paths for source folder and build folder. --- commands/build.py | 2 ++ generators/__init__.py | 17 +++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/commands/build.py b/commands/build.py index 815e630..fef2ce9 100644 --- a/commands/build.py +++ b/commands/build.py @@ -25,6 +25,8 @@ def run(self, select=False): return active_target = self.window.project_data().get("settings", {}).get("active_target", None) if select 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: diff --git a/generators/__init__.py b/generators/__init__.py index aa55127..6c3ed17 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -30,9 +30,12 @@ def __init__(self, window): 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("\\", "/") self.source_folder = os.path.dirname(window.project_file_name()) 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", []) @@ -40,20 +43,6 @@ def __init__(self, window): 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.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): From 28d770a837d6add6a64df38432077308810a71a5 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 18 Jul 2017 15:32:48 +0200 Subject: [PATCH 11/64] Add check for self.proc in __del__ of Server --- server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 4b8cfdc..71efde2 100644 --- a/server.py +++ b/server.py @@ -47,7 +47,8 @@ def __init__(self, env=env) def __del__(self): - self.proc.kill() + if self.proc: + self.proc.kill() def on_data(self, _, data): data = data.decode("utf-8").strip() From 047c9766e68846f2c036b0a4778d540cf56ad94d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 18 Jul 2017 17:48:42 +0200 Subject: [PATCH 12/64] Replace ad-hoc check_output with library check_output --- generators/windows/Visual_Studio.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/generators/windows/Visual_Studio.py b/generators/windows/Visual_Studio.py index 9488d1b..96ecab6 100644 --- a/generators/windows/Visual_Studio.py +++ b/generators/windows/Visual_Studio.py @@ -1,5 +1,6 @@ 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 @@ -12,9 +13,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) From f1ed406178c5e3c9a6ff6e274d61bb29bb57a365 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 18 Jul 2017 18:04:20 +0200 Subject: [PATCH 13/64] Enable "set global setting" in the UI --- CMakeBuilder.sublime-commands | 4 ++++ Main.sublime-menu | 3 +++ commands/__init__.py | 1 + 3 files changed, 8 insertions(+) diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index cf092ec..ee5f03a 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -46,5 +46,9 @@ { "command": "cmake_dump_inputs", "caption": "CMakeBuilder: Dump CMake Inputs" + }, + { + "command": "cmake_set_global_setting", + "caption": "CMakeBuilder: Set Global Setting" } ] diff --git a/Main.sublime-menu b/Main.sublime-menu index bbac04b..d5e98e6 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -56,6 +56,9 @@ }, { "command": "cmake_dump_inputs" + }, + { + "command": "cmake_set_global_setting" } ] } diff --git a/commands/__init__.py b/commands/__init__.py index b0f564c..f2ee50a 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -11,6 +11,7 @@ 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 from .command import ServerManager From eb82e816e4dece07aeebd7b9112156dacdb2d9bc Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Fri, 21 Jul 2017 15:08:07 +0200 Subject: [PATCH 14/64] Add set_global_setting.py, not sure how this file dissapeared --- commands/set_global_setting.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 commands/set_global_setting.py diff --git a/commands/set_global_setting.py b/commands/set_global_setting.py new file mode 100644 index 0000000..fbb3eca --- /dev/null +++ b/commands/set_global_setting.py @@ -0,0 +1,7 @@ +from .command import CmakeCommand + + +class CmakeSetGlobalSettingCommand(CmakeCommand): + + def run(self): + self.server.global_settings() From 519a83f90d7a4194cc9d2d9eed34f3660e3468ef Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 25 Jul 2017 13:17:01 +0200 Subject: [PATCH 15/64] Flake8 fixes --- __init__.py | 2 +- commands/command.py | 2 + event_listeners/__init__.py | 1 + event_listeners/configure_on_save.py | 13 ++-- generators/__init__.py | 63 ++++++++++++------ generators/linux/Ninja.py | 17 ++--- generators/linux/Unix_Makefiles.py | 21 +++--- generators/linux/__init__.py | 2 + generators/osx/Ninja.py | 19 +++--- generators/osx/Unix_Makefiles.py | 21 +++--- generators/osx/__init__.py | 2 + generators/windows/NMake_Makefiles.py | 27 ++++---- generators/windows/Ninja.py | 25 +++---- generators/windows/Visual_Studio.py | 19 ++++-- generators/windows/__init__.py | 3 + server.py | 95 +++++++++++++++++---------- support/__init__.py | 16 +++-- support/check_output.py | 8 ++- support/expand_variables.py | 1 + support/get_cmake_value.py | 1 + support/get_setting.py | 1 + support/has_server_mode.py | 1 + 22 files changed, 223 insertions(+), 137 deletions(-) diff --git a/__init__.py b/__init__.py index 7a16d39..54a8f65 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ __version__ = "1.0.0" -__version_info__ = (1,0,0) +__version_info__ = (1, 0, 0) from CMakeBuilder.commands import * from CMakeBuilder.event_listeners import * diff --git a/commands/command.py b/commands/command.py index 0e14360..2af6557 100644 --- a/commands/command.py +++ b/commands/command.py @@ -36,6 +36,8 @@ def on_load(self, view): return except AttributeError as e: return + except TypeError as e: + return server = self.__class__._servers.get(window_id, None) if not server: try: diff --git a/event_listeners/__init__.py b/event_listeners/__init__.py index 8ab78a0..8b73b9f 100644 --- a/event_listeners/__init__.py +++ b/event_listeners/__init__.py @@ -1 +1,2 @@ from .configure_on_save import ConfigureOnSave +__all__ = ["ConfigureOnSave"] diff --git a/event_listeners/configure_on_save.py b/event_listeners/configure_on_save.py index 35ad35e..342ea89 100644 --- a/event_listeners/configure_on_save.py +++ b/event_listeners/configure_on_save.py @@ -1,13 +1,14 @@ -import sublime import sublime_plugin -import functools from ..support import get_setting from ..commands import CmakeConfigure2Command + def _configure(window): - if not CmakeConfigure2Command(window).is_enabled(): return + if not CmakeConfigure2Command(window).is_enabled(): + return window.run_command("cmake_configure2") + class ConfigureOnSave(sublime_plugin.EventListener): def on_post_save(self, view): @@ -18,7 +19,7 @@ def on_post_save(self, view): name = view.file_name() if not name: return - if (name.endswith("CMakeLists.txt") or - name.endswith("CMakeCache.txt") or - name.endswith(".sublime-project")): + 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 6c3ed17..5b2a08c 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -2,7 +2,8 @@ import sys import os import glob -from CMakeBuilder.support import * +from CMakeBuilder.support import get_setting + class CMakeGenerator(object): @@ -18,7 +19,8 @@ def create(cls, window): elif sublime.platform() == "windows": generator_str = "Visual Studio" else: - raise AttributeError("unknown sublime platform: %s" % sublime.platform()) + raise AttributeError( + "unknown sublime platform: %s" % sublime.platform()) GeneratorClass = class_from_generator_string(generator_str) return GeneratorClass(window) @@ -30,18 +32,23 @@ def __init__(self, window): 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("\\", "/") + self.build_folder = os.path.abspath(self.build_folder)\ + .replace("\\", "/") self.source_folder = os.path.dirname(window.project_file_name()) - while os.path.isfile(os.path.join(self.source_folder, "..", "CMakeLists.txt")): + 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.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.target_architecture = self._pop( + data, "target_architecture", "x86") + self.visual_studio_versions = self._pop( + data, "visual_studio_versions", [15, 14]) self.window = window assert self.build_folder @@ -52,10 +59,10 @@ def __repr__(self): return repr(type(self)) def get_env(self): - return {} # Empty dict + return {} # Empty dict def variants(self): - return [] # Empty list + return [] # Empty list def on_pre_configure(self): pass @@ -72,11 +79,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() } @@ -115,15 +124,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 = [] @@ -132,6 +145,7 @@ 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': @@ -141,37 +155,49 @@ def class_from_generator_string(generator_string): 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': @@ -180,4 +206,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..774c049 100644 --- a/generators/linux/Ninja.py +++ b/generators/linux/Ninja.py @@ -2,6 +2,7 @@ import subprocess import sublime + class Ninja(CMakeGenerator): def __repr__(self): @@ -17,7 +18,7 @@ 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], @@ -32,24 +33,24 @@ def variants(self): sublime.error_message(errs) return 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..17bd0ca 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): @@ -21,7 +22,7 @@ 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], @@ -40,25 +41,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..23d5ede 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): @@ -17,7 +18,7 @@ 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], @@ -32,24 +33,24 @@ def variants(self): sublime.error_message(errs) return 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 +64,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..17bd0ca 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): @@ -21,7 +22,7 @@ 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], @@ -40,25 +41,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 3813e33..0c64fde 100644 --- a/generators/windows/NMake_Makefiles.py +++ b/generators/windows/NMake_Makefiles.py @@ -4,6 +4,7 @@ import sublime import subprocess + class NMake_Makefiles(CMakeGenerator): def __repr__(self): @@ -13,7 +14,7 @@ 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 get_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 1c1683b..94d045a 100644 --- a/generators/windows/Ninja.py +++ b/generators/windows/Ninja.py @@ -4,6 +4,7 @@ import sublime import subprocess + class Ninja(CMakeGenerator): def __repr__(self): @@ -13,7 +14,7 @@ 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 get_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 96ecab6..bf1dadf 100644 --- a/generators/windows/Visual_Studio.py +++ b/generators/windows/Visual_Studio.py @@ -6,6 +6,7 @@ import subprocess import sublime + class Visual_Studio(CMakeGenerator): def __repr__(self): @@ -70,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) @@ -87,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 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: @@ -103,7 +107,8 @@ def get_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/server.py b/server.py index 71efde2..e5f12e8 100644 --- a/server.py +++ b/server.py @@ -20,12 +20,12 @@ def __hash__(self): class Server(Default.exec.ProcessListener): - def __init__(self, - cmake_settings, - experimental=True, - debug=True, - protocol=(1,0), - env={}): + def __init__(self, + cmake_settings, + experimental=True, + debug=True, + protocol=(1, 0), + env={}): self.cmake = cmake_settings self.experimental = experimental self.protocol = protocol @@ -41,8 +41,8 @@ def __init__(self, if debug: cmd.append("--debug") self.proc = Default.exec.AsyncProcess( - cmd=cmd, - shell_cmd=None, + cmd=cmd, + shell_cmd=None, listener=self, env=env) @@ -84,16 +84,16 @@ def send_dict(self, thedict): def send_handshake(self): best_protocol = self.protocols[0] for protocol in self.protocols: - if (protocol["major"] == self.protocol[0] and - protocol["minor"] == self.protocol[1]): + if (protocol["major"] == self.protocol[0] and + protocol["minor"] == self.protocol[1]): best_protocol = protocol break if protocol["isExperimental"] and not self.experimental: continue if protocol["major"] > best_protocol["major"]: best_protocol = protocol - elif (protocol["major"] == best_protocol["major"] and - protocol["minor"] > best_protocol["minor"]): + elif (protocol["major"] == best_protocol["major"] and + protocol["minor"] > best_protocol["minor"]): best_protocol = protocol self.protocol = best_protocol self.send_dict({ @@ -114,8 +114,9 @@ def configure(self, cache_arguments={}): window = self.cmake.window view = window.create_output_panel("cmake.configure", True) view.settings().set( - "result_file_regex", - r'CMake\s(?:Error|Warning)(?:\s\(dev\))?\sat\s(.+):(\d+)()\s?\(?(\w*)\)?:') + "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") @@ -193,27 +194,29 @@ def receive_reply(self, thedict): thedict.pop("capabilities") self.items = [] self.types = [] - for k,v in thedict.items(): + 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.cmake.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, + 'new value for "' + key + '": ', + old_value, + on_done_input, + None, None) window.show_quick_panel(self.items, on_done) elif reply == "codemodel": @@ -221,7 +224,7 @@ def on_done_input(new_value): self.include_paths = set() self.targets = set() for config in configurations: - name = config.pop("name") + # name = config.pop("name") projects = config.pop("projects") for project in projects: targets = project.pop("targets") @@ -233,9 +236,18 @@ def on_done_input(new_value): 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)) + 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)) + 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", []) @@ -243,11 +255,19 @@ def on_done_input(new_value): 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.add( + Target( + "BUILD ALL", + "BUILD ALL", + "ALL", + self.cmake.build_folder)) data = self.cmake.window.project_data() self.targets = list(self.targets) - data["settings"]["compile_commands"] = self.cmake.build_folder_pre_expansion - data["settings"]["ecc_flags_sources"] = [{"file": "compile_commands.json", "search_in": self.cmake.build_folder_pre_expansion}] + data["settings"]["compile_commands"] = \ + self.cmake.build_folder_pre_expansion + data["settings"]["ecc_flags_sources"] = [{ + "file": "compile_commands.json", + "search_in": self.cmake.build_folder_pre_expansion}] self.cmake.window.set_project_data(data) elif reply == "cache": cache = thedict.pop("cache") @@ -262,21 +282,26 @@ def on_done_input(new_value): docstring = "" key = item["key"] value = item["value"] - self.items.append([key + " [" + t.lower() + "]", value, docstring]) + 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.cmake.window.show_input_panel( 'new value for "' + key + '": ', old_value, on_done_input, None, None) + self.cmake.window.show_quick_panel(self.items, on_done) else: print("received unknown reply type:", reply) @@ -302,7 +327,7 @@ def receive_progress(self, thedict): self.compute() else: status = "{0} {1:.0f}%".format( - thedict["progressMessage"], + thedict["progressMessage"], 100.0 * (float(current) / float(maximum - minimum))) view.set_status("cmake_" + thedict["inReplyTo"], status) @@ -316,13 +341,15 @@ def receive_message(self, thedict): assert view window.run_command("show_panel", {"panel": "output.{}".format(name)}) view = window.find_output_panel(name) - view.run_command("append", - {"characters": thedict["message"] + "\n", - "force": True, - "scroll_to_end": True}) - + view.run_command("append", { + "characters": thedict["message"] + "\n", + "force": True, + "scroll_to_end": True}) + def receive_signal(self, thedict): - if thedict["name"] == "dirty" and not self.is_configuring and not self.is_building: + if (thedict["name"] == "dirty" and not + self.is_configuring and not + self.is_building): self.configure() else: print("received signal") @@ -336,7 +363,7 @@ def dump_to_new_view(self, thedict, name): thedict.pop("inReplyTo") thedict.pop("cookie") view.run_command( - "append", + "append", {"characters": json.dumps(thedict, indent=2), "force": True}) view.set_read_only(True) view.set_syntax_file("Packages/JavaScript/JSON.sublime-syntax") diff --git a/support/__init__.py b/support/__init__.py index 2bd704c..8d01bcf 100644 --- a/support/__init__.py +++ b/support/__init__.py @@ -1,5 +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 .has_server_mode import has_server_mode + + +__all__ = [ + "check_output", + "expand_variables", + "get_cmake_value", + "get_setting", + "has_server_mode"] 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/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/has_server_mode.py b/support/has_server_mode.py index c91584d..314eef7 100644 --- a/support/has_server_mode.py +++ b/support/has_server_mode.py @@ -3,6 +3,7 @@ _server_mode = None + def has_server_mode(): global _server_mode if _server_mode is None: From 054649c2f975e92470950b97201bd7ea13211a1f Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 25 Jul 2017 22:16:51 +0200 Subject: [PATCH 16/64] Improvements to target handling --- commands/build.py | 42 +++++++++++++++++++++++++++++++++--------- commands/set_target.py | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/commands/build.py b/commands/build.py index fef2ce9..9ab84e4 100644 --- a/commands/build.py +++ b/commands/build.py @@ -1,7 +1,9 @@ import sublime import Default.exec +import os from .command import CmakeCommand, ServerManager + class CmakeExecCommand(Default.exec.ExecCommand): def run(self, window_id, **kwargs): @@ -11,32 +13,53 @@ def run(self, window_id, **kwargs): 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=False): if not self.is_enabled(): sublime.error_message("Cannot build a CMake target!") return - active_target = self.window.project_data().get("settings", {}).get("active_target", None) + 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 = int(f.read()) + else: + active_target = None if select 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 ] + 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): - self.window.run_command("cmake_set_target", {"index": index}) - if index == -1: + 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 - target = self.server.targets[index] if target.type == "RUN": if sublime.platform() in ("linux", "osx"): prefix = "./" @@ -53,7 +76,8 @@ def _on_done(self, index): self.window.run_command( "cmake_exec", { "window_id": self.window.id(), - "cmd": self.cmake.cmd(None if target.type == "ALL" else target), + "cmd": self.cmake.cmd( + None if target.type == "ALL" else target), "file_regex": self.cmake.file_regex(), "syntax": self.cmake.syntax(), "working_dir": self.cmake.build_folder diff --git a/commands/set_target.py b/commands/set_target.py index e6261e5..1e94ed1 100644 --- a/commands/set_target.py +++ b/commands/set_target.py @@ -1,25 +1,46 @@ +import os +import sublime from .command import CmakeCommand class CmakeSetTargetCommand(CmakeCommand): - def run(self, index=None): - if not index: - self.items = [ [t.name, t.type, t.directory] for t in self.server.targets ] + 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): - data = self.window.project_data() - if index == -1: - data.get("settings", {}).pop("active_target", None) + if isinstance(index, str): + self._write_to_file(index) + elif index == -1: self.window.active_view().erase_status("cmake_active_target") else: - data["settings"]["active_target"] = index name = self.server.targets[index].name - self.window.active_view().set_status("cmake_active_target", "TARGET: " + name) - self.window.set_project_data(data) + 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) def description(self): return "Set Target..." From 207b61fd9440b325222ca8e9a090de6a59737245 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 30 Jul 2017 17:05:51 +0200 Subject: [PATCH 17/64] Try to use a sublime-build instead --- commands/build.py | 24 ++++++++++++++++-------- commands/command.py | 16 ++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/commands/build.py b/commands/build.py index 9ab84e4..dbc2a9b 100644 --- a/commands/build.py +++ b/commands/build.py @@ -31,7 +31,7 @@ def run(self, select=False): "active_target.txt") if os.path.exists(path): with open(path, "r") as f: - active_target = int(f.read()) + active_target = f.read() else: active_target = None if select or active_target is None: @@ -65,13 +65,21 @@ def _on_done(self, index): prefix = "./" else: prefix = "" - self.window.run_command( - "cmake_exec", { - "window_id": self.window.id(), - "shell_cmd": prefix + target.fullname, - "working_dir": target.directory - } - ) + try: + import TerminalView # will throw if not present + self.window.run_command( + "terminal_view_exec", { + "cmd": [prefix + target.fullname], + "working_dir": target.directory + }) + except Exception as e: + self.window.run_command( + "cmake_exec", { + "window_id": self.window.id(), + "shell_cmd": prefix + target.fullname, + "working_dir": target.directory + } + ) else: self.window.run_command( "cmake_exec", { diff --git a/commands/command.py b/commands/command.py index 2af6557..2cfc558 100644 --- a/commands/command.py +++ b/commands/command.py @@ -16,7 +16,7 @@ def is_enabled(self): class ServerManager(sublime_plugin.EventListener): - """Manages the bijection between cmake-enabled projects and server + """Manages the bijection between cmake-enabled projects and server objects.""" _servers = {} @@ -65,10 +65,10 @@ def on_activated(self, view): on_clone = on_load - def on_window_command(self, window, command_name, command_args): - if command_name != "build": - return None - server = ServerManager.get(window) - if not server: - return None - return ("cmake_build", command_args) + # def on_window_command(self, window, command_name, command_args): + # if command_name != "build": + # return None + # server = ServerManager.get(window) + # if not server: + # return None + # return ("cmake_build", command_args) From 105d95323d6df40ffebe869327bcdc09be473c6d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 30 Jul 2017 22:01:39 +0200 Subject: [PATCH 18/64] Minor tweaks --- commands/command.py | 6 +----- server.py | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/commands/command.py b/commands/command.py index 2cfc558..cd21bdf 100644 --- a/commands/command.py +++ b/commands/command.py @@ -7,12 +7,8 @@ class CmakeCommand(sublime_plugin.WindowCommand): def is_enabled(self): - try: - self.cmake = CMakeGenerator.create(self.window) - except Exception as e: - return False self.server = ServerManager.get(self.window) - return self.server is not None and super(sublime_plugin.WindowCommand, self).is_enabled() + return self.server and super(CmakeCommand, self).is_enabled() class ServerManager(sublime_plugin.EventListener): diff --git a/server.py b/server.py index e5f12e8..4980cc4 100644 --- a/server.py +++ b/server.py @@ -2,6 +2,7 @@ import json import sublime import copy +import time class Target(object): From 03bbd3b38bb7e21e7c2161d4598b8f04787e2a3b Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 30 Jul 2017 22:01:54 +0200 Subject: [PATCH 19/64] Add tests folder for unit tests --- tests/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..1a62b32 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,3 @@ +# Unit Testing + +We use https://github.com/randy3k/UnitTesting for the tests. From d6e076dbe7558a64833784da5ea4ff10314f58f5 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 30 Jul 2017 22:53:39 +0200 Subject: [PATCH 20/64] Update commands to not use self.cmake but self.server.cmake instead --- commands/build.py | 8 ++++---- commands/command.py | 3 ++- commands/configure2.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/commands/build.py b/commands/build.py index dbc2a9b..054228f 100644 --- a/commands/build.py +++ b/commands/build.py @@ -84,10 +84,10 @@ def _on_done(self, index): self.window.run_command( "cmake_exec", { "window_id": self.window.id(), - "cmd": self.cmake.cmd( + "cmd": self.server.cmake.cmd( None if target.type == "ALL" else target), - "file_regex": self.cmake.file_regex(), - "syntax": self.cmake.syntax(), - "working_dir": self.cmake.build_folder + "file_regex": self.server.cmake.file_regex(), + "syntax": self.server.cmake.syntax(), + "working_dir": self.server.cmake.build_folder } ) diff --git a/commands/command.py b/commands/command.py index cd21bdf..4b5d258 100644 --- a/commands/command.py +++ b/commands/command.py @@ -8,7 +8,8 @@ class CmakeCommand(sublime_plugin.WindowCommand): def is_enabled(self): self.server = ServerManager.get(self.window) - return self.server and super(CmakeCommand, self).is_enabled() + return (self.server is not None and + super(CmakeCommand, self).is_enabled()) class ServerManager(sublime_plugin.EventListener): diff --git a/commands/configure2.py b/commands/configure2.py index 8d85b1c..02d46a9 100644 --- a/commands/configure2.py +++ b/commands/configure2.py @@ -4,4 +4,4 @@ class CmakeConfigure2Command(CmakeCommand): def run(self): - self.server.configure(self.cmake.command_line_overrides) + self.server.configure(self.server.cmake.command_line_overrides) From 16737d06a8f0ace1111eb15e110f398ffdd22fcf Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 09:51:28 +0200 Subject: [PATCH 21/64] Add "cmake_show_configure_output" command --- CMakeBuilder.sublime-build | 4 ++++ CMakeBuilder.sublime-commands | 8 ++++++-- CMakeBuilder.sublime-settings | 21 +++++++++++++-------- commands/__init__.py | 1 + commands/show_configure_output.py | 7 +++++++ server.py | 27 +++++++++++++++++---------- 6 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 CMakeBuilder.sublime-build create mode 100644 commands/show_configure_output.py diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build new file mode 100644 index 0000000..6ea459e --- /dev/null +++ b/CMakeBuilder.sublime-build @@ -0,0 +1,4 @@ +{ + "target": "cmake_build", + "selector": "source.cmake | source.c | source.c++" +} diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index ee5f03a..94b0064 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" }, { @@ -50,5 +50,9 @@ { "command": "cmake_set_global_setting", "caption": "CMakeBuilder: Set Global Setting" + }, + { + "command": "cmake_show_configure_output", + "caption": "CMakeBuilder: Show Configure Output" } ] diff --git a/CMakeBuilder.sublime-settings b/CMakeBuilder.sublime-settings index 7ae99a9..e39726c 100644 --- a/CMakeBuilder.sublime-settings +++ b/CMakeBuilder.sublime-settings @@ -1,23 +1,28 @@ { - // 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. - + //========================================================================== - + + // 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 a warning or 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 +40,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/commands/__init__.py b/commands/__init__.py index f2ee50a..a7825c8 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -14,4 +14,5 @@ from .set_global_setting import CmakeSetGlobalSettingCommand from .set_target import CmakeSetTargetCommand from .write_build_targets import CmakeWriteBuildTargetsCommand +from .show_configure_output import CmakeShowConfigureOutputCommand from .command import ServerManager diff --git a/commands/show_configure_output.py b/commands/show_configure_output.py new file mode 100644 index 0000000..190104d --- /dev/null +++ b/commands/show_configure_output.py @@ -0,0 +1,7 @@ +from .command import CmakeCommand + + +class CmakeShowConfigureOutputCommand(CmakeCommand): + + def run(self): + self.window.run_command("show_panel", {"panel": "output.cmake.configure"}) diff --git a/server.py b/server.py index 4980cc4..68a4da7 100644 --- a/server.py +++ b/server.py @@ -3,7 +3,7 @@ import sublime import copy import time - +import threading class Target(object): @@ -121,7 +121,9 @@ def configure(self, cache_arguments={}): view.settings().set("result_base_dir", self.cmake.source_folder) view.set_syntax_file( "Packages/CMakeBuilder/Syntax/Configure.sublime-syntax") - window.run_command("show_panel", {"panel": "output.cmake.configure"}) + 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 = [] @@ -340,21 +342,26 @@ def receive_message(self, thedict): name = "cmake." + thedict["inReplyTo"] view = window.find_output_panel(name) assert view - window.run_command("show_panel", {"panel": "output.{}".format(name)}) + settings = sublime.load_settings("CMakeBuilder.sublime-settings") + if settings.get("server_configure_verbose", False): + window.run_command("show_panel", {"panel": "output.{}".format(name)}) view = window.find_output_panel(name) view.run_command("append", { "characters": thedict["message"] + "\n", "force": True, "scroll_to_end": True}) + _signal_lock = threading.Lock() + def receive_signal(self, thedict): - if (thedict["name"] == "dirty" and not - self.is_configuring and not - self.is_building): - self.configure() - else: - print("received signal") - print(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.cmake.window.new_file() From 9496919b85a9c6c215a2269d957836679c50f3af Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 10:11:26 +0200 Subject: [PATCH 22/64] Move to a .sublime-build file. This works okay now. --- CMakeBuilder.sublime-build | 11 ++++++++++- Main.sublime-menu | 17 ++++++++++------- commands/build.py | 4 ++-- commands/clear_cache.py | 15 ++++++++------- commands/command.py | 12 ++++++------ commands/diagnose.py | 5 +++-- commands/dump_file_system_watchers.py | 5 +++-- commands/dump_inputs.py | 5 +++-- commands/edit_cache.py | 5 +++-- commands/new_project.py | 3 ++- commands/open_build_folder.py | 3 ++- commands/reveal_include_directories.py | 5 +++-- commands/run_ctest.py | 9 +++++---- commands/set_global_setting.py | 4 ++++ commands/set_target.py | 3 ++- commands/show_configure_output.py | 7 ++++++- 16 files changed, 72 insertions(+), 41 deletions(-) diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build index 6ea459e..ca0af57 100644 --- a/CMakeBuilder.sublime-build +++ b/CMakeBuilder.sublime-build @@ -1,4 +1,13 @@ { "target": "cmake_build", - "selector": "source.cmake | source.c | source.c++" + "selector": "source.cmake | source.c | source.c++", + "variants": + [ + { + "name": "Build Single Target", + "target": "cmake_build", + "selector": "source.cmake | source.c | source.c++", + "select_target": true + } + ] } diff --git a/Main.sublime-menu b/Main.sublime-menu index d5e98e6..e8e2447 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", @@ -59,6 +59,9 @@ }, { "command": "cmake_set_global_setting" + }, + { + "command": "cmake_show_configure_output" } ] } @@ -67,17 +70,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/commands/build.py b/commands/build.py index 054228f..7e7c493 100644 --- a/commands/build.py +++ b/commands/build.py @@ -21,7 +21,7 @@ def on_finished(self, proc): class CmakeBuildCommand(CmakeCommand): - def run(self, select=False): + def run(self, select_target=False): if not self.is_enabled(): sublime.error_message("Cannot build a CMake target!") return @@ -34,7 +34,7 @@ def run(self, select=False): active_target = f.read() else: active_target = None - if select or active_target is 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?") 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 index 4b5d258..da4ef60 100644 --- a/commands/command.py +++ b/commands/command.py @@ -63,9 +63,9 @@ def on_activated(self, view): on_clone = on_load # def on_window_command(self, window, command_name, command_args): - # if command_name != "build": - # return None - # server = ServerManager.get(window) - # if not server: - # return None - # return ("cmake_build", command_args) + # if command_name != "build": + # return None + # server = ServerManager.get(window) + # if not server: + # return None + # return ("cmake_build", command_args) 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 index c7984f0..c3917c3 100644 --- a/commands/dump_file_system_watchers.py +++ b/commands/dump_file_system_watchers.py @@ -3,9 +3,10 @@ class CmakeDumpFileSystemWatchersCommand(CmakeCommand): """Prints the watched files to a new view""" - + def run(self): self.server.file_system_watchers() - def description(self): + @classmethod + def description(cls): return "Dump File System Watchers" diff --git a/commands/dump_inputs.py b/commands/dump_inputs.py index d6cbb3f..edd308a 100644 --- a/commands/dump_inputs.py +++ b/commands/dump_inputs.py @@ -3,9 +3,10 @@ class CmakeDumpInputsCommand(CmakeCommand): """Prints the cmake inputs to a new view""" - + def run(self): self.server.cmake_inputs() - def description(self): + @classmethod + def description(cls): return "Dump CMake Inputs" diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 9ec83d2..3ca98e9 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -11,8 +11,9 @@ def is_enabled(self): return os.path.exists(os.path.join(build_folder, "CMakeCache.txt")) except Exception as e: return False - - def description(self): + + @classmethod + def description(cls): return 'Edit Cache...' def run(self): 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..7c8832d 100644 --- a/commands/open_build_folder.py +++ b/commands/open_build_folder.py @@ -12,7 +12,8 @@ def is_enabled(self): except Exception as e: return False - def description(self): + @classmethod + def description(cls): return 'Browse Build Folder…' def run(self): diff --git a/commands/reveal_include_directories.py b/commands/reveal_include_directories.py index 6e81953..a6e9c4f 100644 --- a/commands/reveal_include_directories.py +++ b/commands/reveal_include_directories.py @@ -3,7 +3,7 @@ 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") @@ -11,5 +11,6 @@ def run(self): for path in self.server.include_paths: view.run_command("append", {"characters": path + "\n", "force": True}) - def description(self): + @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 index fbb3eca..4b16e46 100644 --- a/commands/set_global_setting.py +++ b/commands/set_global_setting.py @@ -5,3 +5,7 @@ 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 index 1e94ed1..ead5fd2 100644 --- a/commands/set_target.py +++ b/commands/set_target.py @@ -42,5 +42,6 @@ def _write_to_file(self, name): self.window.active_view() \ .set_status("cmake_active_target", "TARGET: " + name) - def description(self): + @classmethod + def description(cls): return "Set Target..." diff --git a/commands/show_configure_output.py b/commands/show_configure_output.py index 190104d..76fec92 100644 --- a/commands/show_configure_output.py +++ b/commands/show_configure_output.py @@ -4,4 +4,9 @@ class CmakeShowConfigureOutputCommand(CmakeCommand): def run(self): - self.window.run_command("show_panel", {"panel": "output.cmake.configure"}) + self.window.run_command("show_panel", + {"panel": "output.cmake.configure"}) + + @classmethod + def description(cls): + return "Show Configure Output" From 7273177d474569c7d6f53990d6c3dfe2b44bc12d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 14:13:30 +0200 Subject: [PATCH 23/64] Better experience --- CMakeBuilder.sublime-build | 11 +--- CMakeBuilder.sublime-commands | 4 ++ CMakeBuilder.sublime-settings | 2 +- Main.sublime-menu | 3 ++ __init__.py | 5 +- commands/__init__.py | 1 + commands/command.py | 75 +++++++++++++++++++++------- commands/configure.py | 6 +-- event_listeners/__init__.py | 2 - event_listeners/configure_on_save.py | 25 ---------- server.py | 4 +- 11 files changed, 75 insertions(+), 63 deletions(-) delete mode 100644 event_listeners/__init__.py delete mode 100644 event_listeners/configure_on_save.py diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build index ca0af57..6ea459e 100644 --- a/CMakeBuilder.sublime-build +++ b/CMakeBuilder.sublime-build @@ -1,13 +1,4 @@ { "target": "cmake_build", - "selector": "source.cmake | source.c | source.c++", - "variants": - [ - { - "name": "Build Single Target", - "target": "cmake_build", - "selector": "source.cmake | source.c | source.c++", - "select_target": true - } - ] + "selector": "source.cmake | source.c | source.c++" } diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index 94b0064..fe1fdb4 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -54,5 +54,9 @@ { "command": "cmake_show_configure_output", "caption": "CMakeBuilder: Show Configure Output" + }, + { + "command": "cmake_restart_server", + "caption": "CMakeBuilder: Restart Server For This Project" } ] diff --git a/CMakeBuilder.sublime-settings b/CMakeBuilder.sublime-settings index e39726c..222e8bf 100644 --- a/CMakeBuilder.sublime-settings +++ b/CMakeBuilder.sublime-settings @@ -20,7 +20,7 @@ // 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 a warning or an error occurs. + // when an error occurs. "server_configure_verbose": false, // If the command "CMakeBuilder: Configure" exited with status 0, should we diff --git a/Main.sublime-menu b/Main.sublime-menu index e8e2447..da2cdcd 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -62,6 +62,9 @@ }, { "command": "cmake_show_configure_output" + }, + { + "command": "cmake_restart_server" } ] } diff --git a/__init__.py b/__init__.py index 54a8f65..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 a7825c8..df0b819 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -16,3 +16,4 @@ from .write_build_targets import CmakeWriteBuildTargetsCommand from .show_configure_output import CmakeShowConfigureOutputCommand from .command import ServerManager +from .command import CmakeRestartServerCommand diff --git a/commands/command.py b/commands/command.py index da4ef60..39d54e6 100644 --- a/commands/command.py +++ b/commands/command.py @@ -1,7 +1,22 @@ import sublime_plugin +import sublime +import os from ..generators import CMakeGenerator from ..server import Server from ..support import has_server_mode +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): @@ -12,6 +27,21 @@ def is_enabled(self): super(CmakeCommand, self).is_enabled()) +class CmakeRestartServerCommand(CmakeCommand): + + def run(self): + try: + window_id = self.window.id() + cmake = CMakeGenerator.create(self.window) + ServerManager._servers[window_id] = Server(cmake) + except Exception as e: + sublime.errror_message(str(e)) + + @classmethod + def description(cls): + return "Restart Server For This Project" + + class ServerManager(sublime_plugin.EventListener): """Manages the bijection between cmake-enabled projects and server objects.""" @@ -47,25 +77,34 @@ def on_load(self, view): def on_activated(self, view): self.on_load(view) - index = view.settings().get("active_target", None) - if not index: + try: + server = self.__class__.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") + + + def on_post_save(self, view): + if not view: return - server = self.__class__.get(view.window()) - if not server: - view.erase_status("cmake_active_target") + if not get_setting(view, "configure_on_save", False): return - if not server.targets: - view.erase_status("cmake_active_target") + name = view.file_name() + if not name: return - view.set_status("cmake_active_target", "TARGET: " + server.targets[int(index)].name) - - on_clone = on_load - - # def on_window_command(self, window, command_name, command_args): - # if command_name != "build": - # return None - # server = ServerManager.get(window) - # if not server: - # return None - # return ("cmake_build", command_args) + if name.endswith(".sublime-project"): + server = self.__class__.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 4f5e746..3788890 100644 --- a/commands/configure.py +++ b/commands/configure.py @@ -19,7 +19,7 @@ def is_enabled(self): return True except Exception as e: return False - + def description(self): return 'Configure' @@ -111,12 +111,12 @@ def run(self, write_build_targets=False, silence_dev_warnings=False): 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/event_listeners/__init__.py b/event_listeners/__init__.py deleted file mode 100644 index 8b73b9f..0000000 --- a/event_listeners/__init__.py +++ /dev/null @@ -1,2 +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 342ea89..0000000 --- a/event_listeners/configure_on_save.py +++ /dev/null @@ -1,25 +0,0 @@ -import sublime_plugin -from ..support import get_setting -from ..commands import CmakeConfigure2Command - - -def _configure(window): - if not CmakeConfigure2Command(window).is_enabled(): - return - window.run_command("cmake_configure2") - - -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/server.py b/server.py index 68a4da7..2f0607f 100644 --- a/server.py +++ b/server.py @@ -73,6 +73,8 @@ def on_finished(self, _): .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() @@ -174,7 +176,7 @@ def receive_reply(self, thedict): reply = thedict["inReplyTo"] if reply == "handshake": self.cmake.window.status_message( - "CMake server {}.{} at your service!" + "CMake server protocol {}.{}, handshake is OK" .format(self.protocol["major"], self.protocol["minor"])) self.configure() elif reply == "setGlobalSettings": From aaded36a1ea20596c497a9b9682329bab6a0a442 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 14:28:27 +0200 Subject: [PATCH 24/64] Better error handling --- server.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index 2f0607f..da12864 100644 --- a/server.py +++ b/server.py @@ -114,6 +114,7 @@ def configure(self, cache_arguments={}): if self.is_configuring: return self.is_configuring = True + self.bad_configure = False window = self.cmake.window view = window.create_output_panel("cmake.configure", True) view.settings().set( @@ -183,7 +184,11 @@ def receive_reply(self, thedict): self.cmake.window.status_message( "Global CMake setting is modified") elif reply == "configure": - self.cmake.window.status_message("Project is configured") + if self.bad_configure: + self.is_configuring = False + self.cmake.window.status_message("Some errors occured during configure!") + else: + self.cmake.window.status_message("Project is configured") elif reply == "compute": self.cmake.window.status_message("Project is generated") self.is_configuring = False @@ -328,7 +333,7 @@ def receive_progress(self, thedict): current = thedict["progressCurrent"] if maximum == current: view.erase_status("cmake_" + thedict["inReplyTo"]) - if thedict["inReplyTo"] == "configure": + if thedict["inReplyTo"] == "configure" and not self.bad_configure: self.compute() else: status = "{0} {1:.0f}%".format( @@ -347,11 +352,11 @@ def receive_message(self, thedict): settings = sublime.load_settings("CMakeBuilder.sublime-settings") if settings.get("server_configure_verbose", False): window.run_command("show_panel", {"panel": "output.{}".format(name)}) - view = window.find_output_panel(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() @@ -377,3 +382,11 @@ def dump_to_new_view(self, thedict, name): {"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.cmake.window.run_command("show_panel", {"panel": "output.cmake.configure"}) + From e3ef48fb7c377cafe47713ac3a1e80cf3b9f5e32 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 15:13:28 +0200 Subject: [PATCH 25/64] rename the build system to "CMake" --- CMakeBuilder.sublime-build | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 CMakeBuilder.sublime-build diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build deleted file mode 100644 index 6ea459e..0000000 --- a/CMakeBuilder.sublime-build +++ /dev/null @@ -1,4 +0,0 @@ -{ - "target": "cmake_build", - "selector": "source.cmake | source.c | source.c++" -} From 8b8d207243b897b09f27092439ecb80f33a64d4c Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 16:02:58 +0200 Subject: [PATCH 26/64] Fix error regarding write build targets command --- commands/write_build_targets.py | 4 ++-- generators/__init__.py | 2 +- generators/linux/Ninja.py | 10 +++------- generators/linux/Unix_Makefiles.py | 6 +----- generators/osx/Ninja.py | 9 ++------- generators/osx/Unix_Makefiles.py | 6 +----- 6 files changed, 10 insertions(+), 27 deletions(-) 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/generators/__init__.py b/generators/__init__.py index 5b2a08c..d530faf 100644 --- a/generators/__init__.py +++ b/generators/__init__.py @@ -95,7 +95,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 diff --git a/generators/linux/Ninja.py b/generators/linux/Ninja.py index 774c049..888a08c 100644 --- a/generators/linux/Ninja.py +++ b/generators/linux/Ninja.py @@ -1,6 +1,7 @@ from CMakeBuilder.generators import CMakeGenerator import subprocess import sublime +import os class Ninja(CMakeGenerator): @@ -15,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, @@ -30,8 +27,7 @@ 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 = [ diff --git a/generators/linux/Unix_Makefiles.py b/generators/linux/Unix_Makefiles.py index 17bd0ca..764192c 100644 --- a/generators/linux/Unix_Makefiles.py +++ b/generators/linux/Unix_Makefiles.py @@ -19,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, diff --git a/generators/osx/Ninja.py b/generators/osx/Ninja.py index 23d5ede..4ee5de0 100644 --- a/generators/osx/Ninja.py +++ b/generators/osx/Ninja.py @@ -15,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, @@ -30,8 +26,7 @@ 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 = [ diff --git a/generators/osx/Unix_Makefiles.py b/generators/osx/Unix_Makefiles.py index 17bd0ca..764192c 100644 --- a/generators/osx/Unix_Makefiles.py +++ b/generators/osx/Unix_Makefiles.py @@ -19,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, From 6331603037511497f1c8b4a9c539c6f2a061b395 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 16:54:47 +0200 Subject: [PATCH 27/64] Add CMake.sublime-build --- CMake.sublime-build | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CMake.sublime-build diff --git a/CMake.sublime-build b/CMake.sublime-build new file mode 100644 index 0000000..75af317 --- /dev/null +++ b/CMake.sublime-build @@ -0,0 +1,11 @@ +{ + "target": "cmake_build", + "selector": "source.cmake | source.c | source.c++", + "variants": + { + "target": "cmake_build", + "name": "Select & Build Target", + "selector": "source.cmake | source.c | source.c++", + "select_target": true + } +} From 562357c887262d0759f3872c8d4b809ade45ecc0 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 31 Jul 2017 16:56:00 +0200 Subject: [PATCH 28/64] Add error message for old users --- commands/build.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/commands/build.py b/commands/build.py index 7e7c493..7e61998 100644 --- a/commands/build.py +++ b/commands/build.py @@ -2,6 +2,7 @@ import Default.exec import os from .command import CmakeCommand, ServerManager +from ..support import has_server_mode class CmakeExecCommand(Default.exec.ExecCommand): @@ -22,6 +23,17 @@ def on_finished(self, proc): class CmakeBuildCommand(CmakeCommand): def run(self, select_target=False): + if not has_server_mode(): + 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 From 1c9bf89c22e2cee39f4ca0c32a674ed4494ed438 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 1 Aug 2017 10:38:40 +0200 Subject: [PATCH 29/64] Add unit test scaffolding --- .appveyor.yml | 23 +++++++++++++ .coveragerc | 2 ++ .gitignore | 4 +++ .travis.yml | 55 +++++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/proj1/CMakeLists.txt | 2 ++ tests/proj1/main.c | 4 +++ tests/proj1/proj1.sublime-project | 15 +++++++++ tests/test_configure.py | 0 unittesting.json | 9 +++++ 10 files changed, 114 insertions(+) create mode 100644 .appveyor.yml create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 tests/__init__.py create mode 100644 tests/proj1/CMakeLists.txt create mode 100644 tests/proj1/main.c create mode 100644 tests/proj1/proj1.sublime-project create mode 100644 tests/test_configure.py create mode 100644 unittesting.json diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..9dd8e70 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,23 @@ +environment: + # The package name + PACKAGE: "UnitTesting-example" + 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 \ No newline at end of file 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/.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..369ac2a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +env: + global: + - PACKAGE="UnitTesting-example" # 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 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/proj1/CMakeLists.txt b/tests/proj1/CMakeLists.txt new file mode 100644 index 0000000..02a27a5 --- /dev/null +++ b/tests/proj1/CMakeLists.txt @@ -0,0 +1,2 @@ +project(proj1 VERSION 0.1.0 LANGUAGES C) +add_executable(a main.c) diff --git a/tests/proj1/main.c b/tests/proj1/main.c new file mode 100644 index 0000000..c7fa6a8 --- /dev/null +++ b/tests/proj1/main.c @@ -0,0 +1,4 @@ +int main(int argc, char const *argv[]) +{ + return 0; +} diff --git a/tests/proj1/proj1.sublime-project b/tests/proj1/proj1.sublime-project new file mode 100644 index 0000000..4a7713d --- /dev/null +++ b/tests/proj1/proj1.sublime-project @@ -0,0 +1,15 @@ +{ + "folders": + [ + { + "path": "." + } + ], + "settings": + { + "cmake": + { + "build_folder": "${project_path}/build" + } + } +} diff --git a/tests/test_configure.py b/tests/test_configure.py new file mode 100644 index 0000000..e69de29 diff --git a/unittesting.json b/unittesting.json new file mode 100644 index 0000000..45a7ede --- /dev/null +++ b/unittesting.json @@ -0,0 +1,9 @@ +{ + "tests_dir" : "tests", + "pattern" : "test*.py", + "async": false, + "deferred": false, + "verbosity": 2, + "capture_console": false, + "output": null +} \ No newline at end of file From 7158ee6bb225125fc42598aa8e7be8566b5ee2ac Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 1 Aug 2017 22:07:27 +0200 Subject: [PATCH 30/64] Add more test scaffolding --- tests/README.md | 10 ++++++++ tests/fixtures.py | 39 +++++++++++++++++++++++++++++++ tests/proj1/CMakeLists.txt | 2 -- tests/proj1/main.c | 4 ---- tests/proj1/proj1.sublime-project | 15 ------------ tests/test_configure.py | 23 ++++++++++++++++++ unittesting.json | 10 +++----- 7 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 tests/fixtures.py delete mode 100644 tests/proj1/CMakeLists.txt delete mode 100644 tests/proj1/main.c delete mode 100644 tests/proj1/proj1.sublime-project diff --git a/tests/README.md b/tests/README.md index 1a62b32..63e0bdd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,3 +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 ProjectFixtureTestCase. diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..7ac4481 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,39 @@ +"""Defines TestCase""" +import unittesting +import os +import sublime + + +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() + 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) diff --git a/tests/proj1/CMakeLists.txt b/tests/proj1/CMakeLists.txt deleted file mode 100644 index 02a27a5..0000000 --- a/tests/proj1/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -project(proj1 VERSION 0.1.0 LANGUAGES C) -add_executable(a main.c) diff --git a/tests/proj1/main.c b/tests/proj1/main.c deleted file mode 100644 index c7fa6a8..0000000 --- a/tests/proj1/main.c +++ /dev/null @@ -1,4 +0,0 @@ -int main(int argc, char const *argv[]) -{ - return 0; -} diff --git a/tests/proj1/proj1.sublime-project b/tests/proj1/proj1.sublime-project deleted file mode 100644 index 4a7713d..0000000 --- a/tests/proj1/proj1.sublime-project +++ /dev/null @@ -1,15 +0,0 @@ -{ - "folders": - [ - { - "path": "." - } - ], - "settings": - { - "cmake": - { - "build_folder": "${project_path}/build" - } - } -} diff --git a/tests/test_configure.py b/tests/test_configure.py index e69de29..9c54172 100644 --- a/tests/test_configure.py +++ b/tests/test_configure.py @@ -0,0 +1,23 @@ +from CMakeBuilder.tests.fixtures import TestCase + + +class TestConfigure(TestCase): + + cmake_settings = r""" + { + "build_folder": "${project_path}/build" + } + """ + + files = [ + ("CMakeLists.txt", r""" + project(foo VERSION 0.1 LANGUAGES C) + message(STATUS "okay") + """) + ] + + + def test_configure(self): + self.window.run_command("cmake_configure") + self.assertTrue(True) + self.assertTrue(True) diff --git a/unittesting.json b/unittesting.json index 45a7ede..9d4e572 100644 --- a/unittesting.json +++ b/unittesting.json @@ -1,9 +1,5 @@ { "tests_dir" : "tests", - "pattern" : "test*.py", - "async": false, - "deferred": false, - "verbosity": 2, - "capture_console": false, - "output": null -} \ No newline at end of file + "pattern" : "test_*", + "deferred": true +} From 2b7aa7baa61091efa1571ba25ec5ef573df118a9 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 1 Aug 2017 22:32:16 +0200 Subject: [PATCH 31/64] Change to capabilities --- commands/build.py | 4 +-- commands/command.py | 6 ++-- commands/insert_diagnosis.py | 59 +++++++++++++++++------------------- support/__init__.py | 4 +-- support/has_server_mode.py | 18 ----------- 5 files changed, 35 insertions(+), 56 deletions(-) delete mode 100644 support/has_server_mode.py diff --git a/commands/build.py b/commands/build.py index 7e61998..6e9e977 100644 --- a/commands/build.py +++ b/commands/build.py @@ -2,7 +2,7 @@ import Default.exec import os from .command import CmakeCommand, ServerManager -from ..support import has_server_mode +from ..support import capabilities class CmakeExecCommand(Default.exec.ExecCommand): @@ -23,7 +23,7 @@ def on_finished(self, proc): class CmakeBuildCommand(CmakeCommand): def run(self, select_target=False): - if not has_server_mode(): + 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 " diff --git a/commands/command.py b/commands/command.py index 39d54e6..f3852da 100644 --- a/commands/command.py +++ b/commands/command.py @@ -3,7 +3,7 @@ import os from ..generators import CMakeGenerator from ..server import Server -from ..support import has_server_mode +from ..support import capabilities from ..support import get_setting @@ -53,8 +53,8 @@ def get(cls, window): return cls._servers.get(window.id(), None) def on_load(self, view): - if not has_server_mode(): - print("cmake is not capable of server mode") + if not capabilities("serverMode"): + print("CMakeBuilder: cmake is not capable of server mode") return try: window_id = view.window().id() 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/support/__init__.py b/support/__init__.py index 8d01bcf..54e9340 100644 --- a/support/__init__.py +++ b/support/__init__.py @@ -2,7 +2,7 @@ from .expand_variables import expand_variables from .get_cmake_value import get_cmake_value from .get_setting import get_setting -from .has_server_mode import has_server_mode +from .capabilities import capabilities __all__ = [ @@ -10,4 +10,4 @@ "expand_variables", "get_cmake_value", "get_setting", - "has_server_mode"] + "capabilities"] diff --git a/support/has_server_mode.py b/support/has_server_mode.py deleted file mode 100644 index 314eef7..0000000 --- a/support/has_server_mode.py +++ /dev/null @@ -1,18 +0,0 @@ -from .check_output import check_output -import json - -_server_mode = None - - -def has_server_mode(): - global _server_mode - if _server_mode is None: - try: - output = check_output("cmake -E capabilities") - except Exception as e: - print("CMakeBuilder: Error: Could not load cmake's capabilities") - _server_mode = False - else: - output = json.loads(output) - _server_mode = output.get("serverMode", False) - return _server_mode From 13ae1e67f0b80031032b2c524bdebc70490ada9c Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 1 Aug 2017 22:32:48 +0200 Subject: [PATCH 32/64] Add capabilities file --- support/capabilities.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 support/capabilities.py 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) From b4518a081a6c5e5dd42def29763867792007f743 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 6 Aug 2017 16:58:46 +0200 Subject: [PATCH 33/64] Better user selections --- commands/build.py | 9 +- commands/command.py | 273 ++++++++++++++++++++++++++++++++++++++++---- server.py | 74 ++++++------ 3 files changed, 296 insertions(+), 60 deletions(-) diff --git a/commands/build.py b/commands/build.py index 6e9e977..c0ee7eb 100644 --- a/commands/build.py +++ b/commands/build.py @@ -84,7 +84,7 @@ def _on_done(self, index): "cmd": [prefix + target.fullname], "working_dir": target.directory }) - except Exception as e: + except Exception: self.window.run_command( "cmake_exec", { "window_id": self.window.id(), @@ -96,10 +96,9 @@ def _on_done(self, index): self.window.run_command( "cmake_exec", { "window_id": self.window.id(), - "cmd": self.server.cmake.cmd( - None if target.type == "ALL" else target), - "file_regex": self.server.cmake.file_regex(), - "syntax": self.server.cmake.syntax(), + "cmd": target.cmd(), + "file_regex": self.server.cmake.file_regex, + "syntax": self.server.cmake.syntax, "working_dir": self.server.cmake.build_folder } ) diff --git a/commands/command.py b/commands/command.py index f3852da..a3b3e3b 100644 --- a/commands/command.py +++ b/commands/command.py @@ -1,7 +1,6 @@ import sublime_plugin import sublime import os -from ..generators import CMakeGenerator from ..server import Server from ..support import capabilities from ..support import get_setting @@ -32,8 +31,7 @@ class CmakeRestartServerCommand(CmakeCommand): def run(self): try: window_id = self.window.id() - cmake = CMakeGenerator.create(self.window) - ServerManager._servers[window_id] = Server(cmake) + ServerManager._servers.pop(window_id, None) except Exception as e: sublime.errror_message(str(e)) @@ -42,6 +40,29 @@ 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.""" @@ -52,28 +73,238 @@ class ServerManager(sublime_plugin.EventListener): def get(cls, window): return cls._servers.get(window.id(), None) + def __init__(self): + self._is_selecting = False + self.generator = "" + self.source_folder = "" + self.build_folder = "" + self.toolset = "" + self.platform = "" + def on_load(self, view): if not capabilities("serverMode"): print("CMakeBuilder: cmake is not capable of server mode") return - try: - window_id = view.window().id() - cmake = CMakeGenerator.create(view.window()) - except KeyError as e: - return - except AttributeError as e: - return - except TypeError as e: - return - server = self.__class__._servers.get(window_id, None) - if not server: - try: - self.__class__._servers[window_id] = Server(cmake) - except Exception as e: - print(str(e)) - return - elif str(server.cmake) != str(cmake): - self.__class__._servers[window_id] = Server(cmake) + if self._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. + self.window = view.window() + if not self.window: + return + server = self.get(self.window) + if server: + return + + # No server running. Check if there are build settings. + data = self.window.project_data() + settings = data.get("settings", None) + if not settings or not isinstance(settings, dict): + print("no settings") + return + cmake = settings.get("cmake", None) + if not cmake or not isinstance(cmake, dict): + print("no cmake dict") + return + self.schemes = cmake.get("schemes", None) + if (not self.schemes or + not isinstance(self.schemes, list) or + len(self.schemes) == 0): + print("no schemes") + 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. + 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 + self.source_folder = os.path.dirname(cmake_file) + print("found source folder:", self.source_folder) + + # At this point we have a bunch of schemes and we have a source folder. + self.items = [] + for scheme in self.schemes: + if not isinstance(scheme, dict): + sublime.error_message("Please make sure all of your schemes " + "are JSON dictionaries.") + self.items.append(["INVALID SCHEME", ""]) + continue + name = scheme.get("name", "Untitled Scheme") + build_folder = scheme.get("build_folder", "${project_path}/build") + variables = self.window.extract_variables() + build_folder = sublime.expand_variables(build_folder, variables) + self.items.append([name, build_folder]) + if len(self.schemes) == 0: + print("found schemes dict, but it is empty") + return + self._is_selecting = True + if len(self.schemes) == 1: + # Select the only scheme possible. + self._on_done_select_scheme(0) + else: + # Ask the user what he/she wants. + self.window.show_quick_panel(self.items, + self._on_done_select_scheme) + + def _on_done_select_scheme(self, index): + if index == -1: + self._is_selecting = False + return + self.name = self.items[index][0] + if self.name == "INVALID SCHEME": + self._is_selecting = False + return + self.build_folder_pre_expansion = self.schemes[index]["build_folder"] + self.build_folder = sublime.expand_variables( + self.build_folder_pre_expansion, self.window.extract_variables()) + self.command_line_overrides = self.schemes[index].get( + "command_line_overrides", {}) + self._select_generator() + + def _select_generator(self): + if self.generator: + self._select_toolset() + return + self.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) + self.items.append([g["name"], platform_support, toolset_support]) + if len(self.items) == 1: + self._on_done_select_generator(0) + else: + self.window.show_quick_panel(self.items, + self._on_done_select_generator) + + def _on_done_select_generator(self, index): + if index == 1: + self._is_selecting = False + return + self.generator = self.items[index][0] + platform_support = self.items[index][1] + toolset_support = self.items[index][2] + self.platform_support = True if "True" in platform_support else False + self.toolset_support = True if "True" in toolset_support else False + if self.platform_support: + text = "Platform for {} (Press Enter for default): ".format( + self.generator) + self.window.show_input_panel(text, "", + self._on_done_select_platform, + None, None) + elif self.toolset_support: + self._select_toolset() + else: + self._run_configure_with_new_settings() + + def _select_toolset(self): + if self.toolset: + return + text = "Toolset for {}: (Press Enter for default): ".format( + self.generator) + self.window.show_input_panel(text, "", self._on_done_select_toolset, + None, None) + + def _on_done_select_platform(self, platform): + self.platform = platform + if self.toolset_support: + self._select_toolset() + else: + self._run_configure_with_new_settings() + + def _on_done_select_toolset(self, toolset): + self.toolset = toolset + self._run_configure_with_new_settings() + + def _run_configure_with_new_settings(self): + self._is_selecting = False + cmake_settings = CMakeSettings() + cmake_settings.source_folder = self.source_folder + cmake_settings.build_folder = self.build_folder + + cmake_settings.build_folder_pre_expansion = \ + self.build_folder_pre_expansion + + cmake_settings.generator = self.generator + cmake_settings.platform = self.platform + cmake_settings.toolset = self.toolset + cmake_settings.command_line_overrides = self.command_line_overrides + + if sublime.platform() in ("osx", "linux"): + cmake_settings.file_regex = \ + r'(.+[^:]):(\d+):(\d+): (?:fatal )?((?:error|warning): .+)$' + if "Makefile" in self.generator: + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Make.sublime-syntax" + elif "Ninja" in self.generator: + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Ninja.sublime-syntax" + else: + print("CMakeBuilder: Warning: Generator", self.generator, + "will not have syntax highlighting in the output panel.") + elif sublime.platform() == "windows": + if "Ninja" in self.generator: + cmake_settings.file_regex = r'^(.+)\((\d+)\):() (.+)$' + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Ninja+CL.sublime-syntax" + elif "Visual Studio" in self.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 self.generator: + cmake_settings.file_regex = r'^(.+)\((\d+)\):() (.+)$' + cmake_settings.syntax = \ + "Packages/CMakeBuilder/Syntax/Make.sublime-syntax" + else: + print("CMakeBuilder: Warning: Generator", self.generator, + "will not have syntax highlighting in the output panel.") + else: + sublime.error_message("Unknown platform: " + sublime.platform()) + return + + server = Server(self.window, cmake_settings) + self.source_folder = "" + self.build_folder = "" + self.build_folder_pre_expansion = "" + self.generator = "" + self.platform = "" + self.toolset = "" + self.items = [] + self.schemes = [] + self.command_line_overrides = {} + self.__class__._servers[self.window.id()] = server def on_activated(self, view): self.on_load(view) diff --git a/server.py b/server.py index da12864..9b6afaa 100644 --- a/server.py +++ b/server.py @@ -7,26 +7,38 @@ class Target(object): - __slots__ = ("name", "fullname", "type", "directory") + __slots__ = ("name", "fullname", "type", "directory", "config") - def __init__(self, name, fullname, type, directory): + 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 @@ -68,7 +80,7 @@ def on_data(self, _, data): self.inside_json_object = True def on_finished(self, _): - self.cmake.window.status_message( + self.window.status_message( "CMake Server has quit (exit code {})" .format(self.proc.exit_code())) @@ -104,7 +116,9 @@ def send_handshake(self): "protocolVersion": self.protocol, "sourceDirectory": self.cmake.source_folder, "buildDirectory": self.cmake.build_folder, - "generator": str(self.cmake) + "generator": self.cmake.generator, + "platform": self.cmake.platform, + "toolset": self.cmake.toolset }) def set_global_setting(self, key, value): @@ -115,7 +129,7 @@ def configure(self, cache_arguments={}): return self.is_configuring = True self.bad_configure = False - window = self.cmake.window + window = self.window view = window.create_output_panel("cmake.configure", True) view.settings().set( "result_file_regex", @@ -176,21 +190,21 @@ def receive_dict(self, thedict): def receive_reply(self, thedict): reply = thedict["inReplyTo"] if reply == "handshake": - self.cmake.window.status_message( + self.window.status_message( "CMake server protocol {}.{}, handshake is OK" .format(self.protocol["major"], self.protocol["minor"])) self.configure() elif reply == "setGlobalSettings": - self.cmake.window.status_message( + self.window.status_message( "Global CMake setting is modified") elif reply == "configure": if self.bad_configure: self.is_configuring = False - self.cmake.window.status_message("Some errors occured during configure!") + self.window.status_message("Some errors occured during configure!") else: - self.cmake.window.status_message("Project is configured") + self.window.status_message("Project is configured") elif reply == "compute": - self.cmake.window.status_message("Project is generated") + self.window.status_message("Project is generated") self.is_configuring = False self.codemodel() elif reply == "fileSystemWatchers": @@ -209,7 +223,7 @@ def receive_reply(self, thedict): continue self.items.append([str(k), str(v)]) self.types.append(type(v)) - window = self.cmake.window + window = self.window def on_done(index): if index == -1: @@ -247,17 +261,12 @@ def on_done_input(new_value): target_fullname = target_name target_dir = target.pop("buildDirectory") self.targets.add( - Target( - target_name, - target_fullname, - target_type, - target_dir)) + 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)) + 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", []) @@ -266,19 +275,16 @@ def on_done_input(new_value): if path: self.include_paths.add(path) self.targets.add( - Target( - "BUILD ALL", - "BUILD ALL", - "ALL", - self.cmake.build_folder)) - data = self.cmake.window.project_data() + Target("BUILD ALL", "BUILD ALL", "ALL", + self.cmake.build_folder, "")) + data = self.window.project_data() self.targets = list(self.targets) data["settings"]["compile_commands"] = \ self.cmake.build_folder_pre_expansion data["settings"]["ecc_flags_sources"] = [{ "file": "compile_commands.json", "search_in": self.cmake.build_folder_pre_expansion}] - self.cmake.window.set_project_data(data) + self.window.set_project_data(data) elif reply == "cache": cache = thedict.pop("cache") self.items = [] @@ -305,14 +311,14 @@ def on_done(index): def on_done_input(new_value): self.configure({key: value}) - self.cmake.window.show_input_panel( + self.window.show_input_panel( 'new value for "' + key + '": ', old_value, on_done_input, None, None) - self.cmake.window.show_quick_panel(self.items, on_done) + self.window.show_quick_panel(self.items, on_done) else: print("received unknown reply type:", reply) @@ -320,14 +326,14 @@ def receive_error(self, thedict): reply = thedict["inReplyTo"] msg = thedict["errorMessage"] if reply in ("configure", "compute"): - self.cmake.window.status_message(msg) + 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.cmake.window.active_view() + view = self.window.active_view() minimum = thedict["progressMinimum"] maximum = thedict["progressMaximum"] current = thedict["progressCurrent"] @@ -342,7 +348,7 @@ def receive_progress(self, thedict): view.set_status("cmake_" + thedict["inReplyTo"], status) def receive_message(self, thedict): - window = self.cmake.window + window = self.window if thedict["inReplyTo"] in ("configure", "compute"): name = "cmake.configure" else: @@ -371,7 +377,7 @@ def receive_signal(self, thedict): print(thedict) def dump_to_new_view(self, thedict, name): - view = self.cmake.window.new_file() + view = self.window.new_file() view.set_scratch(True) view.set_name(name) thedict.pop("type") @@ -388,5 +394,5 @@ def _check_for_errors_in_configure(self, view): errorcount = len(scopes) if errorcount > 0: self.bad_configure = True - self.cmake.window.run_command("show_panel", {"panel": "output.cmake.configure"}) + self.window.run_command("show_panel", {"panel": "output.cmake.configure"}) From b67e184a4fcc1f1f56b92130c8cc98cb1170d20c Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 8 Aug 2017 21:34:50 +0200 Subject: [PATCH 34/64] Recursively go up parent directories until we find the true cmake root --- commands/command.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/commands/command.py b/commands/command.py index a3b3e3b..2465f04 100644 --- a/commands/command.py +++ b/commands/command.py @@ -134,19 +134,22 @@ def on_load(self, view): return # We found a CMakeLists.txt file, but we might be embedded into a # larger project. Find the true root file. - 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 + 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") - if not cmake_file: - # We found the actual root of the project earlier. - cmake_file = old_cmake_file + 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 self.source_folder = os.path.dirname(cmake_file) print("found source folder:", self.source_folder) From 6c1da47980432f2ad78ad403101b58b56d3861ee Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Fri, 11 Aug 2017 23:53:56 +0200 Subject: [PATCH 35/64] Update "Edit Cache" command for v2 --- commands/edit_cache.py | 57 +++++++++++++++++++++++++----------------- server.py | 11 ++++++-- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 3ca98e9..185ed8b 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -1,27 +1,38 @@ -import sublime, sublime_plugin, os -from CMakeBuilder.support import * -from .command import ServerManager +import sublime +import sublime_plugin +import os +from .command import ServerManager, CmakeCommand +from ..support import capabilities -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 +if capabilities("serverMode"): - @classmethod - def description(cls): - return 'Edit Cache...' + class CmakeEditCacheCommand(CmakeCommand): - def run(self): - self.server = ServerManager.get(self.window) - if self.server: + def run(self): self.server.cache() - return - 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": "@"}) + +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 + + @classmethod + def description(cls): + return 'Edit Cache...' + + def run(self): + self.server = ServerManager.get(self.window) + if self.server: + self.server.cache() + return + 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": "@"}) diff --git a/server.py b/server.py index 9b6afaa..338e5cc 100644 --- a/server.py +++ b/server.py @@ -72,8 +72,15 @@ def on_data(self, _, data): for piece in data: if piece == ']== "CMake Server" ==]': self.inside_json_object = False - self.receive_dict(json.loads(self.data_parts)) - self.data_parts = '' + try: + d = json.loads(self.data_parts) + except ValueError as e: + print(str(e)) + sublime.error_message("Could not JSON-decode: " + self.data_parts) + else: + self.receive_dict(d) + finally: + self.data_parts = '' if self.inside_json_object: self.data_parts += piece if piece == '[== "CMake Server" ==[': From fa74f2141e85f79553d96284cd44bb5b84877cce Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sat, 12 Aug 2017 15:50:14 +0200 Subject: [PATCH 36/64] Fix data decoding Also add cmake_switch_scheme command, but it doesn't work yet Make an attempt at unit testing --- CMakeBuilder.sublime-commands | 4 ++ Main.sublime-menu | 3 ++ commands/edit_cache.py | 25 ++++++----- commands/open_build_folder.py | 54 +++++++++++++++++------- commands/switch_scheme.py | 11 +++++ server.py | 79 +++++++++++++++++++++-------------- tests/README.md | 2 +- tests/__init__.py | 0 8 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 commands/switch_scheme.py delete mode 100644 tests/__init__.py diff --git a/CMakeBuilder.sublime-commands b/CMakeBuilder.sublime-commands index fe1fdb4..5b9790c 100644 --- a/CMakeBuilder.sublime-commands +++ b/CMakeBuilder.sublime-commands @@ -55,6 +55,10 @@ "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/Main.sublime-menu b/Main.sublime-menu index da2cdcd..bda97f4 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -63,6 +63,9 @@ { "command": "cmake_show_configure_output" }, + { + "command": "cmake_switch_scheme" + }, { "command": "cmake_restart_server" } diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 185ed8b..6b68109 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -1,19 +1,30 @@ import sublime import sublime_plugin import os -from .command import ServerManager, CmakeCommand +from .command import CmakeCommand from ..support import capabilities + +class EditCacheMixin(object): + + @classmethod + def description(cls): + return "Edit Cache..." + + if capabilities("serverMode"): - class CmakeEditCacheCommand(CmakeCommand): + + class CmakeEditCacheCommand(CmakeCommand, EditCacheMixin): def run(self): self.server.cache() else: - class CmakeEditCacheCommand(sublime_plugin.WindowCommand): + + class CmakeEditCacheCommand(sublime_plugin.WindowCommand, EditCacheMixin): + """Edit an entry from the CMake cache.""" def is_enabled(self): try: @@ -23,15 +34,7 @@ def is_enabled(self): except Exception as e: return False - @classmethod - def description(cls): - return 'Edit Cache...' - def run(self): - self.server = ServerManager.get(self.window) - if self.server: - self.server.cache() - return 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")) diff --git a/commands/open_build_folder.py b/commands/open_build_folder.py index 7c8832d..bd04c3e 100644 --- a/commands/open_build_folder.py +++ b/commands/open_build_folder.py @@ -1,22 +1,44 @@ -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 +class OpenBuildFolderMixin(object): @classmethod def description(cls): - return 'Browse Build Folder…' + return "Browse Build Folder..." + + +if capabilities("cmakeServer"): + + + class CmakeOpenBuildFolderCommand(CmakeCommand, OpenBuildFolderMixin): + + def is_enabled(self): + return True - 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)}) + def run(self): + build_folder = self.server.cmake.build_folder + self.window.run_command("open_dir", args={"dir": os.path.realpath(build_folder)}) + +else: + + + class CmakeOpenBuildFolderCommand(sublime_plugin.WindowCommand, OpenBuildFolderMixin): + """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 + + 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/switch_scheme.py b/commands/switch_scheme.py new file mode 100644 index 0000000..d8ce218 --- /dev/null +++ b/commands/switch_scheme.py @@ -0,0 +1,11 @@ +from .command import CmakeCommand, ServerManager + + +class CmakeSwitchSchemeCommand(CmakeCommand): + + def run(self): + ServerManager._servers.pop(self.window.id(), None) + ServerManager.on_load(self.window.active_view()) + + def description(self): + return "Switch Scheme" diff --git a/server.py b/server.py index 338e5cc..0c1ef04 100644 --- a/server.py +++ b/server.py @@ -5,6 +5,7 @@ import time import threading + class Target(object): __slots__ = ("name", "fullname", "type", "directory", "config") @@ -42,6 +43,7 @@ def __init__(self, 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 = '' @@ -63,28 +65,53 @@ 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 - data = data.splitlines() - for piece in data: - if piece == ']== "CMake Server" ==]': - self.inside_json_object = False - try: - d = json.loads(self.data_parts) - except ValueError as e: - print(str(e)) - sublime.error_message("Could not JSON-decode: " + self.data_parts) - else: - self.receive_dict(d) - finally: - self.data_parts = '' + + while data: if self.inside_json_object: - self.data_parts += piece - if piece == '[== "CMake Server" ==[': - self.inside_json_object = True + 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( @@ -104,20 +131,8 @@ def send_dict(self, thedict): self.send(data) def send_handshake(self): - best_protocol = self.protocols[0] - for protocol in self.protocols: - if (protocol["major"] == self.protocol[0] and - protocol["minor"] == self.protocol[1]): - best_protocol = protocol - break - if protocol["isExperimental"] and not self.experimental: - continue - if protocol["major"] > best_protocol["major"]: - best_protocol = protocol - elif (protocol["major"] == best_protocol["major"] and - protocol["minor"] > best_protocol["minor"]): - best_protocol = protocol - self.protocol = best_protocol + self.protocol = {"major": 1, "minor": 0, + "isExperimental": True} self.send_dict({ "type": "handshake", "protocolVersion": self.protocol, @@ -176,9 +191,9 @@ def global_settings(self): self.send_dict({"type": "globalSettings"}) def receive_dict(self, thedict): - t = thedict["type"] + t = thedict.pop("type") if t == "hello": - self.protocols = thedict["supportedProtocolVersions"] + self.supported_protocols = thedict.pop("supportedProtocolVersions") self.send_handshake() elif t == "reply": self.receive_reply(thedict) diff --git a/tests/README.md b/tests/README.md index 63e0bdd..606090c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -10,4 +10,4 @@ or UnitTesting: Test Current File -To add a new test, inherit from ProjectFixtureTestCase. +To add a new test, inherit from TestCase defined in fixtures.py. diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From ab204a34a7c97be2a81cf5e84917981f7c802683 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sat, 12 Aug 2017 16:41:11 +0200 Subject: [PATCH 37/64] Various fixes --- commands/__init__.py | 1 + commands/command.py | 1 + commands/edit_cache.py | 17 ++++++++--------- commands/switch_scheme.py | 7 ++++--- server.py | 4 +--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/commands/__init__.py b/commands/__init__.py index df0b819..b3971bb 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -15,5 +15,6 @@ from .set_target import CmakeSetTargetCommand from .write_build_targets import CmakeWriteBuildTargetsCommand from .show_configure_output import CmakeShowConfigureOutputCommand +from .switch_scheme import CmakeSwitchSchemeCommand from .command import ServerManager from .command import CmakeRestartServerCommand diff --git a/commands/command.py b/commands/command.py index 2465f04..6d1078c 100644 --- a/commands/command.py +++ b/commands/command.py @@ -32,6 +32,7 @@ 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)) diff --git a/commands/edit_cache.py b/commands/edit_cache.py index 6b68109..77c0f81 100644 --- a/commands/edit_cache.py +++ b/commands/edit_cache.py @@ -5,17 +5,13 @@ from ..support import capabilities -class EditCacheMixin(object): - - @classmethod - def description(cls): - return "Edit Cache..." - - if capabilities("serverMode"): - class CmakeEditCacheCommand(CmakeCommand, EditCacheMixin): + class CmakeEditCacheCommand(CmakeCommand): + + def description(self): + return "Edit Cache..." def run(self): self.server.cache() @@ -23,7 +19,7 @@ def run(self): else: - class CmakeEditCacheCommand(sublime_plugin.WindowCommand, EditCacheMixin): + class CmakeEditCacheCommand(sublime_plugin.WindowCommand): """Edit an entry from the CMake cache.""" def is_enabled(self): @@ -34,6 +30,9 @@ def is_enabled(self): 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()) diff --git a/commands/switch_scheme.py b/commands/switch_scheme.py index d8ce218..c8dcf7b 100644 --- a/commands/switch_scheme.py +++ b/commands/switch_scheme.py @@ -4,8 +4,9 @@ class CmakeSwitchSchemeCommand(CmakeCommand): def run(self): - ServerManager._servers.pop(self.window.id(), None) - ServerManager.on_load(self.window.active_view()) + ServerManager._servers.pop(self.window.id(), None)x + self.window.focus_view(self.window.active_view()) - def description(self): + @classmethod + def description(cls): return "Switch Scheme" diff --git a/server.py b/server.py index 0c1ef04..350404b 100644 --- a/server.py +++ b/server.py @@ -234,8 +234,7 @@ def receive_reply(self, thedict): elif reply == "cmakeInputs": self.dump_to_new_view(thedict, "CMake Inputs") elif reply == "globalSettings": - thedict.pop("type") - thedict.pop("inReplyTo") + # thedict.pop("inReplyTo") thedict.pop("cookie") thedict.pop("capabilities") self.items = [] @@ -402,7 +401,6 @@ def dump_to_new_view(self, thedict, name): view = self.window.new_file() view.set_scratch(True) view.set_name(name) - thedict.pop("type") thedict.pop("inReplyTo") thedict.pop("cookie") view.run_command( From b149cde62a3eda3ba851f56ab0a7ae8b1daf5d48 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sat, 12 Aug 2017 16:43:17 +0200 Subject: [PATCH 38/64] Rename build system, and fix syntax mistake --- CMake.sublime-build => CMakeBuilder.sublime-build | 2 -- commands/switch_scheme.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) rename CMake.sublime-build => CMakeBuilder.sublime-build (66%) diff --git a/CMake.sublime-build b/CMakeBuilder.sublime-build similarity index 66% rename from CMake.sublime-build rename to CMakeBuilder.sublime-build index 75af317..2fa13f2 100644 --- a/CMake.sublime-build +++ b/CMakeBuilder.sublime-build @@ -3,9 +3,7 @@ "selector": "source.cmake | source.c | source.c++", "variants": { - "target": "cmake_build", "name": "Select & Build Target", - "selector": "source.cmake | source.c | source.c++", "select_target": true } } diff --git a/commands/switch_scheme.py b/commands/switch_scheme.py index c8dcf7b..cefe89f 100644 --- a/commands/switch_scheme.py +++ b/commands/switch_scheme.py @@ -4,7 +4,7 @@ class CmakeSwitchSchemeCommand(CmakeCommand): def run(self): - ServerManager._servers.pop(self.window.id(), None)x + ServerManager._servers.pop(self.window.id(), None) self.window.focus_view(self.window.active_view()) @classmethod From dedf19b09c8aee43ea7de37c9d4879e2c71a563b Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sat, 12 Aug 2017 16:56:51 +0200 Subject: [PATCH 39/64] Made all methods of ServerManager classmethods, so we can access them --- commands/command.py | 223 ++++++++++++++++++---------------- commands/open_build_folder.py | 23 ++-- commands/switch_scheme.py | 2 +- 3 files changed, 128 insertions(+), 120 deletions(-) diff --git a/commands/command.py b/commands/command.py index 6d1078c..14b664f 100644 --- a/commands/command.py +++ b/commands/command.py @@ -75,33 +75,34 @@ def get(cls, window): return cls._servers.get(window.id(), None) def __init__(self): - self._is_selecting = False - self.generator = "" - self.source_folder = "" - self.build_folder = "" - self.toolset = "" - self.platform = "" + self.__class__._is_selecting = False + self.__class__.generator = "" + self.__class__.source_folder = "" + self.__class__.build_folder = "" + self.__class__.toolset = "" + self.__class__.platform = "" - def on_load(self, view): + @classmethod + def on_load(cls, view): if not capabilities("serverMode"): print("CMakeBuilder: cmake is not capable of server mode") return - if self._is_selecting: + 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. - self.window = view.window() - if not self.window: + cls.window = view.window() + if not cls.window: return - server = self.get(self.window) + server = cls.get(cls.window) if server: return # No server running. Check if there are build settings. - data = self.window.project_data() + data = cls.window.project_data() settings = data.get("settings", None) if not settings or not isinstance(settings, dict): print("no settings") @@ -110,10 +111,10 @@ def on_load(self, view): if not cmake or not isinstance(cmake, dict): print("no cmake dict") return - self.schemes = cmake.get("schemes", None) - if (not self.schemes or - not isinstance(self.schemes, list) or - len(self.schemes) == 0): + cls.schemes = cmake.get("schemes", None) + if (not cls.schemes or + not isinstance(cls.schemes, list) or + len(cls.schemes) == 0): print("no schemes") return @@ -151,169 +152,177 @@ def on_load(self, view): # We found the actual root of the project earlier. cmake_file = old_cmake_file break - self.source_folder = os.path.dirname(cmake_file) - print("found source folder:", self.source_folder) + 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. - self.items = [] - for scheme in self.schemes: + 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.") - self.items.append(["INVALID SCHEME", ""]) + cls.items.append(["INVALID SCHEME", ""]) continue name = scheme.get("name", "Untitled Scheme") build_folder = scheme.get("build_folder", "${project_path}/build") - variables = self.window.extract_variables() + variables = cls.window.extract_variables() build_folder = sublime.expand_variables(build_folder, variables) - self.items.append([name, build_folder]) - if len(self.schemes) == 0: + cls.items.append([name, build_folder]) + if len(cls.schemes) == 0: print("found schemes dict, but it is empty") return - self._is_selecting = True - if len(self.schemes) == 1: + cls._is_selecting = True + if len(cls.schemes) == 1: # Select the only scheme possible. - self._on_done_select_scheme(0) + cls._on_done_select_scheme(0) else: # Ask the user what he/she wants. - self.window.show_quick_panel(self.items, - self._on_done_select_scheme) + cls.window.show_quick_panel(cls.items, + cls._on_done_select_scheme) - def _on_done_select_scheme(self, index): + @classmethod + def _on_done_select_scheme(cls, index): if index == -1: - self._is_selecting = False + cls._is_selecting = False return - self.name = self.items[index][0] - if self.name == "INVALID SCHEME": - self._is_selecting = False + cls.name = cls.items[index][0] + if cls.name == "INVALID SCHEME": + cls._is_selecting = False return - self.build_folder_pre_expansion = self.schemes[index]["build_folder"] - self.build_folder = sublime.expand_variables( - self.build_folder_pre_expansion, self.window.extract_variables()) - self.command_line_overrides = self.schemes[index].get( + 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", {}) - self._select_generator() + cls._select_generator() - def _select_generator(self): - if self.generator: - self._select_toolset() + @classmethod + def _select_generator(cls): + if cls.generator: + cls._select_toolset() return - self.items = [] + 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) - self.items.append([g["name"], platform_support, toolset_support]) - if len(self.items) == 1: - self._on_done_select_generator(0) + cls.items.append([g["name"], platform_support, toolset_support]) + if len(cls.items) == 1: + cls._on_done_select_generator(0) else: - self.window.show_quick_panel(self.items, - self._on_done_select_generator) + cls.window.show_quick_panel(cls.items, + cls._on_done_select_generator) - def _on_done_select_generator(self, index): + @classmethod + def _on_done_select_generator(cls, index): if index == 1: - self._is_selecting = False + cls._is_selecting = False return - self.generator = self.items[index][0] - platform_support = self.items[index][1] - toolset_support = self.items[index][2] - self.platform_support = True if "True" in platform_support else False - self.toolset_support = True if "True" in toolset_support else False - if self.platform_support: + 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 + if cls.platform_support: text = "Platform for {} (Press Enter for default): ".format( - self.generator) - self.window.show_input_panel(text, "", - self._on_done_select_platform, + cls.generator) + cls.window.show_input_panel(text, "", + cls._on_done_select_platform, None, None) - elif self.toolset_support: - self._select_toolset() + elif cls.toolset_support: + cls._select_toolset() else: - self._run_configure_with_new_settings() + cls._run_configure_with_new_settings() - def _select_toolset(self): - if self.toolset: + @classmethod + def _select_toolset(cls): + if cls.toolset: return text = "Toolset for {}: (Press Enter for default): ".format( - self.generator) - self.window.show_input_panel(text, "", self._on_done_select_toolset, + cls.generator) + cls.window.show_input_panel(text, "", cls._on_done_select_toolset, None, None) - def _on_done_select_platform(self, platform): - self.platform = platform - if self.toolset_support: - self._select_toolset() + @classmethod + def _on_done_select_platform(cls, platform): + cls.platform = platform + if cls.toolset_support: + cls._select_toolset() else: - self._run_configure_with_new_settings() + cls._run_configure_with_new_settings() - def _on_done_select_toolset(self, toolset): - self.toolset = toolset - self._run_configure_with_new_settings() + @classmethod + def _on_done_select_toolset(cls, toolset): + cls.toolset = toolset + cls._run_configure_with_new_settings() - def _run_configure_with_new_settings(self): - self._is_selecting = False + @classmethod + def _run_configure_with_new_settings(cls): + cls._is_selecting = False cmake_settings = CMakeSettings() - cmake_settings.source_folder = self.source_folder - cmake_settings.build_folder = self.build_folder + cmake_settings.source_folder = cls.source_folder + cmake_settings.build_folder = cls.build_folder cmake_settings.build_folder_pre_expansion = \ - self.build_folder_pre_expansion + cls.build_folder_pre_expansion - cmake_settings.generator = self.generator - cmake_settings.platform = self.platform - cmake_settings.toolset = self.toolset - cmake_settings.command_line_overrides = self.command_line_overrides + 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 self.generator: + if "Makefile" in cls.generator: cmake_settings.syntax = \ "Packages/CMakeBuilder/Syntax/Make.sublime-syntax" - elif "Ninja" in self.generator: + elif "Ninja" in cls.generator: cmake_settings.syntax = \ "Packages/CMakeBuilder/Syntax/Ninja.sublime-syntax" else: - print("CMakeBuilder: Warning: Generator", self.generator, + print("CMakeBuilder: Warning: Generator", cls.generator, "will not have syntax highlighting in the output panel.") elif sublime.platform() == "windows": - if "Ninja" in self.generator: + 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 self.generator: + 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 self.generator: + 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", self.generator, + print("CMakeBuilder: Warning: Generator", cls.generator, "will not have syntax highlighting in the output panel.") else: sublime.error_message("Unknown platform: " + sublime.platform()) return - server = Server(self.window, cmake_settings) - self.source_folder = "" - self.build_folder = "" - self.build_folder_pre_expansion = "" - self.generator = "" - self.platform = "" - self.toolset = "" - self.items = [] - self.schemes = [] - self.command_line_overrides = {} - self.__class__._servers[self.window.id()] = server + 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 - def on_activated(self, view): - self.on_load(view) + @classmethod + def on_activated(cls, view): + cls.on_load(view) try: - server = self.__class__.get(view.window()) + server = cls.get(view.window()) path = os.path.join(server.cmake.build_folder, "CMakeFiles", "CMakeBuilder", @@ -324,8 +333,8 @@ def on_activated(self, view): except Exception as e: view.erase_status("cmake_active_target") - - def on_post_save(self, view): + @classmethod + def on_post_save(cls, view): if not view: return if not get_setting(view, "configure_on_save", False): @@ -334,7 +343,7 @@ def on_post_save(self, view): if not name: return if name.endswith(".sublime-project"): - server = self.__class__.get(view.window()) + server = cls.get(view.window()) if not server: _configure(view.window()) else: diff --git a/commands/open_build_folder.py b/commands/open_build_folder.py index bd04c3e..14b9b11 100644 --- a/commands/open_build_folder.py +++ b/commands/open_build_folder.py @@ -5,20 +5,15 @@ from ..support import capabilities -class OpenBuildFolderMixin(object): +if capabilities("serverMode"): - @classmethod - def description(cls): - return "Browse Build Folder..." + class CmakeOpenBuildFolderCommand(CmakeCommand): + """Opens the build folder.""" -if capabilities("cmakeServer"): - - - class CmakeOpenBuildFolderCommand(CmakeCommand, OpenBuildFolderMixin): - - def is_enabled(self): - return True + @classmethod + def description(cls): + return "Browse Build Folder..." def run(self): build_folder = self.server.cmake.build_folder @@ -27,9 +22,13 @@ def run(self): else: - class CmakeOpenBuildFolderCommand(sublime_plugin.WindowCommand, OpenBuildFolderMixin): + 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"] diff --git a/commands/switch_scheme.py b/commands/switch_scheme.py index cefe89f..aa6ce31 100644 --- a/commands/switch_scheme.py +++ b/commands/switch_scheme.py @@ -5,7 +5,7 @@ class CmakeSwitchSchemeCommand(CmakeCommand): def run(self): ServerManager._servers.pop(self.window.id(), None) - self.window.focus_view(self.window.active_view()) + ServerManager.on_activated(self.window.active_view()) @classmethod def description(cls): From 911363e7128617a3e0d93b1f528e6910a7baa2a5 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Mon, 14 Aug 2017 23:43:49 +0200 Subject: [PATCH 40/64] Minor improvements --- CMakeBuilder.sublime-build | 1 + commands/command.py | 36 ++++++++++++++++++++++++++++++------ server.py | 2 ++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CMakeBuilder.sublime-build b/CMakeBuilder.sublime-build index 2fa13f2..f36ea9b 100644 --- a/CMakeBuilder.sublime-build +++ b/CMakeBuilder.sublime-build @@ -4,6 +4,7 @@ "variants": { "name": "Select & Build Target", + "selector": "source.cmake | source.c | source.c++", "select_target": true } } diff --git a/commands/command.py b/commands/command.py index 14b664f..21c15cb 100644 --- a/commands/command.py +++ b/commands/command.py @@ -1,6 +1,7 @@ import sublime_plugin import sublime import os +import pickle from ..server import Server from ..support import capabilities from ..support import get_setting @@ -212,11 +213,11 @@ def _select_generator(cls): cls._on_done_select_generator(0) else: cls.window.show_quick_panel(cls.items, - cls._on_done_select_generator) + cls._on_done_select_generator) @classmethod def _on_done_select_generator(cls, index): - if index == 1: + if index == -1: cls._is_selecting = False return cls.generator = cls.items[index][0] @@ -224,12 +225,14 @@ def _on_done_select_generator(cls, index): 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) + cls._on_done_select_platform, + None, None) elif cls.toolset_support: cls._select_toolset() else: @@ -238,15 +241,18 @@ def _on_done_select_generator(cls, index): @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) + 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: @@ -255,6 +261,7 @@ def _on_done_select_platform(cls, platform): @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 @@ -305,7 +312,24 @@ def _run_configure_with_new_settings(cls): 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 = "" diff --git a/server.py b/server.py index 350404b..01fe126 100644 --- a/server.py +++ b/server.py @@ -3,6 +3,8 @@ import sublime import copy import time +import os +import pickle import threading From 91c4fdb114e2aebdce599e187b953828fd03e7a5 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 15 Aug 2017 21:12:30 +0200 Subject: [PATCH 41/64] Improve build command, but it still has bugs --- commands/build.py | 66 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/commands/build.py b/commands/build.py index c0ee7eb..3df3bc9 100644 --- a/commands/build.py +++ b/commands/build.py @@ -73,25 +73,7 @@ def _on_done(self, index): sublime.error_message("Unknown type: " + type(index)) return if target.type == "RUN": - if sublime.platform() in ("linux", "osx"): - prefix = "./" - else: - prefix = "" - try: - import TerminalView # will throw if not present - self.window.run_command( - "terminal_view_exec", { - "cmd": [prefix + target.fullname], - "working_dir": target.directory - }) - except Exception: - self.window.run_command( - "cmake_exec", { - "window_id": self.window.id(), - "shell_cmd": prefix + target.fullname, - "working_dir": target.directory - } - ) + self._handle_run_target(target) else: self.window.run_command( "cmake_exec", { @@ -102,3 +84,49 @@ def _on_done(self, index): "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 + # cmd.extend(["&&", target.directory + "/" + prefix + target.fullname]) + try: + if sublime.platform() == "osx": + cmd = ["/bin/bash", "-l", "-c", " ".join(cmd)] + elif sublime.platform() == "linux": + cmd = ["/bin/bash", "-c", " ".join(cmd)] + 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(), + "shell_cmd": " ".join(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 # NOQA will throw if not present + self.window.run_command( + "terminal_view_exec", { + "cmd": cmd, + "working_dir": self.server.cmake.build_folder + }) From fdc87152a3a9ddf09710fb76a7472e5d20a433c3 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 15 Aug 2017 21:12:38 +0200 Subject: [PATCH 42/64] Add 2.0.0-alpha message --- messages/2.0.0-alpha.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 messages/2.0.0-alpha.txt 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 From 583577e8593515182e6da6867d01f5852a55cf6d Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 15 Aug 2017 23:19:39 +0200 Subject: [PATCH 43/64] A brute-force solution to the "build and run" problem We create a three-line shell-script that lives in $build_folder/CMakefiles/CMakeBuilder/runtargets and does the right thing. --- commands/build.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/commands/build.py b/commands/build.py index 3df3bc9..3015368 100644 --- a/commands/build.py +++ b/commands/build.py @@ -100,12 +100,21 @@ def _handle_run_target(self, target): 'target for "run" target ' + target.name) return - # cmd.extend(["&&", target.directory + "/" + prefix + target.fullname]) + path = os.path.join(self.server.cmake.build_folder, "CMakeFiles", + "CMakeBuilder", "runtargets") + if not os.path.isdir(path): + os.makedirs(path, exist_ok=True) + path = os.path.join(path, t.name + ".sh") + if not os.path.isfile(path): + with open(path, "w") as f: + f.write(" ".join(cmd) + "\n") + f.write("cd " + t.directory + "\n") + f.write(prefix + t.fullname + "\n") try: if sublime.platform() == "osx": - cmd = ["/bin/bash", "-l", "-c", " ".join(cmd)] + cmd = ["/bin/bash", "-l", path] elif sublime.platform() == "linux": - cmd = ["/bin/bash", "-c", " ".join(cmd)] + cmd = ["/bin/bash", path] elif sublime.platform() == "windows": raise ImportError else: From acaa5311184efaf2ad1164db6dbdb67f362a0f99 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 15 Aug 2017 23:37:57 +0200 Subject: [PATCH 44/64] More granular control over what gets overwritten --- CMakeBuilder.sublime-settings | 16 ++++++++++++++++ server.py | 26 ++++++++++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CMakeBuilder.sublime-settings b/CMakeBuilder.sublime-settings index 222e8bf..9fde24c 100644 --- a/CMakeBuilder.sublime-settings +++ b/CMakeBuilder.sublime-settings @@ -18,6 +18,22 @@ //========================================================================== + // 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. diff --git a/server.py b/server.py index 01fe126..f361105 100644 --- a/server.py +++ b/server.py @@ -6,6 +6,7 @@ import os import pickle import threading +import shutil class Target(object): @@ -302,12 +303,25 @@ def on_done_input(new_value): self.cmake.build_folder, "")) data = self.window.project_data() self.targets = list(self.targets) - data["settings"]["compile_commands"] = \ - self.cmake.build_folder_pre_expansion - data["settings"]["ecc_flags_sources"] = [{ - "file": "compile_commands.json", - "search_in": self.cmake.build_folder_pre_expansion}] - self.window.set_project_data(data) + path = os.path.join(self.cmake.build_folder, + "compile_commands.json") + if os.path.isfile(path): + 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) elif reply == "cache": cache = thedict.pop("cache") self.items = [] From 3d133d974ea1b58a584ce50978e112e3157b60d6 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Tue, 15 Aug 2017 23:52:30 +0200 Subject: [PATCH 45/64] Add thirdparty compdb python package See: https://github.com/Sarcasm/compdb --- thirdparty/__init__.py | 0 thirdparty/compdb/__about__.py | 14 + thirdparty/compdb/__init__.py | 28 ++ thirdparty/compdb/__main__.py | 13 + thirdparty/compdb/cli.py | 556 +++++++++++++++++++++ thirdparty/compdb/complementer/__init__.py | 39 ++ thirdparty/compdb/complementer/headerdb.py | 281 +++++++++++ thirdparty/compdb/config.py | 279 +++++++++++ thirdparty/compdb/core.py | 217 ++++++++ thirdparty/compdb/db/__init__.py | 0 thirdparty/compdb/db/json.py | 118 +++++ thirdparty/compdb/db/memory.py | 25 + thirdparty/compdb/filelist.py | 61 +++ thirdparty/compdb/models.py | 61 +++ thirdparty/compdb/utils.py | 78 +++ 15 files changed, 1770 insertions(+) create mode 100644 thirdparty/__init__.py create mode 100644 thirdparty/compdb/__about__.py create mode 100644 thirdparty/compdb/__init__.py create mode 100644 thirdparty/compdb/__main__.py create mode 100644 thirdparty/compdb/cli.py create mode 100644 thirdparty/compdb/complementer/__init__.py create mode 100644 thirdparty/compdb/complementer/headerdb.py create mode 100644 thirdparty/compdb/config.py create mode 100644 thirdparty/compdb/core.py create mode 100644 thirdparty/compdb/db/__init__.py create mode 100644 thirdparty/compdb/db/json.py create mode 100644 thirdparty/compdb/db/memory.py create mode 100644 thirdparty/compdb/filelist.py create mode 100644 thirdparty/compdb/models.py create mode 100644 thirdparty/compdb/utils.py diff --git a/thirdparty/__init__.py b/thirdparty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/thirdparty/compdb/__about__.py b/thirdparty/compdb/__about__.py new file mode 100644 index 0000000..fc0a7a1 --- /dev/null +++ b/thirdparty/compdb/__about__.py @@ -0,0 +1,14 @@ +__all__ = [ + '__author__', + '__desc__', + '__prog__', + '__url__', + '__version__', +] + +# these variables are used by the module and also by the setup.py +__author__ = 'Guillaume Papin' +__desc__ = '''The compilation database Swiss army knife''' +__prog__ = 'compdb' +__url__ = 'https://github.com/Sarcasm/compdb' +__version__ = '0.1.1' diff --git a/thirdparty/compdb/__init__.py b/thirdparty/compdb/__init__.py new file mode 100644 index 0000000..b9bc464 --- /dev/null +++ b/thirdparty/compdb/__init__.py @@ -0,0 +1,28 @@ +from __future__ import print_function, unicode_literals, absolute_import + +from compdb.__about__ import ( + __author__, + __url__, + __desc__, + __prog__, + __version__, ) + +__all__ = [ + '__author__', + '__desc__', + '__prog__', + '__url__', + '__version__', +] + + +class CompdbError(Exception): + '''Base exception for errors raised by compdb''' + + def __init__(self, message, cause=None): + super(CompdbError, self).__init__(message) + self.cause = cause + + +class NotImplementedError(NotImplementedError, CompdbError): + pass diff --git a/thirdparty/compdb/__main__.py b/thirdparty/compdb/__main__.py new file mode 100644 index 0000000..540440e --- /dev/null +++ b/thirdparty/compdb/__main__.py @@ -0,0 +1,13 @@ +from __future__ import print_function, unicode_literals, absolute_import + +import os +import sys + +# allow invokation of the style 'python /path/to/compdb' +if __package__ == '': + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from compdb.cli import main # noqa: E402 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/thirdparty/compdb/cli.py b/thirdparty/compdb/cli.py new file mode 100644 index 0000000..3eec9e7 --- /dev/null +++ b/thirdparty/compdb/cli.py @@ -0,0 +1,556 @@ +from __future__ import print_function, unicode_literals, absolute_import + +import argparse +import io +import os +import sys +import textwrap + +import compdb.config +import compdb.db.json +import compdb.complementer.headerdb +from compdb.__about__ import (__desc__, __prog__, __version__) +from compdb import (filelist, utils) +from compdb.db.json import JSONCompileCommandSerializer +from compdb.core import CompilationDatabase + + +# http://python-3-patterns-idioms-test.readthedocs.org/en/latest/Metaprogramming.html#example-self-registration-of-subclasses +class CommandRegistry(type): + def __init__(self, name, bases, nmspc): + super(CommandRegistry, self).__init__(name, bases, nmspc) + if not hasattr(self, 'registry'): + self.registry = set() + # keep only leaf classes + self.registry -= set(bases) + self.registry.add(self) + + def __iter__(self): + return iter(sorted(self.registry, key=lambda c: c.name)) + + def __str__(self): + if self in self.registry: + return self.__name__ + return self.__name__ + ": " + ", ".join([sc.__name__ for sc in self]) + + +if sys.version_info[0] < 3: + + class RegisteredCommand(): + __metaclass__ = CommandRegistry +else: + # Probably a bad idea but the syntax is incompatible in python2 + exec('class RegisteredCommand(metaclass=CommandRegistry): pass') + + +class Environment(object): + def __init__(self): + self.__complementers = {} + + def register_complementer(self, name, cls): + self.__complementers[name] = cls + + def get_complementer(self, name): + return self.__complementers[name] + + +class CommandBase(RegisteredCommand): + def __init__(self): + self.env = None + self.config = None + self.args = None + self._database = None + + def set_env(self, env): + self.env = env + + def set_config(self, config): + self.config = config + + def set_args(self, args): + self.args = args + + def get_database_classes(self): + return [compdb.db.json.JSONCompilationDatabase] + + def make_unpopulated_database(self): + db = CompilationDatabase() + for database_cls in self.get_database_classes(): + db.register_backend(database_cls) + for complementer_name in (self.config.compdb.complementers or []): + complementer_cls = self.env.get_complementer(complementer_name) + db.add_complementer(complementer_name, complementer_cls()) + return db + + def populate_database(self, database): + try: + if self.args.build_paths: + database.add_directories(self.args.build_paths) + elif self.config.compdb.build_dir: + database.add_directory_patterns(self.config.compdb.build_dir) + except compdb.models.ProbeError as e: + print("error: invalid database(s): {}".format(e), file=sys.stderr) + sys.exit(1) + + def make_database(self): + db = self.make_unpopulated_database() + self.populate_database(db) + return db + + +class HelpCommand(CommandBase): + name = 'help' + help_short = 'display this help' + + def execute(self): + print('compdb: {}'.format(__desc__)) + print() + print('usage: compdb [general options] ' + 'command [command options] [command arguments]') + print() + print('available commands:') + command_max_len = max(map(len, [c.name for c in RegisteredCommand])) + for c in RegisteredCommand: + print(" {c.name:<{max_len}} {c.help_short}".format( + c=c, max_len=command_max_len)) + + +class VersionCommand(CommandBase): + name = 'version' + help_short = 'display this version of {}'.format(__prog__) + + @classmethod + def options(cls, parser): + parser.add_argument( + '--short', action='store_true', help='machine readable version') + + def execute(self): + if self.args.short: + print(__version__) + else: + print(__prog__, "version", __version__) + + +class ConfigCommand(CommandBase): + name = 'config' + help_short = 'get directory and global configuration options' + + @classmethod + def options(cls, parser): + # http://stackoverflow.com/a/18283730/951426 + # http://bugs.python.org/issue9253#msg186387 + subparsers = parser.add_subparsers( + title='available subcommands', + metavar='', + dest='subcommand') + subparsers.dest = 'subcommand' + # subcommand seems to be required by default in python 2.7 but not 3.5, + # forcing it to true limit the differences between the two + subparsers.required = True + + subparser = subparsers.add_parser( + 'print-user-conf', + help='print the user configuration path', + formatter_class=SubcommandHelpFormatter) + subparser.set_defaults(config_func=cls.execute_print_user_conf) + + subparser = subparsers.add_parser( + 'print-local-conf', + help='print the project local configuration', + formatter_class=SubcommandHelpFormatter) + subparser.set_defaults(config_func=cls.execute_print_local_conf) + + subparser = subparsers.add_parser( + 'list', + help='list all the configuration keys', + formatter_class=SubcommandHelpFormatter) + subparser.set_defaults(config_func=cls.execute_list) + + subparser = subparsers.add_parser( + 'dump', + help='dump effective configuration', + formatter_class=SubcommandHelpFormatter) + subparser.set_defaults(config_func=cls.execute_dump) + + subparser = subparsers.add_parser( + 'get', + help='get configuration variable effective value', + formatter_class=SubcommandHelpFormatter) + subparser.set_defaults(config_func=cls.execute_get) + subparser.add_argument('key', help='the value to get: SECTION.VAR') + + def execute(self): + self.args.config_func(self) + + def execute_print_user_conf(self): + print(compdb.config.get_user_conf()) + + def execute_print_local_conf(self): + local_conf = compdb.config.get_local_conf() + if not local_conf: + print("error: local configuration not found", file=sys.stderr) + sys.exit(1) + print(local_conf) + + def execute_list(self): + for key in sorted(self.config.options()): + print(key) + + def execute_get(self): + section, option = compdb.config.parse_key(self.args.key) + section = getattr(self.config, section) + print(getattr(section, option)) + + def execute_dump(self): + self.config.get_effective_configuration().write(sys.stdout) + + +class ListCommand(CommandBase): + name = 'list' + help_short = 'list database entries' + + @classmethod + def options(cls, parser): + parser.add_argument( + '-1', + '--unique', + action='store_true', + help='restrict results to a single entry per file') + parser.add_argument( + 'files', nargs='*', help='restrict results to a list of files') + + def _gen_results(self): + database = self.make_database() + for file in self.args.files or [None]: + if file: + compile_commands = database.get_compile_commands( + file, unique=self.args.unique) + else: + compile_commands = database.get_all_compile_commands( + unique=self.args.unique) + yield (file, compile_commands) + + def execute(self): + has_missing_files = False + with JSONCompileCommandSerializer( + utils.stdout_unicode_writer()) as serializer: + for file, compile_commands in self._gen_results(): + has_compile_command = False + for compile_command in compile_commands: + serializer.serialize(compile_command) + has_compile_command = True + if file and not has_compile_command: + print( + 'error: {}: no such entry'.format(file), + file=sys.stderr) + has_missing_files = True + if has_missing_files: + sys.exit(1) + + +class UpdateCommand(CommandBase): + name = 'update' + help_short = 'update or create complementary databases' + + def execute(self): + if not self.config.compdb.complementers: + print( + 'error: no complementers configured ' + '(config compdb.complementers)', + file=sys.stderr) + sys.exit(1) + + database = self.make_unpopulated_database() + database.raise_on_missing_cache = False + self.populate_database(database) + + for state, update in database.update_complements(): + if state == 'begin': + print('Start {complementer}:'.format(**update)) + elif state == 'end': + pass # no visual feedback on purpose for this one + elif state == 'saving': + print(" OUT {file}".format(**update)) + else: + print("unsupported: {}: {}".format(state, update)) + + +def _get_suppressions_patterns_from_file(path): + patterns = [] + with io.open(path, 'r', encoding='utf-8') as f: + for line in f: + pattern = line.partition('#')[0].rstrip() + if pattern: + patterns.append(pattern) + return patterns + + +def _make_file_scanner(config, args): + scanner = filelist.FileScanner() + scanner.add_suppressions(args.suppress) + scanner.add_suppressions(config.scan_files.suppress or []) + for supp_file in args.suppressions_file: + scanner.add_suppressions( + _get_suppressions_patterns_from_file(supp_file)) + for supp_file in (config.scan_files.suppressions_files or []): + scanner.add_suppressions( + _get_suppressions_patterns_from_file(supp_file)) + groups = args.groups.split(',') + for group in groups: + scanner.enable_group(group) + return scanner + + +class ScanFilesCommand(CommandBase): + name = 'scan-files' + help_short = 'scan directory for source files' + help_detail = """ + Lookup given paths for source files. + + Source files includes C, C++ files, headers, and more. + """ + + @classmethod + def config_schema(cls, schema): + schema.register_path_list('suppressions-files', + 'files containing suppress patterns') + schema.register_string_list('suppress', + 'ignore files matching these patterns') + + @classmethod + def options(cls, parser): + parser.add_argument( + '--suppress', + metavar='pattern', + action='append', + default=[], + help='ignore files matching the given pattern') + parser.add_argument( + '--suppressions-file', + metavar='file', + action='append', + default=[], + help='add suppress patterns from file') + parser.add_argument( + '-g', + '--groups', + help="restrict search to files of the groups [source,header]", + default="source,header") + parser.add_argument( + 'path', + nargs='*', + default=["."], + help="search path(s) (default: %(default)s)") + + def execute(self): + scanner = _make_file_scanner(self.config, self.args) + # join is to have a path separator at the end + prefix_to_skip = os.path.join(os.path.abspath('.'), '') + for path in scanner.scan_many(self.args.path): + # make descendant paths relative + if path.startswith(prefix_to_skip): + path = path[len(prefix_to_skip):] + print(path) + + +class CheckCommand(CommandBase): + name = 'check' + help_short = 'report files absent from the compilation database(s)' + help_detail = """ + Report files that are found in the workspace + but not in the compilation database. + And files that are in the compilation database + but not found in the workspace. + + Exit with status 1 if some file in the workspace + aren't found in the compilation database. + """ + + @classmethod + def options(cls, parser): + ScanFilesCommand.options(parser) + + def execute(self): + scanner = _make_file_scanner(self.config, self.args) + database = self.make_database() + db_files = frozenset(database.get_all_files()) + list_files = frozenset(scanner.scan_many(self.args.path)) + + # this only is not a hard error, files may be in system paths or build + # directory for example + db_only = db_files - list_files + if db_only: + self._print_set_summary(db_only, "compilation database(s)") + + list_only = list_files - db_files + + if not list_only: + sys.exit(0) + + # print difference an exit with error + self._print_set_summary(list_only, "project(s)") + print( + "error: {} file(s) are missing from the compilation database(s)". + format(len(list_only)), + file=sys.stderr) + sys.exit(1) + + @staticmethod + def _print_set_summary(files, name): + print("Only in {}:".format(name)) + cwd = os.getcwd() + for path in sorted(files): + if path.startswith(cwd): + pretty_filename = os.path.relpath(path) + else: + pretty_filename = path + print(' {}'.format(pretty_filename)) + + +# remove the redundant metavar from help output +# +# usage: foo +# +# : +# # <- remove this +# command_a a description +# command_b b description +# +# http://stackoverflow.com/a/13429281/951426 +class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter): + def _format_action(self, action): + parts = super(argparse.RawDescriptionHelpFormatter, + self)._format_action(action) + if action.nargs == argparse.PARSER: + parts = "\n".join(parts.split("\n")[1:]) + return parts + + +def term_columns(): + columns = 80 + + try: + # can happens in tests, when we redirect sys.stdout to a StringIO + stdout_fileno = sys.stdout.fileno() + except (AttributeError, io.UnsupportedOperation): + # fileno() is an AttributeError on Python 2 for StringIO.StringIO + # and io.UnsupportedOperation in Python 3 for io.StringIO + stdout_fileno = None + + if stdout_fileno is not None and os.isatty(stdout_fileno): + try: + columns = int(os.environ["COLUMNS"]) + except (KeyError, ValueError): + try: + import fcntl, termios, struct # noqa: E401 + columns = struct.unpack( + 'HHHH', + fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0)))[1] + except (ImportError, IOError): + pass + return columns + + +def _wrap_paragraphs(text, max_width=None): + paragraph_width = term_columns() + if max_width and paragraph_width > max_width: + paragraph_width = max_width + if paragraph_width > 2: + paragraph_width -= 2 + paragraphs = text.split('\n\n') + return "\n\n".join( + [textwrap.fill(p.strip(), width=paragraph_width) for p in paragraphs]) + + +def setup(env): + env.register_complementer('headerdb', + compdb.complementer.headerdb.Complementer) + + +def main(argv=None): + parser = argparse.ArgumentParser( + description='{}.'.format(__desc__), + formatter_class=SubcommandHelpFormatter) + + parser.add_argument( + '-c', + dest='config_overrides', + metavar='NAME[=VALUE]', + action='append', + default=[], + help='set value of configuration variable NAME for this invocation') + parser.add_argument( + '-p', + dest='build_paths', + metavar="BUILD-DIR", + action='append', + default=[], + help='build path(s)') + + # http://stackoverflow.com/a/18283730/951426 + # http://bugs.python.org/issue9253#msg186387 + subparsers = parser.add_subparsers( + title='available commands', metavar='', dest='command') + subparsers.dest = 'command' + # subcommand seems to be required by default in python 2.7 but not 3.5, + # forcing it to true limit the differences between the two + subparsers.required = True + + env = Environment() + setup(env) + + config_schema = compdb.config.ConfigSchema() + compdb_schema = config_schema.get_section_schema('compdb') + compdb_schema.register_glob_list('build-dir', 'the build directories') + compdb_schema.register_string_list('complementers', 'complementers to use') + for command_cls in RegisteredCommand: + command_description = command_cls.help_short.capitalize() + if not command_description.endswith('.'): + command_description += "." + + # Format detail description, line wrap manually so that unlike the + # default formatter_class used for subparser we can have + # newlines/paragraphs in the detailed description. + if hasattr(command_cls, 'help_detail'): + command_description += "\n\n" + command_description += textwrap.dedent(command_cls.help_detail) + + command_description = textwrap.dedent(""" + description: + """) + _wrap_paragraphs(command_description, 120) + + subparser = subparsers.add_parser( + command_cls.name, + formatter_class=SubcommandHelpFormatter, + help=command_cls.help_short, + epilog=command_description) + if callable(getattr(command_cls, 'options', None)): + command_cls.options(subparser) + if callable(getattr(command_cls, 'config_schema', None)): + section_schema = config_schema.get_section_schema(command_cls.name) + command_cls.config_schema(section_schema) + subparser.set_defaults(cls=command_cls) + + # if no option is specified we default to "help" so we have something + # useful to show to the user instead of an error because of missing + # subcommand + args = parser.parse_args(argv or sys.argv[1:] or ["help"]) + config = compdb.config.LazyTypedConfig(config_schema) + + if args.config_overrides: + # config_overrides is a list of tuples: (var, value) + config_overrides = [] + for override in args.config_overrides: + var, sep, value = override.partition('=') + if not sep: + value = 'yes' + config_overrides.append((var, value)) + config.set_overrides(config_overrides) + + command = args.cls() + command.set_env(env) + command.set_config(config) + command.set_args(args) + + command.execute() diff --git a/thirdparty/compdb/complementer/__init__.py b/thirdparty/compdb/complementer/__init__.py new file mode 100644 index 0000000..a9f35f9 --- /dev/null +++ b/thirdparty/compdb/complementer/__init__.py @@ -0,0 +1,39 @@ +import compdb + + +class ComplementerInterface(object): + """Provides a method to compute a compilation datbase complement. + + .. seealso:: complement() + """ + + def complement(self, layers): + """Compute the complements of multiple layers of databases. + + This method should provide compile commands of files not present in the + compilations databases but that are part of the same project. + + Multiple databases are passed as argument so that the complementer has + the opportunity to reduce duplicates and assign each file to the most + fitting database. + + Example use case #1: + Imagine a build system with one build directory/compdb per target, + 3 targets: + 1. libfoo Foo.h (header-only, no foo.cpp to take options from) + 2. foo-test FooTest.cpp (tests Foo.h, best candidate for the + compile options) + 3. foo-example main.cpp + Includes Foo.h but is not a very good fit compared to + FooTest.cpp in #2 + + Example use case #2: + A multi-compdb project has: + - headers in project A + - project B includes project A headers + + In this multi-project setup, the complementer should have + the opportunity to complement project A's database with the headers + over project B which uses the headers "more indirectly". + """ + raise compdb.NotImplementedError diff --git a/thirdparty/compdb/complementer/headerdb.py b/thirdparty/compdb/complementer/headerdb.py new file mode 100644 index 0000000..b3e00dc --- /dev/null +++ b/thirdparty/compdb/complementer/headerdb.py @@ -0,0 +1,281 @@ +from __future__ import print_function, unicode_literals, absolute_import + +import os +import re + +from compdb.db.memory import InMemoryCompilationDatabase +from compdb.complementer import ComplementerInterface +from compdb.models import CompileCommand + + +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 CompileCommand( + 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 = {} + l = 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] > l: + l = table[i, j] + return l + + +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: + 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 + + +class Complementer(ComplementerInterface): + def complement(self, layers): + return make_headerdb(layers) diff --git a/thirdparty/compdb/config.py b/thirdparty/compdb/config.py new file mode 100644 index 0000000..0465cb0 --- /dev/null +++ b/thirdparty/compdb/config.py @@ -0,0 +1,279 @@ +from __future__ import print_function, unicode_literals, absolute_import + +import configparser +import os +import sys + +import compdb.utils + + +def _xdg_config_home(): + """Return a path under XDG_CONFIG_HOME directory (defaults to '~/.config'). + + https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + """ + return os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) + + +def _win32_config_dir(): + """Return resource under APPDATA directory. + + https://technet.microsoft.com/en-us/library/cc749104(v=ws.10).aspx + """ + # Purposefully use a syntax that triggers an error + # if the APPDATA environment variable does not exists. + # It's not clear what should be the default. + return os.environ['APPDATA'] + + +def _macos_config_dir(): + """Return path under macOS specific configuration directory. + """ + # What should the directory be? + # ~/Library/Application Support/ + # https://developer.apple.com/library/content/documentation/General/Conceptual/MOSXAppProgrammingGuide/AppRuntime/AppRuntime.html#//apple_ref/doc/uid/TP40010543-CH2-SW13 + # ~/Library/Preferences/ + # Someone said so stackoverflow. + # ~/.config/: + # Same as Linux when XDG_CONFIG_HOME is not defined. + # + # Choose the Linux way until someone with more knowledge complains. + return os.path.expanduser('~/.config') + + +def get_user_conf(): + if sys.platform.startswith('win32'): + config_dir = _win32_config_dir() + elif sys.platform.startswith('darwin'): + config_dir = _macos_config_dir() + else: + # Assume Linux-like behavior for other platforms, + # platforms like FreeBSD should have the same behavior as Linux. + # + # A few platforms would be nice to test: + # - cygwin + # - msys2 + config_dir = _xdg_config_home() + return os.path.join(config_dir, 'compdb', 'config') + + +def locate_dominating_file(name, start_dir=os.curdir): + curdir = os.path.abspath(start_dir) + olddir = None + while not curdir == olddir: + if os.path.exists(os.path.join(curdir, name)): + return curdir + olddir = curdir + curdir = os.path.dirname(curdir) + return None + + +def get_local_conf(): + compdb_dir = locate_dominating_file('.compdb') + if compdb_dir: + return os.path.join(compdb_dir, '.compdb') + return None + + +class OptionInvalidError(ValueError): + '''Raise when a key string of the form '
.