blob: aa30eb9e91ea5d021aa4706cf93aa5ed5a1f2bd7 [file] [log] [blame]
#!/usr/bin/python3
# This script converts simple Gyp files to their Bazel equivalents.
# It is not intended to be particularly extensible; it is only going to be used
# once over all of our code and then forgotten about.
# This "only has to work on exactly the code we have now" property also means
# it's pretty picky about many things and doesn't try to handle anything
# beyond what we actually have.
#
# It takes a list of folders to deal with.
#
# Running this script requires PyYAML <pyyaml.org> to be installed.
import sys
import os
import yaml
import collections
import re
'''Converts a Gyp filename to its Bazel equivalent.
Args:
gyp_file_name: The name of the gyp file this is contained in.
This is important for resolving relative paths etc.
file: The name of the file to deal with.
'''
def gyp_file_to_bazel(gyp_file_name, file):
if file.startswith('<(AOS)/'):
return '//aos' + file[6:]
elif file.startswith('<(DEPTH)/'):
return '//' + file[9:]
else:
if '(' in file:
raise RuntimeError('Bad variable in file "%s"' % file)
split_gyp = os.path.dirname(gyp_file_name).split('/')
rest = []
dotdots = 0
doing_dotdots = True
for component in file.split('/'):
if component == '..':
dotdots += 1
if not doing_dotdots:
raise RuntimeError('Bad use of .. in file "%s"' % file)
else:
doing_dotdots = False
rest.append(component)
return '/'.join(['/'] + split_gyp[:-dotdots] + rest)
'''Converts a Gyp target to its Bazel equivalent.
Args:
gyp_file_name: The name of the gyp file this is contained in.
This is important for resolving relative paths etc.
target: The name of the target to deal with.
'''
def gyp_target_to_bazel(gyp_file_name, target):
if not ':' in target:
if '.' in target:
raise RuntimeError('Have a filename instead of a target: "%s"' % target)
return ':' + target
if target[0] == ':':
return target
# These thin wrappers won't be copied.
if target == '<(AOS)/build/aos.gyp:logging':
return '//aos/common/logging'
if target == '<(AOS)/build/aos.gyp:logging_interface':
return '//aos/common/logging:logging_interface'
# These are getting moved to the right place manually.
if target == '<(AOS)/common/common.gyp:condition':
return '//aos/linux_code/ipc_lib:condition'
if target == '<(AOS)/common/common.gyp:mutex':
return '//aos/linux_code/ipc_lib:mutex'
if target == '<(AOS)/common/common.gyp:event':
return '//aos/linux_code/ipc_lib:event'
# By building ..., we can mostly ignore these.
if (target == '<(AOS)/build/aos_all.gyp:Prime' or
target == '../../frc971/frc971.gyp:All'):
return '//aos:prime_binaries'
split = target.split(':')
if len(split) != 2:
raise RuntimeError('Not sure how to parse target "%s"' % target)
if split[0] == '<(EXTERNALS)':
return '//third_party/' + split[1]
split_path = split[0].rsplit('/', 1)
if len(split_path) != 2:
raise RuntimeError('TODO(Brian): Handle referring to this .gyp file as %s!' % split[0])
if not split_path[1].endswith('.gyp'):
raise RuntimeError('Not sure how to deal with gyp filename "%s"' % split[0])
folder = gyp_file_to_bazel(gyp_file_name, split_path[0])
if not folder.endswith(split_path[1][:-4]):
raise RuntimeError('Not sure how to deal with non-matching gyp file "%s"' % target)
return '%s:%s' % (folder, split[1])
'''Represents a Bazel build target.
Subclasses represent actual concrete things which are emitted into a BUILD
file.'''
class BuildTarget(object):
def __init__(self, type, name):
self.__type = type
self.__name = name
def add_dep(self, bazel_dep):
self.__deps.append(bazel_dep)
def _type(self):
return self.__type
'''Returns a collections.OrderedDict with all of the attributes on the
Bazel rule this represents.
Subclasses are expected to override this and add their own attributes
in the appropriate order.'''
def attrs(self):
r = collections.OrderedDict()
r['name'] = self.__name
return r
'''Returns a set of load statements.
Subclasses are expected to override this and add their own loads.
Each element of the result is the arguments to a single load call.'''
def loads(self):
return set()
'''Returns the Bazel representation of a given object as an attribute
value.'''
def __to_bazel_string(o):
if isinstance(o, str):
return repr(o)
if hasattr(o, '__iter__'):
r = ['[']
for c in o:
r.append(' %s,' % BuildTarget.__to_bazel_string(c))
r.append(' ]')
return '\n'.join(r)
else:
return str(o)
def __str__(self):
r = [self.__type + '(']
for name, value in self.attrs().items():
if value:
r.append(' %s = %s,' % (name, BuildTarget.__to_bazel_string(value)))
r.append(')')
return '\n'.join(r)
'''Represents a cc_* target.'''
class CcBuildTarget(BuildTarget):
def __init__(self, type, name):
if not type.startswith('cc_'):
raise
super(CcBuildTarget, self).__init__(type, name)
self.__srcs = []
self.__hdrs = []
self.__deps = []
self.__tags = []
def add_src(self, src):
self.__srcs.append(src)
def add_hdr(self, hdr):
self.__hdrs.append(hdr)
def add_dep(self, dep):
self.__deps.append(dep)
def add_tag(self, tag):
if self._type() != 'cc_test':
raise RuntimeError(
'Trying to add tag %s to non-test type %s' % (tag, self._type()))
self.__tags.append(tag)
def attrs(self):
unique_deps = []
for dep in self.__deps:
if dep not in unique_deps:
unique_deps.append(dep)
r = super(CcBuildTarget, self).attrs();
r['srcs'] = self.__srcs
r['hdrs'] = self.__hdrs
r['tags'] = self.__tags
r['deps'] = unique_deps
return r
'''Represents a filegroup target.'''
class FilegroupTarget(BuildTarget):
def __init__(self, name):
super(FilegroupTarget, self).__init__('filegroup', name)
self.__srcs = []
def add_src(self, src):
self.__srcs.append(src)
def attrs(self):
r = super(FilegroupTarget, self).attrs();
r['srcs'] = self.__srcs
return r
'''Represents a queue_library target.'''
class QueueTarget(BuildTarget):
def __init__(self, name):
super(QueueTarget, self).__init__('queue_library', name)
self.__srcs = []
self.__deps = []
def add_src(self, src):
self.__srcs.append(src)
def add_dep(self, dep):
self.__deps.append(dep)
def loads(self):
return set((('/aos/build/queues', 'queue_library'),))
def attrs(self):
r = super(QueueTarget, self).attrs();
r['srcs'] = self.__srcs
r['deps'] = self.__deps
return r
def _warn_attr(keys_to_handle, name, gyp_file_name, attr):
if attr in keys_to_handle:
print('Target %s in %s has %s' % (name, gyp_file_name, attr),
file=sys.stderr)
keys_to_handle.remove(attr)
def main(argv):
for d in argv:
build_targets = []
d = d.rstrip('/')
gyp_file_name = os.path.join(d, os.path.split(d)[-1] + '.gyp')
with open(gyp_file_name, 'r') as gyp_file:
gyp = yaml.load(gyp_file)
if 'targets' not in gyp:
print('No targets entry found in %s!' % gyp_file_name, file=sys.stderr)
return 1
if list(gyp.keys()) != ['targets']:
print('Unknown keys of %s from %s' % (gyp.keys(), gyp_file_name),
file=sys.stderr)
targets = gyp['targets']
for gyp_target in targets:
target = None
keys_to_handle = set(gyp_target.keys())
if 'export_dependent_settings' in gyp_target:
keys_to_handle.remove('export_dependent_settings')
name = gyp_target['target_name']
keys_to_handle.remove('target_name')
_warn_attr(keys_to_handle, name, gyp_file_name, 'actions')
_warn_attr(keys_to_handle, name, gyp_file_name, 'conditions')
_warn_attr(keys_to_handle, name, gyp_file_name, 'copies')
_warn_attr(keys_to_handle, name, gyp_file_name, 'hard_dependency')
_warn_attr(keys_to_handle, name, gyp_file_name,
'direct_dependent_settings')
# These are getting moved to the right place manually.
if gyp_file_name == 'aos/common/common.gyp':
if name == 'condition' or name == 'mutex' or name == 'event':
continue
# By building ..., this becomes irrelevant.
if gyp_file_name == 'frc971/frc971.gyp':
if name == 'All':
continue
if 'variables' in gyp_target:
if 'no_rsync' in gyp_target['variables']:
del gyp_target['variables']['no_rsync']
type = gyp_target['type']
keys_to_handle.remove('type')
if (type in ['static_library', 'executable'] and
not 'includes' in gyp_target):
cc_type = {
'static_library': 'cc_library',
'executable': 'cc_binary',
}[type]
if re.match('.*_test$', name) and cc_type == 'cc_binary':
cc_type = 'cc_test'
target = CcBuildTarget(cc_type, name)
if 'dependencies' in gyp_target:
for dep in gyp_target['dependencies']:
target.add_dep(gyp_target_to_bazel(gyp_file_name, dep))
keys_to_handle.remove('dependencies')
if 'sources' in gyp_target:
for src in gyp_target['sources']:
# In //aos/common:queue_types, this will get dealt with manually
# along with the actions.
if src == '<(print_field_cc)':
continue
if '/' in src:
raise RuntimeError(
'Bad folder for %s in target %s in %s' % (src, name,
gyp_file_name))
target.add_src(src)
# This is sort of a heuristic: if there's a header file matching
# the source file, add it as an hdr. This is going to require some
# manual cleanup, but it'll be close.
src_filename = os.path.join(os.path.dirname(gyp_file_name), src)
if not os.path.exists(src_filename):
raise RuntimeError(
'Can not find source %s in target %s' % (src_filename,
name))
header = src_filename.rsplit('.', 2)[0] + '.h'
if os.path.exists(header):
target.add_hdr(src.rsplit('.', 2)[0] + '.h')
keys_to_handle.remove('sources')
if 'variables' in gyp_target:
vars = gyp_target['variables']
if 'is_special_test' in vars:
if vars['is_special_test'] != 1:
raise RuntimeError(
'Unexpected is_special_test value in target %s' % name)
target.add_tag('manual')
del vars['is_special_test']
elif type == 'none':
target = FilegroupTarget(name)
for dep in gyp_target['dependencies']:
target.add_src(gyp_target_to_bazel(gyp_file_name, dep))
keys_to_handle.remove('dependencies')
elif 'includes' in gyp_target:
includes = gyp_target['includes']
keys_to_handle.remove('includes')
if len(includes) != 1:
raise RuntimeError(
'Not sure how to handle multiple includes in %s' % gyp_target)
include = gyp_file_to_bazel(gyp_file_name, includes[0])
if include == '//aos/build/queues.gypi':
vars = gyp_target['variables']
keys_to_handle.remove('variables')
if 'header_path' not in vars:
raise RuntimeError(
'No header_path for target %s in %s' % (name, gyp_file_name))
if list(vars.keys()) != ['header_path']:
raise RuntimeError(
'Extra variables for target %s in %s' % (name, gyp_file_name))
if vars['header_path'] != os.path.dirname(gyp_file_name):
raise RuntimeError(
'Incorrect header_path for target %s in %s' % (name,
gyp_file_name))
target = QueueTarget(name)
for src in gyp_target['sources']:
if '/' in src:
raise RuntimeError(
'.q src %s in bad dir for target %s in %s' % (src,
name,
gyp_file_name))
target.add_src(src)
keys_to_handle.remove('sources')
if 'dependencies' in gyp_target:
for dep in gyp_target['dependencies']:
target.add_dep(gyp_target_to_bazel(gyp_file_name, dep))
keys_to_handle.remove('dependencies')
else:
raise RuntimeError(
'Unknown include %s for target %s in %s' % (include, name,
gyp_file_name))
else:
raise RuntimeError(
'Unknown type %s for target %s in %s' % (type, name, gyp_file_name))
if not target:
raise
if (gyp_file_name == 'y2015/http_status/http_status.gyp' and
name == 'http_status'):
# We'll handle these manually.
keys_to_handle.remove('include_dirs')
if (gyp_file_name == 'aos/common/common.gyp' and
name == 'queue_types'):
# These will get handled manually as part of dealing with the
# actions.
keys_to_handle.remove('variables')
# If there were variables but they all got deleted, then we don't
# actually have any more to handle.
if 'variables' in keys_to_handle and not gyp_target['variables']:
keys_to_handle.remove('variables')
if keys_to_handle:
raise RuntimeError(
'Unhandled keys for target %s in %s: %s' % (name, gyp_file_name,
keys_to_handle))
build_targets.append(target)
if not build_targets:
print('No output targets for %s' % d, file=sys.stderr)
continue
with open(os.path.join(d, 'BUILD'), 'w') as build_file:
build_file.write(
'package(default_visibility = [\'//visibility:public\'])\n')
loads = set()
for t in build_targets:
loads |= t.loads()
if loads:
build_file.write('\n')
for load in sorted(loads):
build_file.write('load(%s)\n' % (', '.join([repr(part) for part
in load])))
for t in build_targets:
build_file.write('\n')
build_file.write(str(t))
build_file.write('\n')
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))