blob: 96890f72c89346ddfb66d670237bf43cdf5bb954 [file] [log] [blame]
# pylint: disable=g-bad-file-header
# pylint: disable=cell-var-from-loop
# Copyright 2016 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tools for working with the Microsoft Visual C++ toolchain."""
from argparse import ArgumentParser
import ntpath
import os
import re
import subprocess
MAX_PATH = 260 # The maximum number of characters in a Windows path.
MAX_OPTION_LENGTH = 10 # The maximum length of a compiler/linker option.
MAX_DRIVE_LENGTH = 3 # The maximum length of a drive.
ASSEMBLY_AS_C_SOURCE = '/Tc'
LIB_SUFFIX = '.lib'
TMP_PATH = '%{tmp}'
PATH = "%{path}"
INCLUDE = "%{include}"
LIB = "%{lib}"
LIB_TOOL = "%{lib_tool}"
supported_cuda_compute_capabilities = [ %{cuda_compute_capabilities} ]
class Error(Exception):
"""Base class for all script-specific errors."""
pass
def Log(s):
"""Print log messages."""
print('msvc_tools.py: {0}'.format(s))
class ArgParser(object):
"""Class that parses gcc/clang-style options for a Windows.
The particular substitutions that are needed are passed to the object.
"""
def __init__(self, driver, argv, substitutions):
self.driver = driver
self.substitutions = substitutions
self.options = []
self.leftover = []
self.target_arch = None
self.compilation_mode = None
self.deps_file = None
self.output_file = None
self.params_file = None
self.support_whole_archive = %{support_whole_archive}
self.global_whole_archive = None
self.is_cuda_compilation = None
self.cuda_log = False
self._ParseArgs(argv)
def ReplaceLibrary(self, arg):
"""Do the actual replacement if necessary."""
if arg == "/WHOLEARCHIVE":
return []
if arg.startswith("/OUT:") or os.path.splitext(arg)[1] not in ['.a', '.lo']:
return [arg]
if self.global_whole_archive or arg.startswith("/WHOLEARCHIVE:"):
if arg.startswith("/WHOLEARCHIVE:"):
arg = arg[len("/WHOLEARCHIVE:"):]
output = subprocess.check_output([LIB_TOOL, "/list", arg]).decode("utf-8")
object_files = []
for line in output.split("\n"):
line = line.strip()
if line.endswith(".o"):
object_files.append(line)
return object_files
return [arg]
def WholeArchivePreprocess(self):
"""Replace library file with object files if /WHOLEARCHIVE is not supported."""
if self.support_whole_archive:
return
options = []
self.global_whole_archive = "/WHOLEARCHIVE" in self.options
for arg in self.options:
options.extend(self.ReplaceLibrary(arg))
self.options = options
def IsCudaCompilation(self):
"""Check if it's a cuda compilation."""
parser = ArgumentParser()
parser.add_argument('-x', nargs=1)
parser.add_argument('--cuda_log', action='store_true')
args, leftover = parser.parse_known_args(self.leftover)
if args.x and args.x[0] == 'cuda':
if args.cuda_log:
Log('Using nvcc')
self.cuda_log = True
self.leftover = leftover
return True
return False
def GetNvccOptions(self):
"""Collect the -nvcc_options values from self.leftover.
Returns:
The list of options that can be passed directly to nvcc.
"""
parser = ArgumentParser()
parser.add_argument('-nvcc_options', nargs='*', action='append')
args, leftover = parser.parse_known_args(self.leftover)
if args.nvcc_options:
self.leftover = leftover
return ['--'+a for a in sum(args.nvcc_options, [])]
return []
def GetOptionValue(self, option):
"""Extract the list of values for option from self.options.
Args:
option: The option whose value to extract, without the leading '/'.
Returns:
A list of values, either directly following the option,
(eg., /opt val1 val2) or values collected from multiple occurrences of
the option (eg., /opt val1 /opt val2).
"""
parser = ArgumentParser(prefix_chars='/')
parser.add_argument('/' + option, nargs='*', action='append')
args, leftover = parser.parse_known_args(self.options)
if args and vars(args)[option]:
self.options = leftover
return sum(vars(args)[option], [])
return []
def GetOptionsForCudaCompilation(self):
"""Get nvcc options with arguments assembled from self.options."""
src_files = [f for f in self.options if
re.search('\.cpp$|\.cc$|\.c$|\.cxx$|\.C$', f)]
if len(src_files) == 0:
raise Error('No source files found for cuda compilation.')
out_file = [ f for f in self.options if f.startswith('/Fo') ]
if len(out_file) != 1:
raise Error('Please sepecify exactly one output file for cuda compilation.')
out = ['-o', out_file[0][len('/Fo'):]]
nvcc_compiler_options = self.GetNvccOptions()
opt_option = self.GetOptionValue('O')
opt = ['-g', '-G']
if (len(opt_option) > 0 and opt_option[0] != 'd'):
opt = ['-O2']
include_options = self.GetOptionValue('I')
includes = ["-I " + include for include in include_options]
defines = self.GetOptionValue('D')
defines = ['-D' + define for define in defines]
undefines = self.GetOptionValue('U')
undefines = ['-U' + define for define in undefines]
# The rest of the unrecongized options should be passed to host compiler
host_compiler_options = [option for option in self.options if option not in (src_files + out_file)]
m_options = ["-m64"]
nvccopts = ['-D_FORCE_INLINES']
for capability in supported_cuda_compute_capabilities:
capability = capability.replace('.', '')
nvccopts += [r'-gencode=arch=compute_%s,"code=sm_%s,compute_%s"' % (
capability, capability, capability)]
nvccopts += nvcc_compiler_options
nvccopts += undefines
nvccopts += defines
nvccopts += m_options
nvccopts += ['--compiler-options="' + " ".join(host_compiler_options) + '"']
nvccopts += ['-x', 'cu'] + opt + includes + out + ['-c'] + src_files
if self.cuda_log:
Log("Running: " + " ".join(["nvcc"] + nvccopts))
self.options = nvccopts
def _MatchOneArg(self, args):
"""Finds a pattern which matches the beginning elements of args.
Args:
args: A list of arguments to replace.
Returns:
A tuple of (number of arguments parsed, action, match groups).
"""
for (regex, action) in self.substitutions:
if isinstance(regex, str):
regex = [regex]
j = 0
matches = []
for r in regex:
if j < len(args):
match = re.compile('^' + r + '$').match(args[j])
else:
match = None
matches.append(match)
j += 1
if None in matches:
continue
groups = []
for m in matches:
groups.extend(m.groups())
return (len(regex), action, groups)
return (0, '', [])
def _ParseArgs(self, argv):
"""Parses argv and replaces its elements using special tokens.
The following is a list of supported tokens. The format is $TOKEN%d, where
%d is the 0-based group number from regex matches of the pattern.
$CREATE_PATH%d: Touches a file at the path in the matching token.
$LOAD_PARAMS%d: Loads an ld-style params file and appends all arguments to
the current argument list by recursively calling
_ParseArgs.
$%d : Numeric token that just replaces the match group with
the value specified in the match list.
$PATH%d : Replaces the match with a Windows-normalized version of
the match; assumes that the match is a path.
$PATH%d_NO_EXT: Same as $PATH but strips out any file extension.
$TARGET_ARCH : Set self.target_arch to 'x86' or 'x64' for '-m32' and
'-m64', respectively.
$DEBUG_RT : Enforce linkage to debug runtime.
$COMPILE_OUTPUT%d: Sets the output name of a compilation step.
$COMPILATION_MODE: Sets self.compilation_mode from the value of a
'-Xcompilation-mode=' flag.
$CREATE_PRECOMPILED_HEADER: Informs the system that we are generating a
precompiled header rather than an object file.
$GENERATE_DEPS%d: Generates a gcc-style .d file containing dependencies.
Args:
argv: A list of arguments to replace.
Returns:
A list of replaced arguments to pass to the target command.
Raises:
Error: if wrong arguments found
"""
i = 0
matched = []
unmatched = []
files = []
enforce_debug_rt = False
while i < len(argv):
num_matched, action, groups = self._MatchOneArg(argv[i:])
arg = argv[i]
if arg.startswith('/Fo') or arg.startswith('/Fa') or arg.startswith(
'/Fi'):
self.output_file = arg[3:]
self.options.append(
'/Fd%s.pdb' % self.NormPath(os.path.splitext(self.output_file)[0]))
if num_matched == 0:
# Strip out any .a's that have 0 size, they are header or intermediate
# dependency libraries and don't contain any code. 0-length files are
# considered corrupt by the linker (error LNK1136).
if (os.path.isfile(arg) and os.path.splitext(arg)[1] == '.a' and
os.path.getsize(arg) == 0):
i += 1
continue
# If the argument is an absolute path, then add it directly.
if arg[0] == '/':
self.AddOpt(arg)
elif os.path.isfile(arg):
path = self.NormPath(arg)
ext = os.path.splitext(arg)[1].lower()
if ext in ['.s']:
# Treat assembly files as C source files using a special option.
path = ASSEMBLY_AS_C_SOURCE + path
# If this is an actual file on disk then just pass it to the tool.
files.append(path)
elif not arg.endswith(LIB_SUFFIX):
# Ignore .lib files.
unmatched.append(arg)
i += 1
continue
matched += argv[i:i + num_matched]
# Handle special options.
for entry in action:
if entry == '$TARGET_ARCH':
if arg == '-m32':
self.target_arch = 'x86'
elif arg == '-m64':
self.target_arch = 'x64'
else:
raise Error('Unknown target arch flag: %r' % arg)
continue
if entry == '$COMPILATION_MODE':
empty, prefix, mode = arg.partition('-Xcompilation-mode=')
if empty or not prefix or mode not in ['dbg', 'fastbuild', 'opt']:
raise Error('Invalid compilation mode flag: %r' % arg)
self.compilation_mode = mode
continue
if entry == '$DEBUG_RT':
enforce_debug_rt = True
continue
if not groups:
self.options.append(entry)
else:
# Substitute special tokens.
for g in range(0, len(groups)):
value = groups[g]
# Check for special tokens.
if entry == ('$CREATE_PATH%d' % g):
with open(value, 'a'):
os.utime(value, None)
continue
if entry == ('$LOAD_PARAMS%d' % g):
try:
# The arguments in the params file need to be processed as
# regular command-line arguments.
params = [line.rstrip() for line in open(value, 'r')]
self._ParseArgs(params)
# Because we have no write permission to orginal params file,
# create a new params file with addtional suffix
self.params_file = value + '.msvc'
except (IOError, e):
print('Could not open', value, 'for reading:', str(e))
exit(-1)
continue
if entry == ('$GENERATE_DEPS%d' % g):
self.options.append('/showIncludes')
self.deps_file = value
continue
# Regular substitution.
patterns = {
'$%d' % g: value,
'$PATH%d_NO_EXT' % g: self.NormPath(os.path.splitext(value)[0]),
'$PATH%d' % g: self.NormPath(value),
}
pattern = re.compile('(%s)' %
'|'.join(map(re.escape, patterns.keys())))
result = pattern.sub(lambda x: patterns[x.group(0)], entry)
self.options.append(result)
i += num_matched
self.leftover = unmatched
# Select runtime option
# Find the last runtime option passed
rt = None
rt_idx = -1
for i, opt in enumerate(reversed(self.options)):
if opt in ['/MT', '/MTd', '/MD', '/MDd']:
if opt[-1] == 'd':
enforce_debug_rt = True
rt = opt[:3]
rt_idx = len(self.options) - i - 1
break
rt = rt or '/MT' # Default to static runtime
# Add debug if necessary
if enforce_debug_rt:
rt += 'd'
# Include runtime option
if rt_idx >= 0:
self.options[rt_idx] = rt
else:
self.options.append(rt)
# Add in any parsed files
self.options += files
if '/w' in self.options:
self.options = [option for option in self.options if option not in ['/W2', '/W3', '/W4']]
self.is_cuda_compilation = self.IsCudaCompilation()
if self.is_cuda_compilation:
self.GetOptionsForCudaCompilation()
if self.leftover:
print('Warning: Unmatched arguments: ' + ' '.join(self.leftover))
def NormPath(self, path):
"""Uses the current WindowsRunner to normalize the passed path.
Args:
path: the path to normalize.
Returns:
A normalized string representing a path suitable for Windows.
"""
return self.driver.NormPath(path)
def AddOpt(self, option):
"""Adds a single option.
Args:
option: the option to add.
"""
self.options.append(option)
class WindowsRunner(object):
"""Base class that encapsulates the details of running a binary."""
def NormPath(self, path):
"""Normalizes an input unix style path to a < 260 char Windows format.
Windows paths cannot be greater than 260 characters.
Args:
path: A path in unix format.
Returns:
An absolute path in Windows format, rooted from some
directory.
Raises:
Error: if path is too long
"""
abspath = os.path.abspath(path)
# We must allow for the drive letter as well, which is three characters, and
# the length of any compiler option ahead of the path,
if len(abspath) + MAX_DRIVE_LENGTH + MAX_OPTION_LENGTH > MAX_PATH:
print('Warning: path "' + abspath + '" is > than 260 characters (' +
str(len(abspath)) + '); programs may crash with long arguments')
return abspath
def SetupEnvironment(self):
"""Setup proper path for running.
Returns:
An environment suitable for running on Windows.
"""
build_env = os.environ.copy()
build_env['PATH'] = PATH
build_env['INCLUDE'] = INCLUDE
build_env['LIB'] = LIB
build_env['TEMP'] = TMP_PATH
build_env['TMP'] = TMP_PATH
return build_env
def RunBinary(self, binary, args, build_arch, parser):
"""Runs binary on Windows with the passed args.
Args:
binary: The binary to run.
args: The arguments to pass to binary.
build_arch: Either 'x64' or 'x86', which binary architecture to build for.
parser: An ArgParser that contains parsed arguments.
Returns:
The return code from executing binary.
"""
# Filter out some not-so-useful cl windows messages.
filters = [
'.*warning LNK4006: __NULL_IMPORT_DESCRIPTOR already defined.*\n',
'.*warning LNK4044: unrecognized option \'/MT\'; ignored.*\n',
'.*warning LNK4044: unrecognized option \'/link\'; ignored.*\n',
'.*warning LNK4221: This object file does not define any '
'previously.*\n',
# Comment the following line if you want to see warning messages
'.*warning C.*\n',
'\r\n',
'\n\r',
]
# Check again the arguments are within MAX_PATH.
for arg in args:
if os.path.splitext(arg)[1].lower() in ['.c', '.cc', '.cpp', '.s']:
# cl.exe prints out the file name it is compiling; add that to the
# filter.
name = arg.rpartition(ntpath.sep)[2]
filters.append(name)
# Setup the Windows paths and the build environment.
build_env = self.SetupEnvironment()
# Construct a large regular expression for all filters.
output_filter = re.compile('(' + ')|('.join(filters) + ')')
includes_filter = re.compile(r'Note: including file:\s+(.*)')
# Run the command.
if parser.params_file:
try:
# Using parameter file as input when linking static libraries.
params_file = open(parser.params_file, 'w')
for arg in args:
params_file.write(arg + '\n')
params_file.close()
except (IOError, e):
print('Could not open', parser.params_file, 'for writing:', str(e))
exit(-1)
cmd = [binary] + [('@' + os.path.normpath(parser.params_file))]
else:
cmd = [binary] + args
# Save stderr output to a temporary in case we need it.
# Unconmment the following line to see what exact command is executed.
# print("Running: " + " ".join(cmd))
proc = subprocess.Popen(cmd,
env=build_env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True)
deps = []
for line in proc.stdout:
line = line.decode('utf-8')
if not output_filter.match(line):
includes = includes_filter.match(line)
if includes:
filename = includes.group(1).rstrip()
deps += [filename]
else:
print(line.rstrip())
proc.wait()
# Generate deps file if requested.
if parser.deps_file:
with open(parser.deps_file, 'w') as deps_file:
# Start with the name of the output file.
deps_file.write(parser.output_file + ': \\\n')
for i, dep in enumerate(deps):
dep = dep.replace('\\', '/').replace(' ', '\\ ')
deps_file.write(' ' + dep)
if i < len(deps) - 1:
deps_file.write(' \\')
deps_file.write('\n')
return proc.returncode