| Index: tools/sublime/compile_current_file.py
|
| diff --git a/tools/sublime/compile_current_file.py b/tools/sublime/compile_current_file.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..04f154aeed139238853c84cd776849821110066d
|
| --- /dev/null
|
| +++ b/tools/sublime/compile_current_file.py
|
| @@ -0,0 +1,236 @@
|
| +#!/usr/bin/python
|
| +# Copyright 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +import datetime
|
| +import fnmatch
|
| +import logging
|
| +import os
|
| +import os.path
|
| +import queue as Queue
|
| +import sublime
|
| +import sublime_plugin
|
| +import subprocess
|
| +import tempfile
|
| +import threading
|
| +import time
|
| +
|
| +# Change to an absolute reference if ninja is not on your path
|
| +path_to_ninja = 'ninja'
|
| +
|
| +def find_ninja_file(ninja_root_path, relative_file_path_to_find):
|
| + '''
|
| + Returns the first *.ninja file in ninja_root_path that contains
|
| + relative_file_path_to_find. Otherwise, returns None.
|
| + '''
|
| + matches = []
|
| + for root, dirnames, filenames in os.walk(ninja_root_path):
|
| + for filename in fnmatch.filter(filenames, '*.ninja'):
|
| + matches.append(os.path.join(root, filename))
|
| + logging.debug("Found %d Ninja targets", len(matches))
|
| +
|
| + for ninja_file in matches:
|
| + for line in open(ninja_file):
|
| + if relative_file_path_to_find in line:
|
| + return ninja_file
|
| + return None
|
| +
|
| +class PrintOutputCommand(sublime_plugin.TextCommand):
|
| + def run(self, edit, **args):
|
| + self.view.set_read_only(False)
|
| + self.view.insert(edit, self.view.size(), args['text'])
|
| + self.view.show(self.view.size())
|
| + self.view.set_read_only(True)
|
| +
|
| +class CompileCurrentFile(sublime_plugin.TextCommand):
|
| + # static thread so that we don't try to run more than once at a time.
|
| + thread = None
|
| + lock = threading.Lock()
|
| +
|
| + def __init__(self, args):
|
| + super(CompileCurrentFile, self).__init__(args)
|
| + self.thread_id = threading.current_thread().ident
|
| + self.text_to_draw = ""
|
| + self.interrupted = False
|
| +
|
| + def description(self):
|
| + return ("Compiles the file in the current view using Ninja, so all that "
|
| + "this file and it's project depends on will be built first\n"
|
| + "Note that this command is a toggle so invoking it while it runs "
|
| + "will interrupt it.")
|
| +
|
| + def draw_panel_text(self):
|
| + """Draw in the output.exec panel the text accumulated in self.text_to_draw.
|
| +
|
| + This must be called from the main UI thread (e.g., using set_timeout).
|
| + """
|
| + assert self.thread_id == threading.current_thread().ident
|
| + logging.debug("draw_panel_text called.")
|
| + self.lock.acquire()
|
| + text_to_draw = self.text_to_draw
|
| + self.text_to_draw = ""
|
| + self.lock.release()
|
| +
|
| + if len(text_to_draw):
|
| + self.output_panel.run_command('print_output', {'text': text_to_draw})
|
| + self.view.window().run_command("show_panel", {"panel": "output.exec"})
|
| + logging.debug("Added text:\n%s.", text_to_draw)
|
| +
|
| + def update_panel_text(self, text_to_draw):
|
| + self.lock.acquire()
|
| + self.text_to_draw += text_to_draw
|
| + self.lock.release()
|
| + sublime.set_timeout(self.draw_panel_text, 0)
|
| +
|
| + def execute_command(self, command, cwd):
|
| + """Execute the provided command and send ouput to panel.
|
| +
|
| + Because the implementation of subprocess can deadlock on windows, we use
|
| + a Queue that we write to from another thread to avoid blocking on IO.
|
| +
|
| + Args:
|
| + command: A list containing the command to execute and it's arguments.
|
| + Returns:
|
| + The exit code of the process running the command or,
|
| + 1 if we got interrupted.
|
| + -1 if we couldn't start the process
|
| + -2 if we couldn't poll the running process
|
| + """
|
| + logging.debug("Running command: %s", command)
|
| +
|
| + def EnqueueOutput(out, queue):
|
| + """Read all the output from the given handle and insert it into the queue.
|
| +
|
| + Args:
|
| + queue: The Queue object to write to.
|
| + """
|
| + while True:
|
| + # This readline will block until there is either new input or the handle
|
| + # is closed. Readline will only return None once the handle is close, so
|
| + # even if the output is being produced slowly, this function won't exit
|
| + # early.
|
| + # The potential dealock here is acceptable because this isn't run on the
|
| + # main thread.
|
| + data = out.readline()
|
| + if not data:
|
| + break
|
| + queue.put(data, block=True)
|
| + out.close()
|
| +
|
| + try:
|
| + os.chdir(cwd)
|
| + proc = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True,
|
| + stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
|
| + except OSError as e:
|
| + logging.exception('Execution of %s raised exception: %s.', (command, e))
|
| + return -1
|
| +
|
| + # Use a Queue to pass the text from the reading thread to this one.
|
| + stdout_queue = Queue.Queue()
|
| + stdout_thread = threading.Thread(target=EnqueueOutput,
|
| + args=(proc.stdout, stdout_queue))
|
| + stdout_thread.daemon = True # Ensure this exits if the parent dies
|
| + stdout_thread.start()
|
| +
|
| + # We use the self.interrupted flag to stop this thread.
|
| + while not self.interrupted:
|
| + try:
|
| + exit_code = proc.poll()
|
| + except OSError as e:
|
| + logging.exception('Polling execution of %s raised exception: %s.',
|
| + command, e)
|
| + return -2
|
| +
|
| + # Try to read output content from the queue
|
| + current_content = ""
|
| + for _ in range(2048):
|
| + try:
|
| + current_content += stdout_queue.get_nowait().decode('utf-8')
|
| + except Queue.Empty:
|
| + break
|
| + self.update_panel_text(current_content)
|
| + current_content = ""
|
| + if exit_code is not None:
|
| + while stdout_thread.isAlive() or not stdout_queue.empty():
|
| + try:
|
| + current_content += stdout_queue.get(
|
| + block=True, timeout=1).decode('utf-8')
|
| + except Queue.Empty:
|
| + # Queue could still potentially contain more input later.
|
| + pass
|
| + time_length = datetime.datetime.now() - self.start_time
|
| + self.update_panel_text("%s\nDone!\n(%s seconds)" %
|
| + (current_content, time_length.seconds))
|
| + return exit_code
|
| + # We sleep a little to give the child process a chance to move forward
|
| + # before we poll it again.
|
| + time.sleep(0.1)
|
| +
|
| + # If we get here, it's because we were interrupted, kill the process.
|
| + proc.terminate()
|
| + return 1
|
| +
|
| + def run(self, edit, target_build):
|
| + """The method called by Sublime Text to execute our command.
|
| +
|
| + Note that this command is a toggle, so if the thread is are already running,
|
| + calling run will interrupt it.
|
| +
|
| + Args:
|
| + edit: Sumblime Text specific edit brace.
|
| + target_build: Release/Debug/Other... Used for the subfolder of out.
|
| + """
|
| + # There can only be one... If we are running, interrupt and return.
|
| + if self.thread and self.thread.is_alive():
|
| + self.interrupted = True
|
| + self.thread.join(5.0)
|
| + self.update_panel_text("\n\nInterrupted current command:\n%s\n" % command)
|
| + self.thread = None
|
| + return
|
| +
|
| + # It's nice to display how long it took to build.
|
| + self.start_time = datetime.datetime.now()
|
| + # Output our results in the same panel as a regular build.
|
| + self.output_panel = self.view.window().get_output_panel("exec")
|
| + self.output_panel.set_read_only(True)
|
| + self.view.window().run_command("show_panel", {"panel": "output.exec"})
|
| + # TODO(mad): Not sure if the project folder is always the first one... ???
|
| + project_folder = self.view.window().folders()[0]
|
| + self.update_panel_text("Compiling current file %s\n" %
|
| + self.view.file_name())
|
| + # The file must be somewhere under the project folder...
|
| + if (project_folder.lower() !=
|
| + self.view.file_name()[:len(project_folder)].lower()):
|
| + self.update_panel_text(
|
| + "ERROR: File %s is not in current project folder %s\n" %
|
| + (self.view.file_name(), project_folder))
|
| + else:
|
| + # Look for a .ninja file that contains our current file.
|
| + logging.debug("Searching for Ninja target")
|
| + file_relative_path = self.view.file_name()[len(project_folder) + 1:]
|
| + output_dir = os.path.join(project_folder, 'out', target_build)
|
| + ninja_path = find_ninja_file(output_dir, file_relative_path)
|
| + # The ninja file name is needed to construct the full Ninja target path.
|
| + if ninja_path is None:
|
| + self.update_panel_text(
|
| + "ERROR: File %s is not in any Ninja file under %s" %
|
| + (file_relative_path, output_dir))
|
| + else:
|
| + source_relative_path = os.path.relpath(self.view.file_name(),
|
| + output_dir)
|
| + command = [
|
| + path_to_ninja, "-C", os.path.join(project_folder, 'out',
|
| + target_build),
|
| + source_relative_path + '^']
|
| + self.update_panel_text(' '.join(command) + '\n')
|
| + self.interrupted = False
|
| + self.thread = threading.Thread(target=self.execute_command,
|
| + kwargs={"command":command,
|
| + "cwd": output_dir})
|
| + self.thread.start()
|
| +
|
| + time_length = datetime.datetime.now() - self.start_time
|
| + logging.debug("Took %s seconds on UI thread to startup",
|
| + time_length.seconds)
|
| + self.view.window().focus_view(self.view)
|
|
|