Skip to content

Commit

Permalink
Replace gyp win_tool.py with tool_wrapper.py (#492)
Browse files Browse the repository at this point in the history
win_tool.py is incompatible with Python 3. This ports Chromium's
tool_wrapper.py to our buildroot, but doesn't port their full, updated
resource compiler (rc) toolchain.

Chromium moved to a model where they wrap rc in an rc.py script and
support resource compilation on Win, Mac, and Linux. This adds an
additional CIPD dependency that we can avoid. Instead, this ports the
ExecRcWrapper implementation from gyp's win_tool.py, with fixes for
Python 3.

Upstream source:
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/build/toolchain/win

Replaces gyp win_tool.py:
https://chromium.googlesource.com/external/gyp.git/+/refs/heads/master/pylib/gyp/win_tool.py

Issue: flutter/flutter#83043
  • Loading branch information
cbracken committed Jul 30, 2021
1 parent 81b4912 commit ed767ed
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 20 deletions.
25 changes: 13 additions & 12 deletions build/toolchain/win/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ assert(is_win)
# Its arguments are the VS path and the compiler wrapper tool. It will write
# "environment.x86" and "environment.x64" to the build directory and return a
# list to us.
gyp_win_tool_path =
rebase_path("//third_party/gyp/pylib/gyp/win_tool.py", root_build_dir)

# This tool will is used as a wrapper for various commands below.
tool_wrapper_path = rebase_path("tool_wrapper.py", root_build_dir)

toolchain_data = exec_script("setup_toolchain.py",
[
visual_studio_path,
gyp_win_tool_path,
windows_sdk_path,
visual_studio_runtime_dirs,
current_cpu,
Expand Down Expand Up @@ -125,7 +125,8 @@ template("msvc_toolchain") {
}

tool("rc") {
command = "$python_path gyp-win-tool rc-wrapper $env rc.exe {{defines}} {{include_dirs}} /fo{{output}} {{source}}"
command = "$python_path $tool_wrapper_path rc-wrapper $env rc.exe /nologo {{defines}} {{include_dirs}} /fo{{output}} {{source}}"
depsformat = "msvc"
outputs = [
"{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.res",
]
Expand All @@ -146,7 +147,7 @@ template("msvc_toolchain") {
ml = "ml.exe"
x64 = ""
}
command = "$python_path gyp-win-tool asm-wrapper $env $ml $x64 {{defines}} {{include_dirs}} {{asmflags}} "
command = "$python_path $tool_wrapper_path asm-wrapper $env $ml $x64 {{defines}} {{include_dirs}} {{asmflags}} "
if (is_msvc_assembler) {
command += "-c -o{{output}} {{source}}"
} else {
Expand All @@ -159,7 +160,7 @@ template("msvc_toolchain") {

tool("alink") {
rspfile = "{{output}}.rsp"
command = "$python_path gyp-win-tool link-wrapper $env False lib.exe /nologo /ignore:4221 /OUT:{{output}} @$rspfile"
command = "$python_path $tool_wrapper_path link-wrapper $env False lib.exe /nologo /ignore:4221 /OUT:{{output}} @$rspfile"
description = "LIB {{output}}"
outputs = [
# Ignore {{output_extension}} and always use .lib, there's no reason to
Expand All @@ -179,10 +180,10 @@ template("msvc_toolchain") {
"{{root_out_dir}}/{{target_output_name}}{{output_extension}}.lib" # e.g. foo.dll.lib
rspfile = "${dllname}.rsp"

link_command = "$python_path gyp-win-tool link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb @$rspfile"
link_command = "$python_path $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb @$rspfile"

# TODO(brettw) support manifests
#manifest_command = "$python_path gyp-win-tool manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:${dllname}.manifest"
#manifest_command = "$python_path $tool_wrapper_path manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:${dllname}.manifest"
#command = "cmd /c $link_command && $manifest_command"
command = link_command

Expand All @@ -208,10 +209,10 @@ template("msvc_toolchain") {
rspfile = "$binary_output.rsp"
pdbfile = "$binary_output.pdb"

link_command = "$python_path gyp-win-tool link-wrapper $env False link.exe /nologo /OUT:$binary_output /PDB:$pdbfile @$rspfile"
link_command = "$python_path $tool_wrapper_path link-wrapper $env False link.exe /nologo /OUT:$binary_output /PDB:$pdbfile @$rspfile"

# TODO(brettw) support manifests
#manifest_command = "$python_path gyp-win-tool manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:{{output}}.manifest"
#manifest_command = "$python_path $tool_wrapper_path manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:{{output}}.manifest"
#command = "cmd /c $link_command && $manifest_command"
command = link_command

Expand All @@ -230,13 +231,13 @@ template("msvc_toolchain") {
}

tool("stamp") {
command = "$python_path gyp-win-tool stamp {{output}}"
command = "cmd /c type nul > \"{{output}}\""
description = "STAMP {{output}}"
}

tool("copy") {
command =
"$python_path gyp-win-tool recursive-mirror {{source}} {{output}}"
"$python_path $tool_wrapper_path recursive-mirror {{source}} {{output}}"
description = "COPY {{source}} {{output}}"
}
}
Expand Down
13 changes: 5 additions & 8 deletions build/toolchain/win/setup_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,14 @@ def _CopyTool(source_path):


def main():
if len(sys.argv) != 6:
if len(sys.argv) != 5:
print('Usage setup_toolchain.py '
'<visual studio path> <win tool path> <win sdk path> '
'<visual studio path> <win sdk path> '
'<runtime dirs> <target_cpu>')
sys.exit(2)
tool_source = sys.argv[2]
win_sdk_path = sys.argv[3]
runtime_dirs = sys.argv[4]
target_cpu = sys.argv[5]

_CopyTool(tool_source)
win_sdk_path = sys.argv[2]
runtime_dirs = sys.argv[3]
target_cpu = sys.argv[4]

cpus = ('x86', 'x64', 'arm64')
assert target_cpu in cpus
Expand Down
193 changes: 193 additions & 0 deletions build/toolchain/win/tool_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Copyright (c) 2012 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.

"""Utility functions for Windows builds.
This file is copied to the build directory as part of toolchain setup and
is used to set up calls to tools used by the build that need wrappers.
"""

from __future__ import print_function

import os
import re
import shutil
import subprocess
import stat
import sys


BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# A regex matching an argument corresponding to the output filename passed to
# link.exe.
_LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE)

def main(args):
exit_code = WinTool().Dispatch(args)
if exit_code is not None:
sys.exit(exit_code)


class WinTool(object):
"""This class performs all the Windows tooling steps. The methods can either
be executed directly, or dispatched from an argument list."""

def _UseSeparateMspdbsrv(self, env, args):
"""Allows to use a unique instance of mspdbsrv.exe per linker instead of a
shared one."""
if len(args) < 1:
raise Exception("Not enough arguments")

if args[0] != 'link.exe':
return

# Use the output filename passed to the linker to generate an endpoint name
# for mspdbsrv.exe.
endpoint_name = None
for arg in args:
m = _LINK_EXE_OUT_ARG.match(arg)
if m:
endpoint_name = re.sub(r'\W+', '',
'%s_%d' % (m.group('out'), os.getpid()))
break

if endpoint_name is None:
return

# Adds the appropriate environment variable. This will be read by link.exe
# to know which instance of mspdbsrv.exe it should connect to (if it's
# not set then the default endpoint is used).
env['_MSPDBSRV_ENDPOINT_'] = endpoint_name

def Dispatch(self, args):
"""Dispatches a string command to a method."""
if len(args) < 1:
raise Exception("Not enough arguments")

method = "Exec%s" % self._CommandifyName(args[0])
return getattr(self, method)(*args[1:])

def _CommandifyName(self, name_string):
"""Transforms a tool name like recursive-mirror to RecursiveMirror."""
return name_string.title().replace('-', '')

def _GetEnv(self, arch):
"""Gets the saved environment from a file for a given architecture."""
# The environment is saved as an "environment block" (see CreateProcess
# and msvs_emulation for details). We convert to a dict here.
# Drop last 2 NULs, one for list terminator, one for trailing vs. separator.
pairs = open(arch).read()[:-2].split('\0')
kvs = [item.split('=', 1) for item in pairs]
return dict(kvs)

def ExecDeleteFile(self, path):
"""Simple file delete command."""
if os.path.exists(path):
os.unlink(path)

def ExecRecursiveMirror(self, source, dest):
"""Emulation of rm -rf out && cp -af in out."""
if os.path.exists(dest):
if os.path.isdir(dest):
def _on_error(fn, path, dummy_excinfo):
# The operation failed, possibly because the file is set to
# read-only. If that's why, make it writable and try the op again.
if not os.access(path, os.W_OK):
os.chmod(path, stat.S_IWRITE)
fn(path)
shutil.rmtree(dest, onerror=_on_error)
else:
if not os.access(dest, os.W_OK):
# Attempt to make the file writable before deleting it.
os.chmod(dest, stat.S_IWRITE)
os.unlink(dest)

if os.path.isdir(source):
shutil.copytree(source, dest)
else:
shutil.copy2(source, dest)
# Try to diagnose crbug.com/741603
if not os.path.exists(dest):
raise Exception("Copying of %s to %s failed" % (source, dest))

def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args):
"""Filter diagnostic output from link that looks like:
' Creating library ui.dll.lib and object ui.dll.exp'
This happens when there are exports from the dll or exe.
"""
env = self._GetEnv(arch)
if use_separate_mspdbsrv == 'True':
self._UseSeparateMspdbsrv(env, args)
if sys.platform == 'win32':
args = list(args) # *args is a tuple by default, which is read-only.
args[0] = args[0].replace('/', '\\')
# https://docs.python.org/2/library/subprocess.html:
# "On Unix with shell=True [...] if args is a sequence, the first item
# specifies the command string, and any additional items will be treated as
# additional arguments to the shell itself. That is to say, Popen does the
# equivalent of:
# Popen(['/bin/sh', '-c', args[0], args[1], ...])"
# For that reason, since going through the shell doesn't seem necessary on
# non-Windows don't do that there.
pe_name = None
for arg in args:
m = _LINK_EXE_OUT_ARG.match(arg)
if m:
pe_name = m.group('out')
link = subprocess.Popen(args, shell=sys.platform == 'win32', env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# Read output one line at a time as it shows up to avoid OOM failures when
# GBs of output is produced.
for line in link.stdout:
if (not line.startswith(b' Creating library ')
and not line.startswith(b'Generating code')
and not line.startswith(b'Finished generating code')):
print(line)
return link.wait()

def ExecAsmWrapper(self, arch, *args):
"""Filter logo banner from invocations of asm.exe."""
env = self._GetEnv(arch)
if sys.platform == 'win32':
# Windows ARM64 uses clang-cl as assembler which has '/' as path
# separator, convert it to '\\' when running on Windows.
args = list(args) # *args is a tuple by default, which is read-only
args[0] = args[0].replace('/', '\\')
popen = subprocess.Popen(args, shell=True, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = popen.communicate()
for line in out.decode('utf8').splitlines():
if not line.startswith(' Assembling: '):
print(line)
return popen.returncode

def ExecRcWrapper(self, arch, *args):
"""Filter logo banner from invocations of rc.exe. Older versions of RC
don't support the /nologo flag."""
env = self._GetEnv(arch)
popen = subprocess.Popen(args, shell=True, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = popen.communicate()
for line in out.splitlines():
if (not line.startswith(b'Microsoft (R) Windows (R) Resource Compiler') and
not line.startswith(b'Copyright (C) Microsoft Corporation') and line):
print(line)
return popen.returncode

def ExecActionWrapper(self, arch, rspfile, *dirname):
"""Runs an action command line from a response file using the environment
for |arch|. If |dirname| is supplied, use that as the working directory."""
env = self._GetEnv(arch)
# TODO(scottmg): This is a temporary hack to get some specific variables
# through to actions that are set after GN-time. http://crbug.com/333738.
for k, v in os.environ.items():
if k not in env:
env[k] = v
args = open(rspfile).read()
dirname = dirname[0] if dirname else None
return subprocess.call(args, shell=True, env=env, cwd=dirname)


if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

0 comments on commit ed767ed

Please sign in to comment.