Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python | |
| 2 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 import datetime | |
| 7 import fnmatch | |
| 8 import logging | |
| 9 import os | |
| 10 import os.path | |
| 11 import queue as Queue | |
| 12 import sublime | |
| 13 import sublime_plugin | |
| 14 import subprocess | |
| 15 import tempfile | |
| 16 import threading | |
| 17 import time | |
| 18 | |
| 19 # Change to an absolute reference if ninja is not on your path | |
| 20 path_to_ninja = "ninja" | |
| 21 | |
| 22 def find_ninja_file(ninja_root_path, relative_file_path_to_find): | |
| 23 ''' | |
| 24 Returns the first *.ninja file in ninja_root_path that contains | |
| 25 relative_file_path_to_find. Otherwise, returns None. | |
| 26 ''' | |
| 27 matches = [] | |
| 28 for root, dirnames, filenames in os.walk(ninja_root_path): | |
| 29 for filename in fnmatch.filter(filenames, '*.ninja'): | |
| 30 matches.append(os.path.join(root, filename)) | |
| 31 logging.debug("Found %d Ninja targets", len(matches)) | |
| 32 | |
| 33 for ninja_file in matches: | |
| 34 for line in open(ninja_file): | |
| 35 if relative_file_path_to_find in line: | |
| 36 return ninja_file | |
| 37 return None | |
| 38 | |
| 39 class PrintOutputCommand(sublime_plugin.TextCommand): | |
| 40 def run(self, edit, **args): | |
| 41 self.view.set_read_only(False) | |
| 42 self.view.insert(edit, self.view.size(), args['text']) | |
| 43 self.view.show(self.view.size()) | |
| 44 self.view.set_read_only(True) | |
| 45 | |
| 46 class CompileCurrentFile(sublime_plugin.TextCommand): | |
| 47 # static thread so that we don't try to run more than once at a time. | |
| 48 thread = None | |
| 49 lock = threading.Lock() | |
| 50 | |
| 51 def __init__(self, args): | |
| 52 super(CompileCurrentFile, self).__init__(args) | |
| 53 self.thread_id = threading.current_thread().ident | |
| 54 self.text_to_draw = "" | |
| 55 self.interrupted = False | |
| 56 | |
| 57 def description(self): | |
| 58 return ("Compiles the file in the current view using Ninja, so all that " | |
| 59 "this file and it's project depends on will be built first\n" | |
| 60 "Note that this command is a toggle so invoking it while it runs " | |
| 61 "will interrupt it.") | |
| 62 | |
| 63 def draw_panel_text(self): | |
| 64 """Draw in the output.exec panel the text accumulated in self.text_to_draw. | |
| 65 | |
| 66 This must be called from the main UI thread (e.g., using set_timeout). | |
| 67 """ | |
| 68 assert self.thread_id == threading.current_thread().ident | |
| 69 logging.debug("draw_panel_text called.") | |
| 70 self.lock.acquire() | |
| 71 text_to_draw = self.text_to_draw | |
| 72 self.text_to_draw = "" | |
| 73 self.lock.release() | |
| 74 | |
| 75 if len(text_to_draw): | |
| 76 self.output_panel.run_command('print_output', {'text': text_to_draw}) | |
| 77 self.view.window().run_command("show_panel", {"panel": "output.exec"}) | |
| 78 logging.debug("Added text:\n%s.", text_to_draw) | |
| 79 | |
| 80 def update_panel_text(self, text_to_draw): | |
| 81 self.lock.acquire() | |
| 82 self.text_to_draw += text_to_draw | |
| 83 self.lock.release() | |
| 84 sublime.set_timeout(self.draw_panel_text, 0) | |
| 85 | |
| 86 def execute_command(self, command, cwd): | |
| 87 """Execute the provided command and send ouput to panel. | |
| 88 | |
| 89 Because the implementation of subprocess can deadlock on windows, we use | |
| 90 a Queue that we write to from another thread to avoid blocking on IO. | |
| 91 | |
| 92 Args: | |
| 93 command: A list containing the command to execute and it's arguments. | |
| 94 Returns: | |
| 95 The exit code of the process running the command or, | |
| 96 1 if we got interrupted. | |
| 97 -1 if we couldn't start the process | |
| 98 -2 if we couldn't poll the running process | |
| 99 """ | |
| 100 logging.debug("Running command: %s", command) | |
| 101 | |
| 102 def EnqueueOutput(out, queue): | |
| 103 """Read all the output from the given handle and insert it into the queue. | |
| 104 | |
| 105 Args: | |
| 106 queue: The Queue object to write to. | |
| 107 """ | |
| 108 while True: | |
| 109 # This readline will block until there is either new input or the handle | |
| 110 # is closed. Readline will only return None once the handle is close, so | |
| 111 # even if the output is being produced slowly, this function won't exit | |
| 112 # early. | |
| 113 # The potential dealock here is acceptable because this isn't run on the | |
| 114 # main thread. | |
| 115 data = out.readline() | |
| 116 if not data: | |
| 117 break | |
| 118 queue.put(data, block=True) | |
| 119 out.close() | |
| 120 | |
| 121 try: | |
| 122 os.chdir(cwd) | |
| 123 proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, | |
| 124 stderr=subprocess.STDOUT, stdin=subprocess.PIPE) | |
| 125 except OSError as e: | |
| 126 logging.exception('Execution of %s raised exception: %s.', (command, e)) | |
| 127 return -1 | |
| 128 | |
| 129 # Use a Queue to pass the text from the reading thread to this one. | |
| 130 stdout_queue = Queue.Queue() | |
| 131 stdout_thread = threading.Thread(target=EnqueueOutput, | |
| 132 args=(proc.stdout, stdout_queue)) | |
| 133 stdout_thread.daemon = True # Ensure this exits if the parent dies | |
| 134 stdout_thread.start() | |
| 135 | |
| 136 # We use the self.interrupted flag to stop this thread. | |
| 137 while not self.interrupted: | |
| 138 try: | |
| 139 exit_code = proc.poll() | |
| 140 except OSError as e: | |
| 141 logging.exception('Polling execution of %s raised exception: %s.', | |
| 142 command, e) | |
| 143 return -2 | |
| 144 | |
| 145 # Try to read output content from the queue | |
| 146 current_content = "" | |
| 147 for _ in range(2048): | |
| 148 try: | |
| 149 current_content += stdout_queue.get_nowait().decode('utf-8') | |
| 150 except Queue.Empty: | |
| 151 break | |
| 152 self.update_panel_text(current_content) | |
| 153 current_content = "" | |
| 154 if exit_code is not None: | |
| 155 while stdout_thread.isAlive() or not stdout_queue.empty(): | |
| 156 try: | |
| 157 current_content += | |
| 158 stdout_queue.get(block=True, timeout=1).decode('utf-8') | |
| 159 except Queue.Empty: | |
| 160 # Queue could still potentially contain more input later. | |
| 161 pass | |
| 162 time_length = datetime.datetime.now() - self.start_time | |
| 163 self.update_panel_text("%s\nDone!\n(%s seconds)" % | |
| 164 (current_content, time_length.seconds)) | |
| 165 return exit_code | |
| 166 # We sleep a little to give the child process a chance to move forward | |
| 167 # before we poll it again. | |
| 168 time.sleep(0.1) | |
| 169 | |
| 170 # If we get here, it's because we were interrupted, kill the process. | |
| 171 proc.terminate() | |
| 172 return 1 | |
| 173 | |
| 174 def run(self, edit, target_build): | |
| 175 """The method called by Sublime Text to execute our command. | |
| 176 | |
| 177 Note that this command is a toggle, so if the thread is are already running, | |
| 178 calling run will interrupt it. | |
| 179 | |
| 180 Args: | |
| 181 edit: Sumblime Text specific edit brace. | |
| 182 target_build: Release/Debug/Other... Used for the subfolder of out. | |
| 183 """ | |
| 184 # There can only be one... If we are running, interrupt and return. | |
| 185 if self.thread and self.thread.is_alive(): | |
| 186 self.interrupted = True | |
| 187 self.thread.join(5.0) | |
| 188 self.update_panel_text("\n\nInterrupted current command:\n%s\n" % command) | |
| 189 self.thread = None | |
| 190 return | |
| 191 | |
| 192 # It's nice to display how long it took to build. | |
| 193 self.start_time = datetime.datetime.now() | |
| 194 # Output our results in the same panel as a regular build. | |
| 195 self.output_panel = self.view.window().get_output_panel("exec") | |
| 196 self.output_panel.set_read_only(True) | |
| 197 self.view.window().run_command("show_panel", {"panel": "output.exec"}) | |
| 198 # TODO(mad): Not sure if the project folder is always the first one... ??? | |
| 199 project_folder = self.view.window().folders()[0] | |
| 200 self.update_panel_text("Compiling current file %s\n" % | |
| 201 self.view.file_name()) | |
| 202 # The file must be somewhere under the project folder... | |
| 203 if (project_folder.lower() != | |
| 204 self.view.file_name()[:len(project_folder)].lower()): | |
| 205 self.update_panel_text( | |
| 206 "ERROR: File %s is not in current project folder %s\n" % | |
| 207 (self.view.file_name(), project_folder)) | |
| 208 else: | |
| 209 # Look for a .ninja file that contains our current file. | |
| 210 logging.debug("Searching for Ninja target") | |
| 211 file_relative_path = self.view.file_name()[len(project_folder) + 1:] | |
| 212 output_dir = os.path.join(project_folder, 'out', target_build) | |
| 213 ninja_path = find_ninja_file(output_dir, file_relative_path) | |
| 214 # The ninja file name is needed to construct the full Ninja target path. | |
| 215 if ninja_path is None: | |
| 216 self.update_panel_text( | |
| 217 "ERROR: File %s is not in any Ninja file under %s" % | |
| 218 (file_relative_path, output_dir)) | |
| 219 else: | |
| 220 ninja_relative_path = os.path.relpath(ninja_path, output_dir) | |
| 221 ninja_filename = os.path.splitext(os.path.basename(ninja_path))[0] | |
| 222 source_filename = os.path.basename(self.view.file_name()) | |
| 223 source_object_filename = os.path.splitext(source_filename)[0] + '.o' | |
|
Nico
2016/05/23 13:57:54
fwiw, passing "../../path/to/file.cc^" (with the c
| |
| 224 target = os.path.join(os.path.dirname(ninja_relative_path), | |
| 225 ninja_filename, | |
| 226 source_object_filename) | |
| 227 command = [ | |
| 228 path_to_ninja, "-C", os.path.join(project_folder, 'out', | |
| 229 target_build), | |
| 230 target] | |
| 231 print(command) | |
| 232 self.interrupted = False | |
| 233 self.thread = threading.Thread(target=self.execute_command, | |
| 234 kwargs={"command":command, | |
| 235 "cwd": output_dir}) | |
| 236 self.thread.start() | |
| 237 | |
| 238 time_length = datetime.datetime.now() - self.start_time | |
| 239 logging.debug("Took %s seconds on UI thread to startup", | |
| 240 time_length.seconds) | |
| 241 self.view.window().focus_view(self.view) | |
| OLD | NEW |