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 |