blob: b5228cfe672651a56cadfe8ca3d57562da8403e7 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2014 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities for running build commands.
A set of utilities for running build commands.
"""
import errno
import os
import subprocess
import sys
import tempfile
import time
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import pynacl.platform
class Error(Exception):
pass
def FixPath(path):
# On Windows, |path| can be a long relative path: ..\..\..\..\out\Foo\bar...
# If the full path -- os.path.join(os.getcwd(), path) -- is longer than 255
# characters, then any operations that open or check for existence will fail.
# We can't use os.path.abspath here, because that calls into a Windows
# function that still has the path length limit. Instead, we'll cheat and
# normalize the path lexically.
path = os.path.normpath(os.path.join(os.getcwd(), path))
if pynacl.platform.IsWindows():
if len(path) > 255:
raise Error('Path "%s" is too long (%d characters), and will fail.' % (
path, len(path)))
return path
def IsFile(path):
return os.path.isfile(FixPath(path))
def MakeDir(outdir):
outdir = FixPath(outdir)
if outdir and not os.path.exists(outdir):
# There may be a race creating this directory, so ignore failure.
try:
os.makedirs(outdir)
except OSError:
pass
def RemoveFile(path):
os.remove(FixPath(path))
class CommandRunner(object):
"""Basic commandline runner that can run and log commands."""
def __init__(self, options):
self.deferred_log = []
self.commands_are_scripts = False
self.verbose = options.verbose
def SetCommandsAreScripts(self, v):
self.commands_are_scripts = v
def Log(self, msg):
if self.verbose:
sys.stderr.write(str(msg) + '\n')
else:
self.deferred_log.append(str(msg) + '\n')
def EmitDeferredLog(self):
for line in self.deferred_log:
sys.stderr.write(line)
self.deferred_log = []
def CleanOutput(self, out):
if IsFile(out):
# Since nobody can remove a file opened by somebody else on Windows,
# we will retry removal. After trying certain times, we gives up
# and reraise the WindowsError.
retry = 0
while True:
try:
RemoveFile(out)
return
except WindowsError, inst:
# When the errno is errno.ENOENT, we consider the output file
# has been already removed somewhere.
if inst.errno == errno.ENOENT:
return
if retry > 5:
raise Error('FAILED to CleanOutput: %s : %s' % (out, inst))
self.Log('WindowsError %s while removing %s retry=%d' %
(inst, out, retry))
sleep_time = 2**retry
sleep_time = sleep_time if sleep_time < 10 else 10
time.sleep(sleep_time)
retry += 1
def Run(self, cmd_line, get_output=False, normalize_slashes=True,
possibly_script=True, **kwargs):
"""Helper which runs a command line.
Returns the error code if get_output is False.
Returns the output if get_output is True.
"""
if normalize_slashes:
# Use POSIX style path on Windows for POSIX based toolchains
# (just for arguments, not for the path to the command itself).
# If Run() is not invoking a POSIX based toolchain there is no
# need to do this normalization.
cmd_line = ([cmd_line[0]] +
[cmd.replace('\\', '/') for cmd in cmd_line[1:]])
# Windows has a command line length limitation of 8191 characters,
# so store commands in a response file ("@foo") if needed.
temp_file = None
if len(' '.join(cmd_line)) > 8000:
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(' '.join(cmd_line[1:]))
cmd_line = [cmd_line[0], '@' + temp_file.name]
self.Log(' '.join(cmd_line))
try:
runner = subprocess.check_output if get_output else subprocess.call
if (possibly_script and self.commands_are_scripts and
pynacl.platform.IsWindows()):
# Executables that are scripts and not binaries don't want to run
# on Windows without a shell.
result = runner(' '.join(cmd_line), shell=True, **kwargs)
else:
result = runner(cmd_line, **kwargs)
except Exception as err:
raise Error('%s\nFAILED: %s' % (' '.join(cmd_line), str(err)))
finally:
if temp_file is not None:
RemoveFile(temp_file.name)
return result