blob: ca957ece603c1f61a0b46f06c808277275ac8dc1 [file] [log] [blame]
# 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.insert(edit, self.view.size(), args['text'])
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.")
text_to_draw = self.text_to_draw
self.text_to_draw = ""
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.text_to_draw += text_to_draw
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.
command: A list containing the command to execute and it's arguments.
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.
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:
queue.put(data, block=True)
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
# We use the self.interrupted flag to stop this thread.
while not self.interrupted:
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):
current_content += stdout_queue.get_nowait().decode('utf-8')
except Queue.Empty:
current_content = ""
if exit_code is not None:
while stdout_thread.isAlive() or not stdout_queue.empty():
current_content += stdout_queue.get(
block=True, timeout=1).decode('utf-8')
except Queue.Empty:
# Queue could still potentially contain more input later.
time_length = - 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.
# If we get here, it's because we were interrupted, kill the process.
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.
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.update_panel_text("\n\nInterrupted current command:\n%s\n" % command)
self.thread = None
# It's nice to display how long it took to build.
self.start_time =
# Output our results in the same panel as a regular build.
self.output_panel = self.view.window().get_output_panel("exec")
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" %
# The file must be somewhere under the project folder...
if (project_folder.lower() !=
"ERROR: File %s is not in current project folder %s\n" %
(self.view.file_name(), project_folder))
output_dir = os.path.join(project_folder, 'out', target_build)
source_relative_path = os.path.relpath(self.view.file_name(),
command = [
path_to_ninja, "-C", os.path.join(project_folder, 'out',
source_relative_path + '^']
self.update_panel_text(' '.join(command) + '\n')
self.interrupted = False
self.thread = threading.Thread(target=self.execute_command,
"cwd": output_dir})
time_length = - self.start_time
logging.debug("Took %s seconds on UI thread to startup",