blob: 49c2a194f214e8e19f393242867821fabdcb990e [file] [log] [blame]
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
#
# LOCAL SETUP:
# Make sure you have bazel installed first.
# `sudo apt-get install bazel`
# This script is designed to work on Linux only, but may work on other platforms
# with small changes.
#
# WHAT:
# This script creates the BUILD.gn file for XNNPACK by reverse-engineering the
# bazel build.
# HOW:
# By setting the -s option on the bazel build command, bazel logs each compiler
# invocation to the console which is then scraped and put into a configuration
# that gn will accept.
#
# WHY:
# The biggest difficulty of this process is that gn expects each source's
# basename to be unique within a single source set. For example, including both
# "bar/foo.c" and "baz/foo.c" in the same source set is illegal. Therefore, each
# source set will only contain sources from a single directory.
# However, some sources within the same directory may need different compiler
# flags set, so source sets are further split by their flags.
import collections
import logging
import os
import shutil
import subprocess
from operator import attrgetter
_HEADER = '''
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# THIS FILE IS AUTO-GENERATED. DO NOT EDIT.
#
# See //third_party/xnnpack/generate_build_gn.py
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
config("xnnpack_config") {
include_dirs = [
"//third_party/pthreadpool/src/include",
"src/deps/clog/include",
"src/include",
"src/src",
]
cflags=[
"-Wno-unused-function",
]
defines = [
# Don't enable this without first talking to Chrome Security!
# XNNPACK runs in the browser process. The hardening and fuzzing needed
# to ensure JIT can be used safely is not in place yet.
"XNN_ENABLE_JIT=0",
"XNN_ENABLE_ASSEMBLY=1",
"XNN_ENABLE_GEMM_M_SPECIALIZATION=1",
"XNN_ENABLE_MEMOPT=1",
"XNN_ENABLE_SPARSE=1",
"XNN_LOG_LEVEL=0",
"XNN_LOG_TO_STDIO=0",
]
}
'''.strip()
_MAIN_TMPL = '''
source_set("xnnpack") {
public = [ "src/include/xnnpack.h" ]
configs -= [ "//build/config/compiler:chromium_code" ]
configs += [ "//build/config/compiler:no_chromium_code" ]
configs += [ "//build/config/sanitizers:cfi_icall_generalize_pointers" ]
sources = [
%SRCS%
]
deps = [
%TARGETS%,
"//third_party/cpuinfo",
"//third_party/fp16",
"//third_party/fxdiv",
"//third_party/pthreadpool",
]
public_configs = [ ":xnnpack_config" ]
}
'''.strip()
_TARGET_TMPL = '''
source_set("%TARGET_NAME%") {
cflags = [
%ARGS%
]
sources = [
%SRCS%
]
configs -= [ "//build/config/compiler:chromium_code" ]
configs += [ "//build/config/compiler:no_chromium_code" ]
configs += [ "//build/config/sanitizers:cfi_icall_generalize_pointers" ]
deps = [
"//third_party/cpuinfo",
"//third_party/fp16",
"//third_party/fxdiv",
"//third_party/pthreadpool",
]
public_configs = [ ":xnnpack_config" ]
}
'''.strip()
# This is the latest version of the Android NDK that is compatible with
# XNNPACK.
_ANDROID_NDK_VERSION = 'android-ndk-r19c'
_ANDROID_NDK_URL = 'https://dl.google.com/android/repository/android-ndk-r19c-linux-x86_64.zip'
g_android_ndk = None
def _ensure_android_ndk_available():
"""
Ensures that the Android NDK is available to bazel, downloading a new copy if
needed. Raises an Exception if any command fails.
Returns: the full path to the Android NDK
"""
global g_android_ndk
if g_android_ndk:
return g_android_ndk
g_android_ndk = '/tmp/'+_ANDROID_NDK_VERSION
if os.path.exists(g_android_ndk):
logging.info('Using existing Android NDK at ' + g_android_ndk)
return g_android_ndk
logging.info('Downloading new copy of the Android NDK')
zipfile = '/tmp/{ndk}.zip'.format(ndk=_ANDROID_NDK_VERSION)
subprocess.run(['wget', '-O', zipfile, _ANDROID_NDK_URL],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True)
subprocess.run(['unzip', '-o', zipfile, '-d', '/tmp'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True)
return g_android_ndk
# A SourceSet corresponds to a single source_set() gn tuple.
SourceSet = collections.namedtuple(
'SourceSet',
['dir', 'srcs', 'args'],
defaults=['', [], []])
def NameForSourceSet(source_set):
"""
Returns the name to use for a SourceSet in the gn target.
"""
if source_set.dir == 'xnnpack':
return 'xnnpack'
if len(source_set.args) == 0:
return source_set.dir
return '{dir}_{args}'.format(**{
'dir': source_set.dir,
'args': '-'.join([arg[2:] for arg in source_set.args]),
})
# An ObjectBuild corresponds to a single built object, which is parsed from a
# single bazel compiler invocation on a single source file.
ObjectBuild = collections.namedtuple(
'ObjectBuild',
['src', 'dir', 'args'],
defaults=['', '', []])
def _objectbuild_from_bazel_log(log_line):
"""
Attempts to scrape a compiler invocation from a single bazel build output
line. If no invocation is present, None is returned.
"""
split = log_line.strip().split(' ')
if not split[0].endswith('gcc'):
return None
src = ''
dir = ''
args = []
for i, arg in enumerate(split):
if arg == '-c' and split[i + 1].startswith("external/XNNPACK/"):
src = os.path.join('src', split[i + 1][len("external/XNNPACK/"):])
# |src| should look like 'src/src/...'.
src_path = src.split('/')
if len(src_path) == 3:
dir = 'xnnpack'
else:
dir = src_path[2]
if arg.startswith('-m'):
args.append(arg)
return ObjectBuild(src=src, dir=dir, args=args)
def _xnnpack_dir():
"""
Returns the absolute path of //third_party/xnnpack/.
"""
return os.path.dirname(os.path.realpath(__file__))
def _tflite_dir():
"""
Returns the absolute path of //third_party/tflite/src/tensorflow/lite/.
"""
tp_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
return os.path.join(tp_dir, "tflite", "src", "tensorflow", "lite")
def _run_bazel_cmd(args):
"""
Runs a bazel command in the form of bazel <args...>. Returns the stdout and
stderr concatenated, raising an Exception if the command failed.
"""
exec_path = shutil.which("bazel")
if not exec_path:
raise Exception("bazel is not installed. Please run `sudo apt-get install "
+ "bazel` or put the bazel executable in $PATH")
cmd = [exec_path]
cmd.extend(args)
env = os.environ
env.update({
'ANDROID_NDK_HOME': _ensure_android_ndk_available(),
})
proc = subprocess.Popen(cmd,
text=True,
cwd=_tflite_dir(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
raise Exception("bazel command returned non-zero return code:\n"
"cmd: {cmd}\n"
"status: {status}\n"
"stdout: {stdout}\n"
"stderr: {stderr}".format(**{
'cmd': str(cmd),
'status': proc.returncode,
'stdout': stdout,
'stderr': stderr,
})
)
return stdout + "\n" + stderr
def ListAllSrcs():
"""
Runs a bazel command to query and and return all source files for XNNPACK, but
not any dependencies, as relative paths to //third_party/xnnpack/.
"""
logging.info('Querying for the list of all XNNPACK srcs in :tensorflowlite')
out = _run_bazel_cmd([
'cquery',
'kind("source file", deps(:tensorflowlite))',
'--define',
'xnn_enable_jit=false',
])
srcs = []
for line in out.split('\n'):
if line.startswith('@XNNPACK//:'):
srcs.append(os.path.join('src', line.split()[0][len("@XNNPACK//:"):]))
return srcs
def GenerateObjectBuilds(srcs):
"""
Builds XNNPACK with bazel and scrapes out all the ObjectBuild's for all
source files in srcs.
"""
logging.info('Running `bazel clean`')
_run_bazel_cmd(['clean'])
logging.info('Building xnnpack with bazel...')
logs = _run_bazel_cmd([
'build',
':tensorflowlite',
'-s',
'-c', 'opt',
'--define',
'xnn_enable_jit=false',
])
logging.info('scraping %d log lines from bazel build...'
% len(logs.split('\n')))
obs = []
for log in logs.split('\n'):
ob = _objectbuild_from_bazel_log(log)
if ob and ob.src in srcs:
obs.append(ob)
logging.info('Scraped %d built objects' % len(obs))
return obs
def CombineObjectBuildsIntoSourceSets(obs):
"""
Combines all the given ObjectBuild's into SourceSet's by combining source
files whose SourceSet name's (that is thier directory and compiler flags)
match.
"""
sss = {}
for ob in obs:
single = SourceSet(dir=ob.dir, srcs=[ob.src], args=ob.args)
name = NameForSourceSet(single)
if name not in sss:
sss[name] = single
else:
ss = sss[name]
ss = ss._replace(srcs=list(set(ss.srcs + [ob.src])))
sss[name] = ss
xxnpack_ss = sss.pop('xnnpack')
logging.info('Generated %d sub targets for xnnpack' % len(sss))
return xxnpack_ss, sorted(
list(sss.values()), key=lambda ss: NameForSourceSet(ss))
def MakeTargetSourceSet(ss):
"""
Generates the BUILD file text for a build target that supports the main
XNNPACK target, returning it as a string.
"""
target = _TARGET_TMPL
target = target.replace('%ARGS%', ',\n'.join([
' "%s"' % arg for arg in sorted(ss.args)
]))
target = target.replace('%SRCS%', ',\n'.join([
' "%s"' % src for src in sorted(ss.srcs)
]))
target = target.replace('%TARGET_NAME%', NameForSourceSet(ss))
return target
def MakeXNNPACKSourceSet(ss, other_targets):
"""
Generates the BUILD file text for the main XNNPACK build target, given the
XNNPACK SourceSet and the names of all its supporting targets.
"""
target = _MAIN_TMPL
target = target.replace('%SRCS%', ',\n'.join([
' "%s"' % src for src in sorted(ss.srcs)
]))
target = target.replace('%TARGETS%', ',\n'.join([
' ":%s"' % t for t in sorted(other_targets)
]))
return target
def main():
logging.basicConfig(level=logging.INFO)
srcs = ListAllSrcs()
obs = GenerateObjectBuilds(srcs)
xnnpack_ss, other_sss = CombineObjectBuildsIntoSourceSets(obs)
sub_targets = []
for ss in other_sss:
sub_targets.append(MakeTargetSourceSet(ss))
xnnpack_target = MakeXNNPACKSourceSet(
xnnpack_ss,
[NameForSourceSet(ss) for ss in other_sss])
out_path = os.path.join(_xnnpack_dir(), 'BUILD.gn')
logging.info('Writing to ' + out_path)
with open(out_path, 'w') as f:
f.write(_HEADER)
f.write('\n')
f.write(xnnpack_target)
f.write('\n\n')
for target in sub_targets:
f.write(target)
f.write('\n\n')
logging.info('Done! Please run `git cl format`')
if __name__ == "__main__":
main()